← roadmap
subject · The Machine & Its Tongue

Numbers: How the Machine Counts

Bits, binary, hex, and how negatives exist with no minus sign

4–6 hours

Before anything else, one question worth a year: how can a machine made of matter do arithmetic? A rock doesn't count. The answer is one idea, and if it lands here, the whole machine unfolds from it. We won't rush to 'convert binary' — we'll earn the idea, because it's the bedrock the CPU, assembly, C, and every exploit you'll ever write are built on. Read it start to finish; by the end you'll build a program that lets you SEE a number as the switches it really is.

After this subject you can
  • › Explain why meaning in a computer is imposed, never intrinsic
  • › Convert between binary, decimal, and hex by hand — by deriving, not memorizing
  • › Say how negative numbers exist with no minus sign (two's complement), and why it's built that way
  • › Write a C program that reveals any number's bits, its hex, and its signed vs unsigned reading

1 · Meaning is imposed

Deep inside the machine is a switch — a tiny thing that's either on or off. That's all it physically is. It has no idea what 'five' is. The move that created computing: we *decide*, by pure convention, to call on 1 and off 0. Nothing in physics says a switch 'is' a one — we impose that meaning. Do it for billions of switches, agree on rules for what patterns mean, and you have a computer.

Key idea
**A bit is a physical state; what it *means* is a rule we impose on it.** The same on/off pattern can be the number 205, the number −51, a letter, a colour, or part of a CPU instruction — the bits don't change, only the interpretation does. To understand a computer is to always know *which interpretation* you're looking at. (This is also why programs can be hijacked: if meaning is imposed, meaning can be subverted — convince a program to read your data as an instruction and you own it. That's months away, but it's this exact idea.)

Why two states, not ten (so one switch could store a decimal digit)? Not math — reliability. Two clearly-separated states (on / off, with a big gap between) are cheap to build and easy to read a billion times a second without error. Ten precise voltage levels would be a nightmare. The machine is shaped by what physics makes *trustworthy*, not what's elegant — a theme you'll see everywhere.

2 · Counting with two symbols

You already know how to read numbers — in base ten. Write 205 and each column is a power of ten: 2×100 + 0×10 + 5×1. **Binary is the exact same idea with one change: two symbols instead of ten, so each column is a power of *two*.** That's the whole difference.

the pattern 11001101 read as a number
  bit:    1     1     0     0     1     1     0     1
  power: 2^7   2^6   2^5   2^4   2^3   2^2   2^1   2^0
  value: 128    64    32    16     8     4     2     1
  on?:   128 +  64 +   0 +   0 +   8 +   4 +   0 +   1  =  205

To go decimal → binary you recall nothing — you subtract the largest power of two that fits, repeatedly: 205 − 128 = 77 (so bit 7 is 1), 77 − 64 = 13 (bit 6 is 1), 32 too big (bit 5 is 0)… giving 11001101. To go the other way, add the columns that hold a 1. Redoable on any number, on paper. That gap — between deriving and recalling — is the whole difference between knowing and memorizing.

In code, you read one bit with a shift and a mask: (v >> i) & 1. Shifting v right by i slides bit i down to the bottom; & 1 keeps only that bottom bit. You'll use this exact idiom in the project to print a number's bits:

printbits.cc
1void print_byte(unsigned char b) {
2 for (int i = 7; i >= 0; i--) // high bit first
3 putchar('0' + ((b >> i) & 1)); // read bit i: shift it down, mask it
4 putchar('\n');
5}
6// print_byte(205) prints 11001101
your turn — do it on paper
Before moving on: convert 200 and 89 to binary by hand, using 'subtract the largest power of two that fits.' Show every subtraction and the final 8 bits. Then convert 10110110 back to decimal by adding its columns.

3 · The byte, and hex

One bit answers one yes/no question — not much. So we group them: 8 bits = one byte, giving 2⁸ = 256 patterns (0 to 255 read as an unsigned number). The byte is the atom of memory — the machine addresses bytes, never single bits.

Long binary is painful to read (11001101 takes effort). Hexadecimal fixes this: it has 16 symbols (0–9 then A–F for 10–15), and because 2⁴ = 16, one hex digit is exactly four bits. So a byte is always two hex digits — no arithmetic, just read binary in chunks of four:

11001101 in hex
   1100      1101
    C          D      ->   0xCD   (which is 205 — it all lines up)

   one hex digit = one nibble (4 bits); a byte = two hex digits.
Note
Hex is everywhere in low-level work — addresses, register values, colours (#FF8800), byte dumps — because it maps cleanly onto bytes. 0xFF instantly reads as 'all 8 bits set'; 0xFF00 as 'high byte full, low byte empty'. Decimal hides the byte boundaries; hex shows them.

4 · Negatives with no minus sign

A byte is 8 switches — there's no switch for a minus sign. So how does -1 exist? You might steal the top bit to mean 'minus' (called sign-magnitude), but then addition needs special cases and you get *two* zeroes. The scheme actually used, two's complement, is chosen so ordinary binary addition just works with no special case.

The rule to negate a number: flip every bit, then add 1. Find -1 in a byte, starting from +1:

deriving -1 in 8 bits
   +1:           0 0 0 0 0 0 0 1
   flip bits ->  1 1 1 1 1 1 1 0
   add 1     ->  1 1 1 1 1 1 1 1   = 0xFF   <- this bit pattern IS -1
Key idea
So the *same* byte 11111111 is 255 read as unsigned, and −1 read as signed — the bits are identical, only the interpretation differs (section 1, made concrete). The top bit is the sign: 1 means negative. And it's not arbitrary — because (-1) + 1 in this scheme is 11111111 + 1 = 100000000, the 9th bit falls off the byte and you're left with 00000000 = 0. Addition 'just works' for signed and unsigned with the *same* hardware. That's the whole reason it's built this way.
your turn — do it on paper
Work these by hand with flip-and-add-1: write −1, −5, and −128 as 8-bit patterns. For each, circle the sign bit. Then take 0x80 and decode it as a signed value.

The Project

Build a 'byte inspector' in C: given any number, it prints that number's bits, its value in hex, and how those same bits read as an unsigned versus a signed value — turning the 'meaning is imposed' idea into something you can see on screen.

Mandatory
  • 01

    A print_byte(unsigned char) that prints the 8 bits high-bit-first using (b >> i) & 1 (e.g. 205 → 11001101).

  • 02

    A print_bits(unsigned int) that prints all 32 bits, grouped in bytes of 8 with a space between groups.

  • 03

    For a set of values, print each one's: bits, hex (%X), value read as unsigned, and value read as signed char/int.

  • 04

    Run it on 0, 1, 128, 205, 255, and −1, and confirm each line matches what you derived by hand first.

Constraints
  • •

    Only <stdio.h>. Read bits with shifts and masks, not string tricks.

  • •

    Derive every expected output BY HAND before running, then check the program against your derivation.

  • •

    Compiles clean with -Wall -Wextra.

Deliverable
inspect.c
Build & check
gcc -Wall -Wextra inspect.c -o inspect && ./inspect
You were taught everything for this

Everything here was taught above: a byte is 8 bits (sections 1–2), the `(v >> i) & 1` bit-reading idiom is shown in section 2, hex is four bits per digit (section 3), and the same bits read as signed or unsigned via two's complement (section 4). If a part stumps you, reread the section it comes from.

built the project?