Skip to content
Lesson 1.10Computer, Game, and Code Foundations

Bits, Masks, Shifts, and Rotations

Read a byte by its bit positions, select flags with masks, and distinguish discarded shift bits from wrapped rotation bits.

Lesson 1.10
Read or listenPrint chapter or book
About 22 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 1.10Key ideas, reference and questions

A one-page summary of “Bits, Masks, Shifts, and Rotations” you can keep beside your notes. Plain Markdown.

Lesson 1.10 — Bits, Masks, Shifts, and Rotations: cheatsheet

  • Remember: Bit positions run from 0 at the right. A byte's numeric value is the sum of its set-bit weights; flag meanings come from a declared format.
  • Also useful: Set with OR, clear with AND and an inverted mask, toggle with XOR. NOT flips the positions in the operand's type, so record the width.

Quick reference

  • Check any chosen flag: value & mask != 0 — At least one selected bit is set.
  • Check all chosen flags: value & mask == mask — Every selected bit is set.
  • Set chosen bits: value | mask — Other positions stay as they were.
  • Clear chosen bits: value & !mask — Invert a mask of the same width first.
  • Toggle chosen bits: value ^ mask — Apply twice to recover the starting value.
  • Move bits left or right: << / >> on unsigned values — Outgoing bits are discarded.
  • Wrap outgoing bits around: rotate_left / rotate_right — Rotation stays inside the type's width.
  • Decode a packed field: Mask, then shift — A declared field boundary gives the bits meaning.

Review

  • Q: A byte has bits 2 and 0 set. How does their position produce its unsigned value? A: Add their weights, four and one.
  • Q: The same byte value 5 can mean coins or status flags. What establishes that meaning? A: The format and code that use the byte.
Download cheatsheet (.md)

Lesson 1.8 explained how bytes hold a health number. Sometimes a program uses the positions inside one byte separately. One position can record “door open,” another “lamp on,” and another “alarm active.” To inspect or change just one of them, we need bitwise operations: operations on corresponding bit positions rather than on whole decimal values.

A bit is either 0 or 1. An unsigned byte has eight positions, numbered from 0 on the right to 7 on the left. Position i contributes 2^i when its bit is 1. The byte 00000101 has positions 2 and 0 set, so it represents 4 + 1 = 5. Binary literals in Rust begin with 0b.

bit 7: 1280
bit 6: 640
bit 5: 320
bit 4: 160
bit 3: 80
bit 2: 41
bit 1: 20
bit 0: 11
00000101 has two set bits. Its decimal value is 5, not the count of its set bits.

The number 5 alone does not tell us whether it means five coins or two enabled flags. That meaning comes from the code and format using the byte. For our invented status format, bit 0 means door open, bit 1 means lamp on, and bit 2 means alarm active. The other five positions are reserved and start at zero. Status 5 therefore means door open and alarm active.

A mask is a bit pattern used to select positions. A mask of 00000100 selects bit 2, whose weight is 4. AND, written & in Rust, keeps a bit at 1 only when both corresponding input bits are 1:

status: 00000101 5
mask: 00000100 4
AND: 00000100 4

For this format, status & 0b00000100 isolates the alarm bit. Testing whether that result is nonzero tells us whether the alarm is active:

let status: u8 = 0b00000101;
let alarm_mask: u8 = 0b00000100;
let alarm_active: bool = (status & alarm_mask) != 0; // true

For a mask containing several 1 bits, “nonzero” means any selected bit is set. To require all selected bits, compare the result with the mask. With mask 00000011, status 5 has only the door bit, so status & mask is 1: nonzero, but not equal to 3. A zero mask selects nothing.

The bitwise & above produces a number. Boolean && combines true/false conditions, such as “enough gold and item in stock.” They answer different questions and accept different operand types.

Four operations explain the common edits. A 0 in a mask preserves the input position for OR and XOR. AND preserves positions where its mask is 1.

