◈ computers mapVol 1 · Ch 18/19
How Computers Work from the transistor up · chapter 18

18Bit Manipulation

Last chapter we climbed all the way up to a real chip and proved something startling: a phone's CPU is your toy scaled up — the same fetch, decode, execute, just more of everything. But I ended on a quiet catch, and this chapter pays it off. For all its speed, that CPU has only ever done two things: move numbers and do math. It has never lit a pixel, felt a touch, or blinked a light. The moment you try to reach the physical world, you hit a wall. Hardware isn't controlled by whole numbers. It's controlled by individual bits. Picture a single 32-bit register wired to 32 physical pins. You want to switch on pin 25 — turn that one bit on — while leaving the other 31 exactly as they were. Ordinary arithmetic can't do it, and feeling why is the whole hook. When you add, a carry can ripple out of the column you touched and corrupt everything above it. So we need a sharper toolkit, one that speaks in bits, not numbers. Three tools do the whole job. First, hexadecimal lets you name a bit pattern at a glance. The bitwise operations (AND, OR, XOR, NOT) build and apply a mask. And the shift slides a bit into position. Learn those three and you can reach in and set, clear, or test any single bit you like — which is exactly what the final chapter needs to flip a real switch.

01The wall: arithmetic can't touch one bit

Let's stand right where Chapter 17 left us. You have a real CPU now, and it is astonishingly good at one narrow thing: taking numbers and running them through the ALU — add, subtract, compare. That's the machine's native language. But hardware doesn't listen in that language. A chip's outside world is its 32 output pins, and those pins are a single register where each bit is a physical wire. Bit 1 means that pin is driven high; bit 0 means low. So "switch on pin 25" really means "set bit 25 to 1 and touch nothing else." The only number-changing tool the CPU has is addition, so addition is what we'll have to try it with. But first, one question we have to answer before we can even aim: which wire is bit 25?

Not the 25th digit counted off from the left — that's how you read a written numeral, and the machine has never once read that way. Go back to Chapter 4. Every column in a register already carries a weight, doubling as you walk from right to left: 1, 2, 4, 8, 16, and on up. Those weights are the pin numbers. The column worth 2ⁿ is the one we call bit n. So bit 0 is the ones place at the far right, bit 1 is the twos beside it, and bit 25 is the column worth 2²⁵ — the 26th wire in, counting from the right, never from the left. Nothing new arrived there. We only named each column by its own weight's exponent, which Chapter 4 had already stamped on it. But notice what the naming does to the problem. A pin number is an exponent, so arithmetic has exactly one move that lands on column 25: add that column's weight, 2²⁵. Aim addition at pin 25 and watch it go wrong.

a 32-pin register · aim a pin, then press +2ⁿ to switch it on you add 2^25 = 33,554,432 carry ripples up ↑ pin 25 ◄ bit 31 (MSB) bit 0 (LSB) ►
Click any pin to aim. Green = 1 (on), empty = 0 (off).
pin 25 is ON — try to add 2^25
Addition only knows one move: carry. Land on a pin that's already 1 and the carry has to spill up the register — so plain arithmetic can't flip just one pin.
Fig. 1. The carry-ripple wall. Each pin holds one bit; a filled pin is 1, an empty pin is 0. Adding 2ⁿ is arithmetic's only way to try to switch pin n on — and it works cleanly only when that pin is already 0. Aim at a pin that's already 1 (pin 25 is, to start) and press +2ⁿ: the pin you wanted on flips off, and the carry cascades up the register, flipping a whole run of higher pins red. That's the wall — plain addition can't touch one pin in isolation, because it propagates a carry. To flip a single pin without disturbing its neighbours you need an operation with no carry at all.

There's the trap, in your hands. Adding 2²⁵ works only when bit 25 is already 0. The instant it's already 1, the column overflows. The carry marches up the register and flips a whole run of neighbours you swore not to touch. This is the same carry that made the adder in Chapter 6 work, and here it's the enemy. What we need is an operation with no carry at all — one that treats each column as its own private world. But before we build those tools, we need a way to even talk about 32 separate bits without going cross-eyed.

