← Binary Course — Part B, lesson 3 of 4 (Bitwise operations)

7. Masking: Reading and Writing Just the Bits You Care About

LESSON 7 | 0000 0111 | 0x07

The concept

Every AND/OR/XOR/NOT/shift instruction from Lessons 5 and 6 takes a whole word as input and hands back a whole word as output — there's no instruction that means "give me bit 5 by itself," the way an array lets a program address one element on its own. That's not a shortcoming of the paper diagrams used to work these out by hand. A grid like

A:        0  1  1  0
B:        0  0  0  1
A AND B:  0  0  0  0

is a faithful picture of what's physically happening — real hardware typically computes AND/OR/XOR with one logic gate per bit position, all of them wired to fire at the same time. The bits are genuinely there, laid out side by side, exactly like the grid suggests. What's missing is a way for an instruction to name one of those positions independently. A word is the smallest unit the instruction set lets a program address; the bits inside it are real, just not individually reachable — 0110 and 0x6 are two notations for the identical word (Lesson 3), and neither one lets an instruction pick out "just the third bit" as its own separate thing to operate on.

Masking is how a word-oriented instruction set fakes bit-level access anyway. An instruction can't hand back bit 5 in isolation, but it can hand back a whole word whose only meaningful bit happens to be bit 5 — which is exactly what a mask is built to arrange.

The last two lessons introduced five tools — AND, OR, XOR, NOT, and shifts — mostly one at a time. This lesson doesn't add a new tool. It's about combining those five to do something genuinely useful: working with one narrow slice of a word (a single flag bit, or a small multi-bit field) without disturbing anything else living in the same word.

A mask is a bit pattern chosen for exactly one purpose: to pick out which positions an operation should touch and which it should leave alone. Which of the five tools you reach for depends on what "touch" means:

  • Read a field that starts at bit 0. AND with a mask that has 1s over the field and 0s everywhere else. Every position under a mask 1 passes through unchanged (x AND 1 = x); every position under a mask 0 is forced to 0 (x AND 0 = 0, regardless of x). The rest of the word simply vanishes from the result.
  • Read a field that doesn't start at bit 0. Same AND as above, then shift right by the field's starting bit index, to bring the field's own bit 0 down to the real bit 0 so it reads as an ordinary number.
  • Test whether one bit is set, without caring about its numeric position. AND with a mask that has a single 1, built by shifting 1 left to that bit's index. No need to shift the result back down — zero-vs-nonzero is the whole answer.
  • Set specific bits to 1 without touching the rest. OR with a mask that has 1s at the positions to set, 0s elsewhere — Lesson 5's x OR 0 = x is exactly what protects the untouched positions.
  • Clear specific bits to 0 without touching the rest. Start with a mask that has 1s at the positions to clear — the same shape as the "test" mask above — then run it through NOT, which flips it to 0s at those positions and 1s everywhere else. AND the word with that flipped mask: the 0s force the target positions to 0, and the 1s elsewhere let every other bit pass through unchanged.

Nothing forces the clear recipe to go through NOT — a mask with 0s at the target positions and 1s elsewhere could just as well be written down directly. The reason to build it from the "care about" shape instead is what has to be remembered. Read, test, and set all use the exact same mask for a given field: 1s where that field lives, 0s elsewhere. If clearing used its own separately-written mirror-image pattern, every field would need two versions of its mask kept in sync by hand — one shape and its exact opposite — with every field-layout change risking one getting updated and the other forgotten. Deriving the clear mask from the same "care about" shape with NOT means each field only ever needs one mask, period; NOT is just how that one mask gets translated into the form AND needs for clearing. This starts to matter for real once a word holds several packed fields side by side, each with its own mask — see Lesson 8.

Every one of these reuses a rule already proven in Lessons 5 and 6; this lesson is just naming the five recipes built from them.

Example 1 — Reading a field at the bottom of the word

Take W = 0110 1101 (109, the running example since Lesson 2) and read just its low nibble:

mask:      0000 1111
W:         0110 1101
W AND mask: 0000 1101

The mask's 0s over the top nibble force those four positions to 0; the mask's 1s over the bottom nibble let W's own bits through untouched. W AND mask = 0000 1101 = 0xd (Lesson 3's nibble table), 13.

Example 2 — Reading a field that isn't at the bottom

Say bits 5-4 of W hold a 2-bit field, and the goal is that field's plain numeric value. Mask first, then shift the result down to bit 0:

