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.
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.
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.
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.
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.
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.
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.
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.
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, 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.
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.
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<<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.
reg | (1<<n) sets one pin clean.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.
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).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.
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.
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.
&, |, ^, ~, <<, >> — 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.
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.