02Hexadecimal — a human handle on four bits

Write out a 32-bit register in binary — 00000010000000000000000000000000 — and try to spot which pin is set. Your eyes give up around the eighth digit. Binary is the machine's language, but it's a terrible one for humans to read. So engineers use a compromise base that keeps binary's structure but shrinks it: hexadecimal, base 16. Here's the trick that makes it click. 16 = 2⁴, so one hex digit encodes exactly four bits — a nibble. There are sixteen values, 0 through 9 then A through F, one for each of the 16 patterns a nibble can hold. Click through them and watch each digit light up its four bits.

pick a hex digit, or click its bits 9 = one nibble (4 bits) 1 0 0 1 8 4 2 1 8+0+0+1 = 9 hex 8 4 2 1 0 1 2 3 4 5 6 7 8 9 A B C D E F
9 = 1001 (nibble)
16 hex digits, 16 four-bit patterns — a perfect one-to-one map, so a nibble always names cleanly with a single character.
Fig. 2. Sixteen hex digits, sixteen four-bit patterns — click any row on the right and its nibble opens up on the left; click a bit and the hex digit updates to match. That's the whole trick: 16 = 2⁴, so every one of the sixteen digits 0-9, A-F names exactly one four-bit pattern, with none left over and none doubled up. A nibble always fits in a single hex character — that's why hex has exactly sixteen digits.

Sixteen digits, sixteen four-bit patterns — a perfect one-to-one map you can memorize in an afternoon (A = 1010, F = 1111, and so on). And because a nibble is half a byte, the counting is beautifully clean. A byte is two hex digits, and a 32-bit word is eight. No ragged boundaries, no mental long-division. You just chop the bits into groups of four and name each group. Toggle the bits below and watch the hex tick along with them, then type a hex value to watch the bits snap into place.

click a bit to flip it — or type / scrub hex, on the right → 0xA5 this hex string ↔ those bits — the same value, seen two ways
a byte = 2 hex digits, one per nibble
aha: 4 bits make 16 patterns — exactly enough for one hex digit (0-9,A-F). Group the bits in fours and the digit boundary IS the bit boundary, so translating is just naming, never arithmetic.
Fig. 3. Toggle any bit and watch the hex digit beneath its four-bit group tick over instantly — flip bit 5 and only that nibble's digit changes, nothing else moves. Now type a hex value (or drag scrub value) and the bits snap to match it, no computation in between. Switch to word · 32 bit: the same byte becomes just the rightmost two hex digits of a longer string, because a 32-bit word is simply four bytes end to end. Bits and hex are never converted by arithmetic — a nibble has exactly 16 patterns, exactly as many as a hex digit has symbols, so the two boundaries were always the same boundary; chopping into fours is the translation.

That's the payoff of a base that's a power of two: the digit boundary and the bit boundary agree. So translation is just grouping, never real arithmetic. Now compare that to decimal, and you'll see exactly why nobody writes bit patterns in base ten. Here is the same 32-pin register shown three ways at once.

32-bit register — click any pin to flip it 0x hex — each digit is exactly one nibble, always decimal 33,554,432 hex digit '2' = pins 27–24, nothing more decimal 33,554,432 — same value, no pin in sight
bit 25 set → hex digit '2' only
click a pin — the hex digit above it moves, the decimal number below doesn't say which one
Fig. 4. One 32-bit register, three readouts of the same value. Click any pin (0x02000000 starts with just bit 25 set) and watch all three move: the hex digit that changes sits directly under the four pins it names — that blue tie-line never lies. The decimal number changes too, but it gives you nothing to point at; a base that isn't a power of two can't align to any fixed group of bits. That's the whole reason datasheets are written in hex — a base-16 digit is a clean four-bit window, so you can read the wires straight off the number.