mask:            0011 0000
W:               0110 1101
W AND mask:       0010 0000
(>> 4 to normalize): 0000 0010

Masking alone leaves the field sitting at bits 5-4 — correct as a bit pattern, but 0010 0000 is 32, not the field's actual 2-bit value. Shifting right by 4 (the field's own starting position) brings it down to bit 0: 0000 0010 = 2, the field's real value.

Example 3 — Testing a single bit

To check whether bit 2 of W is set, build a one-bit mask by shifting: 1 << 2 = 0000 0100.

mask:      0000 0100
W:         0110 1101
W AND mask: 0000 0100

The result is nonzero, so bit 2 was 1. There's no need to shift this result down to bit 0 — unlike Example 2, nothing here needs to be read as a number; zero-vs-nonzero already answers the yes/no question.

Example 4 — Setting a bit

To set bit 1 of W to 1 without disturbing any other bit, OR with a mask that has only bit 1 set:

mask:      0000 0010
W:         0110 1101
W OR mask:  0110 1111

Bit 1 was 0 and becomes 1. Every other position ORs against the mask's 0, which Lesson 5 already showed leaves it exactly as it was. W OR mask = 0110 1111 = 111.

Example 5 — Clearing a bit

To clear bit 3 of W back to 0 without disturbing anything else, don't AND with a mask that has a 1 at bit 3 — that would extract bit 3, not clear it (Example 3's move, not this one). Instead, build a mask with a 0 only at bit 3, by taking 0000 1000 and running it through NOT:

starting mask:  0000 1000
NOT mask:       1111 0111
W:              0110 1101
W AND NOT mask: 0110 0101

Bit 3's 0 in the NOT'd mask forces that position to 0; every other position ANDs against a 1 and passes through unchanged. W AND NOT mask = 0110 0101 = 101.

Exercises

Exercise 1. W2 = 1011 0110 stores a 3-bit field at bits 6-4. Using Example 2's mask-then-shift method, what is that field's plain numeric value?

Exercise 2. Starting again from W = 0110 1101, set bits 1 and 0 to 1 in a single OR (one mask covering both positions). Give the result in both binary and hex (Lesson 3's nibble table).

Exercise 3. A classmate wants to clear bit 3 of W = 0110 1101 — the same goal as Example 5. Instead of NOT-ing the mask first, they AND W directly with 0000 1000 (a 1 only at bit 3) and report the result as the answer. What do they actually get, and what operation does that 0000 1000 mask correctly perform instead? What should they have done?

Exercise 4. Toggle bit 2 of W = 0110 1101 by XOR-ing with 0000 0100. Then XOR that result with the same mask a second time. What do you get after each step, and why does the second XOR land exactly back on the original W — always, with no exceptions? (Compare this to Lesson 6's Exercise 3, where a left-shift-then-right-shift round trip only sometimes recovered the original value.)

Show Answer Key

1. Mask bits 6-4: 0111 0000. W2 AND mask = 0011 0000. Shifted right by 4 (the field's starting bit): 0000 0011 = 3. The field's value is 3.

2. Mask covering bits 1 and 0: 0000 0011. W OR mask = 0110 1111. Converting each nibble with Lesson 3's table: 0110 → 6, 1111 → f. Result: 0110 1111 = 0x6f.

3. W AND 0000 1000 = 0000 1000 = 8 — not 0. That's because W's bit 3 actually was 1: AND-ing with a mask that has a 1 only at bit 3 forces every other position to 0 and lets bit 3 through unchanged, which extracts (tests) bit 3 rather than clearing it — Example 3's operation, not Example 5's. To actually clear bit 3, the mask needs a 0 at bit 3 and 1s everywhere else, which means running 0000 1000 through NOT first (1111 0111) before AND-ing: W AND 1111 0111 = 0110 0101 = 101, matching Example 5.

4. W XOR 0000 0100 = 0110 1001 = 105 (bit 2 flips from 1 to 0). XOR-ing that same mask again gives 0110 1101 = 109, exactly W. This always works, with no exceptions, because XOR never discards information the way an overflowing shift does: x XOR mask XOR mask always simplifies back to x, since XOR-ing the same value twice at any position cancels out (0 XOR 1 XOR 1 = 0, 1 XOR 1 XOR 1 = 1 — the second XOR always undoes the first). Lesson 6's shift round trip only worked when nothing overflowed off the top; toggling with XOR has no equivalent failure case, because nothing is ever dropped — every bit stays right where it started, just flipped and flipped back.