
Table of Contents
- The ISA: the contract between software and silicon
- Registers: the CPU's workbench
- The ALU: where the math happens
- The control unit: the conductor
- The instruction cycle: fetch → decode → execute
- Reading assembly: a tiny example
- Instruction anatomy
- Calling conventions: how functions really call each other
- The stack: where calls live
- The heap: memory on request
- The big picture
Part of the series:Computers
The computer overview gave the CPU1 one paragraph: it fetches instructions and executes them, billions of times per second. This post opens that black box. We will look at the contract a CPU offers to software, the parts inside it, the exact loop it runs, how to read the assembly language it speaks, and how lofty ideas like “function calls” and “the heap” reduce to bytes and registers.
The ISA: the contract between software and silicon
A processor does not understand Python, JavaScript, or even C. It executes instructions defined by its instruction set architecture (ISA)2 — the complete specification of the instruction set and the machine model that software sees: which instructions exist, what registers are available, how memory is addressed, and how programs interact with the machine.
The ISAs you meet in practice are x86-643 (common in laptops and desktops, historically associated with CISC, with a large, irregular instruction set), ARM4 (your phone, a RISC5 design with fewer, simpler, fixed-size instructions), and RISC-V6 (the open ISA gaining ground in embedded and research hardware). A program compiled for one ISA cannot run on another — which is why your phone cannot execute laptop binaries, and why Apple spent years translating x86 apps to ARM.
Everything below is explained through x86-64 with ARM notes where they differ, since the concepts transfer.
Registers: the CPU’s workbench
A CPU cannot operate directly on RAM for most instructions — memory is too slow and too far. Instead it keeps a small set of storage locations on-chip called processor registers7: the fastest storage directly available to the CPU.
x86-64 gives you sixteen 64-bit general-purpose registers (rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8–r15), plus special ones with fixed jobs: rip (the instruction pointer — the address from which the next instruction is fetched), rsp (the stack pointer), and rflags (condition codes set by every arithmetic result). ARM64 instead provides thirty-one 64-bit registers, x0–x30, of which x30 conventionally serves as the link register. The names differ; the idea is identical: registers are the CPU’s primary workspace for general-purpose computation.
The ALU: where the math happens
The arithmetic logic unit (ALU)8 is the calculator at the heart of the CPU. Conceptually, it takes operands and an operation — add, subtract, AND, OR, XOR, shift — and produces a result; in a modern CPU, the actual latency and throughput depend on the instruction and microarchitecture.
Two details make the ALU more than a dumb adder. First, subtraction is implemented as addition of the two’s complement (the number encoding from the previous post), so one adder circuit covers both. Second, many arithmetic and logical instructions update flags in the status register as a side effect: ZF (result was zero), SF (result was negative), CF (unsigned overflow), OF (signed overflow). Conceptually, a comparison like cmp rax, rbx subtracts the operands and only keeps the resulting flags — and the next instruction, a conditional jump, reads those flags to decide where to go. Many conditional branches ultimately depend on these flags.
The control unit: the conductor
If registers are the workbench and the ALU is the tool, the control unit9 is the craftsperson. It reads the current instruction and choreographs the rest of the chip: route these registers into the ALU, select addition, write the result back there, advance to the next instruction.
Simpler RISC cores do this with hardwired logic — a fixed circuit per instruction. Complex x86 chips instead decode each big CISC instruction into a stream of smaller RISC-like internal steps called micro-operations, then schedule those. Either way, the control unit is what turns a passive byte pattern in memory into coordinated action across the chip.
The instruction cycle: fetch → decode → execute
Conceptually, we can describe instruction execution as a fetch → decode → execute loop — the instruction cycle10:
- Fetch. Copy the bytes at the address in
ripinto the CPU, and advancerippast them. - Decode. Interpret the bytes: which operation, which registers, which memory address.
- Execute. Route data through the ALU or memory, write results back, update flags — and possibly set
ripsomewhere new (a jump) instead of just moving forward.
Watch it happen for add rax, rbx (encoded as the three bytes 48 01 D8):
rip = 0x1000, memory[0x1000] = 48 01 D8
FETCH: load bytes 48 01 D8, rip becomes 0x1003
DECODE: 0x48 is a REX prefix (REX.W), selecting 64-bit operand size;
0x01 = ADD register-to-register,
0xD8 is the ModR/M byte selecting rax as destination and rbx as source
EXECUTE: rax = rax + rbx, flags updated
Three bytes in memory became one addition. A 3 GHz clock ticks three billion times per second, but that does not mean the CPU executes exactly three billion instructions per second. Modern CPUs can decode, issue, and retire multiple instructions per cycle, while individual instructions may take multiple cycles — but the fetch-decode-execute contract is never violated, no matter how exotic the internals get.
Reading assembly: a tiny example
Assembly language11 is the human-readable form of machine code: one line per instruction. Here is a complete function (x86-64, Intel syntax) that adds two integers:
add: ; int add(int a, int b) — a arrives in edi, b in esi
mov eax, edi ; eax = a
add eax, esi ; eax = eax + b
ret ; return, result in eax
Three observations that unlock all assembly reading: instructions are verb destination, source; on x86, call pushes the return address and jumps, while ret pops it and jumps back; and a C call like add(40, 2) is just “put 40 in edi, 2 in esi, call add, read eax.” A function is not a hardware concept. The CPU provides instructions such as call, ret, and jumps; the notion of a function comes from conventions built on top of them.
Instruction anatomy
Zoom out one level and the shape is consistent: at a high level, an instruction consists of an opcode describing the operation and, when needed, operands describing the data it operates on. x86 instructions are variable-length (1 to 15 bytes) to keep common operations tiny; ARM64 instructions are always exactly 4 bytes, trading code density for simpler decoding.
Operands come in a few basic forms: immediate (add rax, 5 — the value sits inside the instruction), register (add rax, rbx), and memory (mov rax, [rbx+16] — base register plus offset, the workhorse of struct field and array access).
Calling conventions: how functions really call each other
That example raises an immediate question: why did the arguments arrive in edi and esi? Because caller and callee must agree on where arguments go — through a calling convention12, part of the platform ABI, followed by every compiler so code from different languages can call each other.
The System V AMD64 ABI (Linux, macOS) says: integer arguments go in rdi, rsi, rdx, rcx, r8, r9 (in order, extras spill onto the stack), the return value comes back in rax, registers rbx, rbp, r12–r15 must be preserved by the callee (everything else the caller must save itself), and the stack stays 16-byte aligned. ARM64 does the same dance with x0–x7 for arguments, x0 for the return value, and x30 as the link register holding the return address.
The stack: where calls live
And where does the return address go? Onto the call stack13: simply a region of ordinary RAM plus the rsp register pointing at its top. push decrements rsp and stores, pop loads and increments (on x86 the stack grows toward lower addresses). When a frame pointer is used, local variables can live at fixed offsets from it; temporaries get pushed and popped — the whole elegant structure is just disciplined use of one register.
A typical call frame on entry, then, is pure convention made visible:
push rbp ; save caller's frame pointer (callee-saved)
mov rbp, rsp ; our frame starts here
sub rsp, 16 ; room for locals
...
mov rsp, rbp ; tear the frame down
pop rbp
ret
Modern compilers often omit the frame pointer and address locals relative to rsp instead. A debugger reconstructs stack traces from information about these frames, return addresses, and unwind metadata. There is no function table in the sky — just return addresses, stack frames, and unwind information that debuggers can use to reconstruct the call chain.
The heap: memory on request
And malloc? The heap is even less magical: it is a region of virtual memory managed by an allocator such as malloc, which obtains pages from the OS (via brk or mmap system calls). malloc and free are not CPU features at all — they are library code maintaining lists of free chunks inside that region. At machine level there are no variables, no objects, no heap allocations. There are only addresses, values, and conventions.
The big picture
A CPU is a contract (the ISA), a workbench (registers), a calculator (the ALU), and a conductor (the control unit), running instructions through an enormously more complicated version of fetch-decode-execute. Assembly exposes that machine directly; the stack, heap, and function calls are conventions built on top of addresses, values, and control flow. The next layer up — how an operating system turns this lonely executor into a machine running a hundred programs at once — is another post.