Look what hex bought you. 0x02000000 shows you that a bit up in the second nibble from the left — pins 27-24 — is set — you can point straight at the pin. The decimal 33554432 is the identical number, yet it tells you nothing about which wires are hot. That's the whole reason hex is stamped all over datasheets and register maps. It's base-16 place value, and since 16 = 2⁴, every hex digit is a clean window onto exactly four bits. Hex is how we'll name our masks. Now let's build the tools that actually act on them.

03The bitwise operations

Here's the good news you've earned: we don't need any new hardware. The tools that touch individual bits are the very gates you wired from transistors back in Chapter 2 — AND, OR, XOR, NOT. Only now they run 32 wide: one gate per wire, all firing at once. That parallel structure is the entire point. It's worth seeing against the adder from Chapter 6, because the difference is the crux of this chapter. The adder deliberately threads a carry from each column into the next. A bitwise operation deliberately does not. Column k of the output depends only on column k of the inputs, and nothing spills sideways. And because no column ever talks to its neighbour, the width is a free parameter. The figures from here on draw eight bits, because eight bits fit on a page. Every word of the argument holds for 32 wires, or 64, unchanged.

So look at what that leaves the second word able to mean. If output column k can only see input column k, then the second operand's bit k has exactly one job available to it: telling column k what to do. It cannot reach the column next door. It cannot carry. So stop reading that word as a quantity. The same eight bits that meant a number a second ago are now eight separate orders, one per wire — and that switch, one object with two readings, is the turn this whole chapter runs on. Which reading is live depends entirely on which operation you hand the word to. A word read as orders is a mask. That isn't a new invention stacked on the gates; it's the only thing carrylessness leaves a second operand able to be. Watch the same two bytes run through both machines: on the adder side the second byte is a number and the carry marches, and on the bitwise side it's a mask, every column obeying its own bit.

128 64 32 16 8 4 2 1 A = 91 B = 46 carry rail — one line threads left ▸ A + B = 137 no carry rail — every column stands alone A & B = 10
Press Step ▸ or flip any A/B bit
Click any A or B bit to flip it. The AND fills in one shot — its columns never wait. Only the adder grows a carry that marches left.
Fig. 5. Two tools, one pair of bytes. Feed A and B into an adder and a bitwise AND at the same time. Step the adder and the carry is born in one column and marches left, each column waiting on the one before it. The AND needs none of that — every output column reads only its own two inputs, so all eight land in a single shot. Flip any bit: a carry appears on the adder side and never on the AND side. That is why bitwise ops are surgical — with no carry threading between columns, one bit can never disturb another.

That's the release from the wall in section 1. Because there's no carry, one column can never corrupt another — exactly the surgical precision we needed. So each of these four ops becomes a specialized tool, defined entirely by what it does to a single column. Take AND first, the tool for clearing and testing. Its rule: output is 1 only when both inputs are 1. That means AND with a 0 always forces the result to 0, and AND with a 1 passes the other bit through unchanged. Feed a byte through a mask and watch which columns survive.

data mask out AND OUT = 10110000 0 in the mask → forced OFF 1 in the mask → passes straight through
AND → 176 (0b10110000)
a stencil: 0 in the mask punches nothing through, 1 lets the data bit read as-is.
Fig. 6. Top row is a fixed data byte, middle row is an editable mask — click any mask bit, or pick a preset. Each column runs its own tiny AND: output is 1 only when both the data bit and the mask bit above it are 1. Set a mask bit to 0 and its whole column dies dark, no matter what the data was; leave it 1 and the data bit reads straight through into out. That's the stencil — 0 punches nothing, 1 lets the light in.