Operation Rust expression Effect on one selected bit
AND value & mask Keep only selected 1 bits
OR value | mask Set selected bits to 1
XOR value ^ mask Flip selected bits
NOT !value Flip every bit within the value’s type

For our status byte, setting the lamp uses OR with bit 1. Clearing the alarm uses AND with a mask that has zero at bit 2 and ones everywhere else. NOT constructs that clearing mask. XOR toggles the door bit:

let status: u8 = 0b00000101; // Door and alarm are on.
let lamp_on = status | 0b00000010; // 00000111: all three are on.
let alarm_off = status & !0b00000100; // 00000001: only door is on.
let door_flipped = status ^ 0b00000001; // 00000100: only alarm is on.

These are three separate calculations from the same starting status. They do not successively update one variable. OR sets an already-set bit to 1 again; XOR would switch it off.

Predict first

A game hides gold 125 (01111101) by storing 125 XOR 90, where 90 is 01011010. The stored byte is 39 (00100111).

Which calculation gets 125 back from the stored 39?

Answer: 39 XOR 90. XOR flips the positions where the key has a 1: bits 6, 4, 3 and 1 of 90. The first pass flipped those four bits of 125; flipping the same four again restores them. 00100111 XOR 01011010 = 01111101 = 125.

XOR with a key, then XOR with it againClick bits to change the value or the key. A 1 in the key flips that position; a 0 leaves it alone.

Encode, then decode with the same key

Encode: value XOR key

Value 125
bit 7bit 0
0
1
1
1
1
1
0
1
Key 90: 1 means flip
0
1
0
1
1
0
1
0
Stored 39
0
0
1
0
0
1
1
1

Decode: stored XOR the same key

Back to 125
0
1
1
1
1
1
0
1

125 XOR 90 = 39, and 39 XOR 90 = 125. The key flips the same 4 positions on both passes, so the second pass undoes the first.

Width matters for NOT. Flipping 00000101 as a u8 gives 11111010, decimal 250. A 32-bit value has 32 positions to flip. A bit pattern must be interpreted with its type and width, just as a memory scan must choose its field width.

Explore an operation before stepping through it

Section titled “Explore an operation before stepping through it”

The starting example isolates the alarm bit: 5 AND 4 gives 4. Change the byte, mask, or operation below. Use “Show me the result” whenever you want the endpoint, then step back to inspect the selected positions. Mask and count controls remain visible for comparison; each explanation states which inputs the operation uses.

Change one byte, mask, or bit operation. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

let byte: u8 = 0b00000101;
let mask: u8 = 0b00000100;
let count: u32 = 1; // 0..=7
let result: u8 = byte & mask;

AND keeps a position at 1 only when both input bits are 1. The result is 00000100 = 4.

Final values: byte = 5; byte bits = 00000101; mask = 4; mask bits = 00000100; count = 1; operation = and; result = 4; result bits = 00000100.

Read every step of the lesson’s example
  1. Step 1, line 1. 5 is 00000101. Add the weights of its 1 positions to recover the decimal value.

    byte = 5; byte bits = 00000101; mask = 4; mask bits = 00000100; count = 1; operation = and; result = not computed yet.

    Memory: bit 7: 128 = 0; bit 6: 64 = 0; bit 5: 32 = 0; bit 4: 16 = 0; bit 3: 8 = 0; bit 2: 4 = 1; bit 1: 2 = 0; bit 0: 1 = 1.

  2. Step 2, line 2. The mask is 00000100 (4). Its 1 positions choose the bits this operation examines or changes.

    byte = 5; byte bits = 00000101; mask = 4; mask bits = 00000100; count = 1; operation = and; result = not computed yet.

    Memory: bit 7: 128 = 0; bit 6: 64 = 0; bit 5: 32 = 0; bit 4: 16 = 0; bit 3: 8 = 0; bit 2: 4 = 1; bit 1: 2 = 0; bit 0: 1 = 0.

  3. Step 3, line 4. AND keeps a position at 1 only when both input bits are 1. The result is 00000100 = 4.

    byte = 5; byte bits = 00000101; mask = 4; mask bits = 00000100; count = 1; operation = and; result = 4; result bits = 00000100.

    Memory: bit 7: 128 = 0; bit 6: 64 = 0; bit 5: 32 = 0; bit 4: 16 = 0; bit 3: 8 = 0; bit 2: 4 = 1; bit 1: 2 = 0; bit 0: 1 = 0.