So AND is a stencil. Put a 0 in the mask wherever you want to force a bit off, and a 1 wherever you want to leave it alone. Its mirror image is OR, the tool for setting. Before I hand you its rule, answer it yourself: both inputs are 1 — what comes out? If your gut said 0, that's English talking. Everyday "or" is exclusive — "tea or coffee" means one of them, not both. The gate's OR is inclusive, and that is the correction. OR's rule: output is 1 when either input is 1, including when both are. So OR with a 1 always forces the result high, and OR with a 0 leaves the bit untouched. It's exactly the opposite stencil. And your gut wasn't wrong, it was early: the exclusive one is a real gate with a real name, and it's next. Drop a mask in and watch the chosen bits light.

result = data OR mask 7 6 5 4 3 2 1 0 data 1 0 1 1 0 0 1 0 mask click › 0 0 0 0 0 0 0 0 ∨ OR ∨ result 1 0 1 1 0 0 1 0 OR outputs 1 the moment either input is 1
click any mask cell to toggle it 0 ↔ 1
no mask bits set — result = data as-is
AHA — a 1 in the mask forces that column high no matter what the data was; a 0 leaves the bit exactly as it found it. OR is the mirror image of AND.
Fig. 7. Top row is a fixed data byte; the middle row is an editable mask — click any cell to flip it. The bottom row is result = data OR mask, computed live: wherever the mask carries a 1, that lane's gate lights gold and the result is forced to 1 no matter what the data held; wherever the mask is 0, the gate stays dark and the result just copies the data through untouched. Try the edges preset, then clear it and click bits by hand — OR is the tool for turning specific bits on while leaving the rest alone.

AND clears, OR sets, and then there's the strange, elegant third one: XOR, the toggle. XOR outputs 1 only when its inputs disagree. That gives it a magic property: XOR with a 1 flips a bit, and XOR with a 0 leaves it. And because flipping twice returns you to the start, XOR is its own undo. Press the same toggle again and again on one bit and watch it swing.

⊕ = XOR, the toggle gate before ⊕ mask = after 00 = 0 the bit 0 = start (0) recent presses, oldest → newest flip swings it 1-0-1-0 · hold leaves it flip presses 0 parity EVEN flip an even number of times → same bit
press the toggle — repeatedly
flips: 0 · back to start
AHA: XOR never checks the bit first — 1 flips it, 0 holds it, and two flips cancel out.
Fig. 8. Pick a starting bit, then press ⊕ 1 — the box flips color and digit every single press, swinging 1-0-1-0, while the equation up top spells out exactly what happened: before ⊕ mask = after. Press ⊕ 0 instead and nothing moves — same box, same digit, because disagreeing needs a 1 on the other side. Watch the parity counter: after an even number of flips you're always back to the exact bit you started with — XOR undoes itself, so it never has to peek at the bit before it touches it.

Set, clear, toggle — and the fourth is the simplest of all. NOT takes no mask and no second operand. It just inverts every bit at once: every 1 becomes a 0, and vice versa. It's the Chapter-2 inverter run 32 wide. And thanks to Chapter 5's two's complement, inverting a number is also the first half of negating it. Flip a whole word and watch it turn inside out.

x — the only input (no mask) 0 0 0 0 0 1 0 1 ~x — every bit inverted, at once x = 5 (00000101) ~x = ? (press NOT)
toggle bits, then press NOT
press NOT first — then +1 lands on −x (Ch.5)
Fig. 9. Look at what's missing: there's only one register up top — no mask row like AND, OR, and XOR each needed. NOT doesn't compare two things column by column; it just runs every wire through its own inverter, the same gate you soldered together back in Chapter 2, copied eight times with nobody talking to their neighbour. Toggle a few bits, predict the flip, then press NOT and watch all eight fire on the same tick — no ripple, no carry, no order. The small check underneath is the payoff from Chapter 5: take that ~x, add 1, and you land exactly on −x. Inverting every bit is the easy half of negation; NOT and a single +1 is the whole trick.

Four operations, four jobs: AND clears and tests, OR sets, XOR toggles, NOT inverts — all with no carry, all in parallel. But every one of them acts where the mask says, and so far we've been handing the mask over ready-made. The last tool is the one that builds a mask aimed at a specific bit: the shift.

04The shift — sliding a bit into position

A shift does something almost too simple to sound useful: it slides every bit in a register one place to the side. But watch what that does to the value. Reach back to Chapter 4, where each place is worth double its right-hand neighbour. Slide every bit one place to the left and each one's weight doubles, so a left shift by one is exactly ×2. It's the same move as sliding a decimal digit left to multiply by ten. Here the base is two, so it multiplies by two. A fresh 0 rolls in to fill the vacated ones place. Shift a number left and watch it double.

the register — 8 bits (1 byte) 128 64 32 16 8 4 2 1 128s place ones place next 0 enters → decimal value 3 doubles with each shift left ⇐
start: value = 3
aha: shifting bits left is exactly shifting decimal digits left — both multiply by the base (2 vs 10).
Fig. 10. Press shift left: the whole row of bits glides one column to the left, a fresh 0 rolls in at the ones place on the right, and the decimal value doubles — because every surviving bit just slid under a place-value label twice as big as the one it left. Keep pressing and you'll watch the leading bit slide clean off the register's left edge: that's an 8-bit byte running out of room, the exact base-2 twin of a decimal digit marching left every time you multiply by ten (37 → 370). A left shift isn't a special trick the machine does — it's just place value, kept moving.

Left shift is ×2, so, run backwards, a right shift is ÷2. Slide every bit down one place and each weight halves, and a fresh 0 rolls in at the top. But there's an honest catch here, and it's worth feeling. The lowest bit has nowhere to go: it slides off the end and is lost. That dropped bit is the remainder you just threw away, which is why right-shift is integer division. 5 ÷ 2 gives 2, not 2.5, and the lost 1 was the leftover half. Now the catch behind the catch, while we're being honest: this section is about unsigned patterns. Roll a 0 into the top of a two's-complement negative and you have wiped its sign bit — the number comes back large and positive, not halved. That's why real machines carry two right shifts: a logical one that rolls in a 0, and an arithmetic one that copies the sign bit down instead, so −8 halves to −4. Ours is the logical one. Shift an odd number right and watch the bit fall off the edge.

current value n 5 0 in → 0 0 1 0 1 16 8 4 2 1 falls off ↓ (discarded) 1 no bit-position holds weight ½ — so the ones bit can't shift down. it falls off — that drop IS the remainder. that's why right-shift is integer division: free halving, lost remainder.
n = 5 — press shift right ›
pick an odd n, hit shift, and watch its low 1 bit tumble off the edge — an even n drops a silent 0 instead.
Fig. 11. Set an odd n (5 is loaded) and hit shift right: every weight halves — 16 becomes 8, 8 becomes 4 — but the ones bit has no slot at weight ½, so it tumbles off the register into the tray, and 5 ÷ 2 → 2 with a 1 left over. Try an even n and the same edge drops a silent 0 — nothing lost, exact division. That dropped bit is the remainder; that's why right-shift is integer division.

Now the two ideas fuse into the move this whole chapter has been walking toward. Start with the number 1 — a single lit bit sitting in the ones place — and shift it left by n. That lone 1 walks up to position n and stops, leaving 0s everywhere else. You've just built a mask that names exactly one bit: 1 << n. Slide the 1 up to position 25 and you've literally written the number that means "pin 25."

1 << 0 = 20 = 0x00000001 = 1 MSB · bit 31 LSB · bit 0 (ones place · start) 1 bit 0 0x one lit 1 at bit n · every other bit stays 0
n = 0
n=0 → bit 0 → 0x00000001
Predict before you drag: you need the mask for pin 25. By hand you can just write 0x02000000 — but how does a program compute that from the bare number 25? Reach for the power section 1 planted in you. Then send n to 25 and read the line.
Fig. 12. Drag n and watch the single lit 1 slide from the ones place to bit n — and watch the line above it: the same number, named four ways at once. 1<<25 lands one bit at bit 25, reads as 0x02000000, and is 2²⁵ = 33554432 — the number that means “pin 25”, arrived at with no carry.