A logical shift moves bits left or right, fills the newly opened positions with zeros, and discards bits that leave the chosen width. In Rust, << shifts left; >> on an unsigned integer shifts right. For an eight-bit value:

start: 00000101 5
shift left 1: 00001010 10
shift right 1: 00000010 2

A left shift by one doubles the unsigned numeric value when the result fits. A right shift by one divides it by two and drops the fractional part. But 10000001 shifted left by one becomes 00000010: the original high bit was discarded. It does not become the wider number 258.

let byte: u8 = 0b10000001;
let left: u8 = byte << 1; // 00000010, decimal 2.
let right: u8 = byte >> 1; // 01000000, decimal 64.

Our controls restrict the count to 0–7 for an eight-bit shift. A zero count leaves the byte unchanged. Do not assume that a count of eight is a portable way to produce zero: count validity and machine count handling depend on the operation and language. Rust’s checked_shl and checked_shr return None for a count at least the type’s width. They check the count; they do not report every bit discarded by a valid shift.

Rotations wrap bits instead of discarding them

Section titled “Rotations wrap bits instead of discarding them”

A rotation moves bits in one direction but carries the outgoing bits to the opposite end of the same width. Rotate 10000001 left once and its leading 1 reappears at the right:

start: 10000001 129
shift left 1: 00000010 2
rotate left 1: 00000011 3
rotate right 1: 11000000 192

Rust provides stable integer methods for this:

let byte: u8 = 0b10000001;
let left = byte.rotate_left(1); // 00000011, decimal 3.
let right = byte.rotate_right(1); // 11000000, decimal 192.

A rotation preserves the number of set bits, though their positions and the numeric value can change. Rotating a byte by eight positions returns the original byte. These methods and their distinction from shifts are documented in the Rust u8 reference.

Packed fields use masks and shifts together

Section titled “Packed fields use masks and shifts together”

Suppose an invented byte stores an item kind in its upper four bits and a quantity in its lower four. 10100011 means kind 10 and quantity 3. A field is packed when it occupies selected bit positions rather than a whole byte of its own.

let packed: u8 = 0b10100011;
let quantity = packed & 0b00001111; // Keep low four bits: 3.
let kind = packed >> 4; // Move high four bits down: 10.

The mask selects; the shift moves selected positions to weights starting at one. These operations explain a packed value only after evidence establishes its field boundaries. “A byte equals 163” alone does not prove this format. Bit order within the number and byte order across several memory addresses are separate questions; Lesson 1.8 develops byte order.

When Lesson 2.3 introduces assembly, recognize the same ideas in and, or, xor, shl, and shr. Track the operand width and the result, then investigate any flags or branches that use it.

Intent Operation Example
Check any chosen flag value & mask != 0 At least one selected bit is set.
Check all chosen flags value & mask == mask Every selected bit is set.
Set chosen bits `value mask`
Clear chosen bits value & !mask Invert a mask of the same width first.
Toggle chosen bits value ^ mask Apply twice to recover the starting value.
Move bits left or right << / >> on unsigned values Outgoing bits are discarded.
Wrap outgoing bits around rotate_left / rotate_right Rotation stays inside the type’s width.
Decode a packed field Mask, then shift A declared field boundary gives the bits meaning.
Knowledge check

Explore the concept

A byte has bits 2 and 0 set. How does their position produce its unsigned value?

Finished Lesson 1.10?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 1: 10 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.