That's the keystone of the toolkit: 1 << n converts a pin number into a mask — a word with a single 1 at position n. Feed that mask to the bitwise ops and you can finally do the thing arithmetic couldn't: reach in and touch one pin, cleanly. Let's assemble the recipes.

05Set, clear, test — the pin toolkit

Everything now clicks together. To set pin n — drive it high without disturbing its neighbours — you build the mask 1 << n and OR it into the register: reg | (1 << n). OR leaves every column alone except where the mask is 1, and the mask is 1 in exactly one place. This is the thing the wall in section 1 wouldn't let us do, and now it's a one-liner. Watch one pin come on while the other 31 hold dead still.

8-bit slice of a 32-bit reg 1 0 1 1 0 1 1 0 128 64 32 16 8 4 2 1 mask 0 0 0 1 0 0 0 0 1 << 4 = 0b00010000 = 0x10 selected set = 1 corrupted
bit 4 is 1 — press OR-in or ADD
OR only forces a 1 where the mask is 1 — and the mask is 1 in exactly one place. That's the whole reason reg | (1<<n) sets one pin clean.
Fig. 13. reg | (1<<n): pick a pin, OR the mask in, and only that pin lights while the rest hold. Press ADD instead and watch the same pin risk a carry into its neighbours.

Setting a bit is OR. Clearing one — forcing pin n low — is the sharpest little maneuver in the chapter, and it's the one hands fumble. So let's fumble it here, on purpose. You want pin n dead. You have 1 << n, and AND is the clearing tool, so predict what reg & (1 << n) leaves in the register. Watch it: exactly backwards. The 31 pins you swore not to touch all go dark, and the only column that can survive is the one you wanted dead. Now the shape is something you can see rather than something I assert. A mask of "a single 1" is the wrong shape for AND, which clears where it sees 0. What AND needs is the photographic negative of that: all 1s except a single 0. So you NOT it first — ~(1 << n) is precisely that flip, and by now it's the only move left. Then AND that in: every bit passes through untouched except the lone 0 column, which gets forced low. Step through the three beats and watch a single pin go dark.

reg = 10110110 · clear pin 4 reg 1<<n result & pin 4 AHA: AND clears wherever it sees a 0. So you can't type the mask — you slide a single 1 in, flip it to a single 0, and only that column drops.
1<<n ~ &
Goal: force pin 4 low, keep rest
Predict: to zero one bit, do you type a mask of all‑1s‑but‑one — or build it?
Fig. 14. To force one pin low, you don't hand‑type a mask of all‑1s‑but‑one. You build it from three tools: 1<<n slides a single 1 into column n, ~ inverts the whole mask into a single 0 surrounded by 1s, and & applies it — AND clears only where it meets that 0, while every all‑1s column passes its bit straight through. Step the beats and watch one pin go dark, untouched neighbours and all.

So the recipe is reg & ~(1 << n), and you didn't memorize it, you cornered it: shift to place the bit, NOT to invert the whole mask, AND to apply it. Three of our tools, composed, to clear one pin surgically. And your wrong answer a moment ago wasn't waste — it was the last verb, already built. That verb isn't changing a pin, it's reading one. To test whether pin n is high, you AND the register with 1 << n, which keeps only that one bit and zeroes the rest. If the result is nonzero, the pin was set. If it's zero, it wasn't. Why nonzero, though, and not 1? Because the surviving bit is still sitting in column n, so it's still worth 2ⁿ. Test pin 25 on a register that has it set and the answer comes back 33554432 — the mask's own value. That's why the test says "nonzero" instead of "equals 1", and why languages count any nonzero as true. If you want a literal 0 or 1, shift it back down: (reg & (1 << n)) >> n. Poke a pin and read the verdict.

reg & (1 << n) — isolate pin n REG MASK OUT & = b7 1 0 0 b6 0 0 0 b5 1 1 1 b4 1 0 0 b3 0 0 0 b2 1 0 0 b1 0 0 0 b0 0 0 0 180 & 32 = 32
pin 5 → SET (nonzero, high)
click a "b" tab to pick the pin · click a REG cell to flip that bit
AND keeps only bit n's column — every other bit collapses to 0, so nonzero means that pin is high.
Fig. 15. Eight pins, one mask. Pick a b tab to choose which pin the mask targets — the mask lights only that column, forcing every other column to 0. AND the register against it and only that one column can survive: nonzero means the pin reads high, zero means it reads low — a boolean the machine can branch on with reg & (1 << n).

And that nonzero-or-zero verdict is not just for your eyes. It's a value the machine can branch on. That's the perfect bridge to the last idea, because branching means deciding, and deciding usually means comparing. How does a machine with only an adder ever ask "are these two numbers equal?"

06CMP: subtract, set the flags, throw the answer away

Back in Chapter 14 you built the flags register — the zero flag that fires when the ALU's last result came out all 0, and the carry flag that fires on an unsigned wrap. And you built JZ and JC to branch on them. Here's how comparison reuses every bit of that, with no new hardware. To compare A and B, the machine runs CMP. It quietly computes A − B through the ordinary subtractor, sets the zero and carry flags from the result, and then throws the difference away. It never wanted the number. It only wanted the verdict the flags carry: if A − B came out zero, then A equals B. Run a compare and watch the flags set while the result is discarded.

A 180 B 90 SUBTRACT A − B ? the ALU's answer the difference dropped discarded — nobody kept it C 0 borrow / carry Z 0 zero flag
set A and B, then press Run CMP
CMP needs no new hardware: it's the Ch.14 subtract, run only for its flags — the number itself is computed and dropped on the spot.
Fig. 16. A and B feed the ordinary subtractor, exactly like Chapter 14's ALU. Press Run CMP: the ALU computes A − B right there in its box, the zero and carry flags latch off that answer a beat later, and then — watch closely — the difference itself drops straight into the bin marked discarded. Try A = B: the difference is zero, so Z lights, even though the number that proved it never survives the instruction. That's the whole trick: CMP is a subtract that only ever wanted its flags.

That's the trick in full: CMP is a subtract whose only product is the flags. The answer is computed and immediately dropped on the floor. (And because Chapter 5's two's complement makes subtraction just "add the negative," the same ALU handles it without a dedicated comparator.) Now finish the thought: a compare is worthless unless something acts on it. So CMP pairs with the conditional jumps from Chapter 14. Set the flags, then JZ or JC reads them and branches. Compare, then jump on the flag. Watch a comparison turn straight into a decision.

Ready — PC sits on the CMP PROGRAM · A=5, B=5 0x10CMP A,B 0x11JZ 0x20 0x12next instr ← if Z=0 0x20target: … ← if Z=1 PC 0x10 Z flag this beat does press Step to run the CMP JZ · which path? Z=1 → JMP 0x20 Z=0 → PC=0x12 not decided yet CMP → writes the Z flag JZ → reads Z, branches gold = the path this beat takes aha: compare-then-branch = an "if"
CMP is next — press Step
flip A vs B any time — even mid-step — and watch which path lights up: CMP only ever sets the flag, JZ only ever reads it.
Fig. 17. A comparison is worthless until something reads it. Step through: CMP A,B computes A−B and throws the number away, keeping only the Z flag — set if they matched. JZ then reads that one bit and decides where the program counter goes next: 0x20 if Z=1, 0x12 if Z=0. Flip A vs B at any beat and watch the whole path re-light without touching a single wire — that's the entire machinery behind an if: compare sets a flag, branch reads it.

There it is — the entire machinery of an "if", built from a subtraction you throw away and a jump you already own. CMP plus JZ/JC is how every loop condition and every branch in every program you'll ever write ultimately gets decided. We've now got the full toolkit. Let's prove it actually works, then step back and see how far we've come.

07Prove it, and where you are

I've asserted a lot of rules in this chapter — AND clears, OR sets, XOR toggles, left-shift doubles, 1 << n builds a mask. You shouldn't take any of them on faith, and you don't have to. Every one of these operations exists in real code, spelled &, |, ^, ~, <<, >>. So let's run the machine's version and the operator version side by side and diff them, across a pile of cases. Two honest notes first. & is not and: the symbol runs a gate on every column, while the word asks one whole-value question and short-circuits. And Python's integers have no fixed width — they grow as long as they like — so ~reg comes back negative and runs off past the byte. To hold it to an 8-bit register we pin it: ~reg & 0xFF. That's our own AND stencil doing the pinning, which is why the width isn't a fudge. Now, if our mental model is right, they'll agree bit for bit.

reg=10110110 (182 · 0xB6) n=3 operation by hand the operators match bit-by-bit loop & | ^ ~ << >> reg & 0x0F ··· ··· · reg | (1<<n) ··· ··· · reg ^ 0xFF ··· ··· · ~reg & 0xFF ··· ··· · (reg << n) & 0xFF ··· ··· · reg >> n ··· ··· · AHA: the loop you'd run by hand and the operators & | ^ ~ << >> a language gives you are two different roads to the same bits — that's the whole toolkit, proven.
reveal a case to run it
Predict: could the hand loop and the operators & | ^ ~ << >> ever disagree?
Fig. 18. Two independent roads to the same answer: the left column builds each result one bit at a time, the truth-table way; the right column runs the actual operators — &, |, ^, ~, <<, >> — that every language, Python included, gives you. Reveal a case at a time or run all six; every row locks green because the bits agree. Flip "inject a bug" and watch a single flipped bit turn one row red — proof that the diff would catch a real mistake, not just wave it through.

Every case matches — the masks you built by hand are exactly what the silicon (and the language on top of it) computes. That's the honest close: no hand-waving, just the same bits coming out both ways. So take the long view for a second. Look at the whole ladder you've climbed, from a single voltage-controlled switch all the way to a machine that can name a bit pattern in hex, build a mask with a shift, and set, clear, or test any pin with a bitwise op. You are here.

THE CLIMB, SO FAR 1 SWITCH 2 BYTE 3 HEX 4 SHIFT 5 BITWISE 6 BIT OPS ch18 · here 7 MMIO ch19 · next flip the pin
click any rung to recall its keystone
BIT OPS · ch18 — you are here
Ch18: fuse hex + shift + bitwise into set/clear/test — any pin, rest untouched.
← click MMIO (Ch19) to preview it flipping a real switch
Fig. 19. The whole climb, one picture — click any rung to recall its keystone. Ch18 stays lit: hex names a pattern, shift places it, bitwise sets/clears/tests it — the exact toolkit Ch19 spends flipping a real hardware pin next.

That's Chapter 18. We started at a wall: arithmetic's carry can't leave the neighbours alone. And we broke through it with three tools that speak in bits. Hexadecimal names a pattern four bits at a time, because 16 = 2⁴ makes every digit a clean nibble. The bitwise operations — the Chapter-2 gates run 32 wide with no carry — give us AND to clear and test, OR to set, XOR to toggle, and NOT to invert. The shift doubles, halves, and — the payoff — slides a lone 1 into position to build a mask with 1 << n. Stack them and you get the surgeon's kit: reg | mask to set, reg & ~mask to clear, reg & mask to test. And CMP turned out to be a subtraction we throw away, feeding the Chapter-14 flags so the machine can decide. But notice what all of this has quietly assumed: that a register of bits is wired to something real, 32 pins out in the world. That's the last secret, and it's a strange one. At a handful of magic addresses, storing a byte doesn't save a number at all — it flips a physical switch, lights an LED, drives a pin. It's the load and store you built long ago, pointed at the outside world. That's memory-mapped I/O, and it's where the whole machine finally reaches out and touches the room. That's next.

iolinked.com
Written by Ajai Raj