The SEQ Stages
The six SEQ stages, made exact. For every Y86-64 instruction — halt, nop, the moves, OPq, the jumps, call and ret, pushq and popq — we write down what Fetch, Decode, Execute, Memory, Write-back, and PC update each compute, as per-instruction stage tables with every row justified.
╌╌╌╌
The last lesson named the six SEQ stages and gave each a one-line job. That is not enough to build anything: a circuit needs the exact value on every wire, for every instruction. This lesson supplies it. For each Y86-64 instruction we fill in a stage table: six rows, one per stage, each holding the precise computation that instruction requires of that stage. These tables are the specification of the processor; lessons 3 through 5 are just the work of realizing them in hardware. Get a table wrong here and the silicon runs the wrong program.
The signals every stage produces
Before the tables, the vocabulary. Each stage produces named signals that later stages consume, and the same names recur in every table.
- Fetch produces
icode:ifun(the opcode),rA,rB(register IDs, if the instruction has a register byte),valC(the 8-byte constant, if present), andvalP, the address of the following instruction: , where is 1 if the instruction has a register byte and is 1 if it has avalC. - Decode selects two source register IDs
srcA,srcBand reads , . It also selects two destination IDsdstE,dstMfor write-back. - Execute drives the ALU with inputs
aluA,aluBand a function, producingvalE; onOPqit sets the condition codes; for branches and conditional moves it computesCnd. - Memory computes an address, then reads () or writes ().
- Write-back writes and .
- PC update picks
newPC.
The names follow a discipline that reading stage tables fluently depends on.
The discipline pays off when tables look alike. pushq and call both write
in Memory, and the names tell you the address
was computed by the ALU (valE), not read from a register. popq and ret both
read at valA, and the name tells you the address came straight from a register
read, bypassing the ALU.
Reading a stage table: OPq
Take the arithmetic instruction OPq rA, rB (which covers addq, subq, andq,
xorq — the ifun selects which). It reads both rA and rB, computes
, sets
the condition codes, and falls through to the next instruction. Every stage gets a
concrete value.
For OPq the source registers are exactly rA and rB, and the one destination is
rB, written from valE. The ALU's second operand aluA is valA and its first
aluB is valB, and the result is valB OP valA — order matters, because
subq rA, rB must compute .
The trivial pair: nop and halt
Two instructions do almost nothing, and their tables say so. nop occupies
a byte and a cycle: fetch it, compute valP, and fall through; every other row is
empty. halt is one row stranger: it too fetches and computes valP, but it also
sets the status Stat to HLT, and a machine whose status is not AOK does not
begin another cycle, so the PC-update row is moot. Nothing else in the datapath
moves.
Idle rows are not wasted rows. nop still takes a full cycle, because in SEQ every
instruction takes exactly one cycle regardless of how little it does, a fact with
consequences for timing,
since the clock must be slow enough for the longest instruction, and nop then
pays for time it never uses.
Register moves: rrmovq, cmovXX, irmovq
The register-to-register move rrmovq rA, rB and the immediate move irmovq V, rB
share a trick: neither has any arithmetic to do, but both route their value
through the ALU anyway, adding zero. rrmovq computes
; irmovq computes
. The ALU is sitting in the middle of the
datapath whether we use it or not, so the cheapest path from value in hand
to
register write port
is straight through it with the other input forced to zero.
The conditional moves cmovXX rA, rB share rrmovq's icode (they differ only in
ifun) and its table, with one addition: Execute also evaluates
, and Write-back happens
only when Cnd holds: the destination select dstE becomes RNONE on a failed
condition, so the write port idles and rB keeps its old value. The move is
squashed, not skipped: the instruction still spends its cycle and still falls
through to valP.
Memory instructions: rmmovq and mrmovq
The memory instructions use the Execute stage as an address adder and give the
Memory stage real work. rmmovq rA, D(rB) stores R[rA] to memory at
; mrmovq D(rB), rA loads from the same address into rA. The
ALU adds the displacement valC to the base valB in both cases; the difference is
only the direction of the memory access and which register, if any, is written.
rmmovq writes a register in no stage (its Write-back row is empty), while
mrmovq writes rA from valM. Compare the two Memory rows: the same address
valE appears on both, once as a write target, once as a read source: one address
computation serves both directions.
It is worth running one table with real numbers, because the abstract rows hide how
little arithmetic each stage does. Take mrmovq 8(%rdx), %rax with %rdx = 0x200
and the eight bytes at address 0x208 holding the value 0x2a. The bytes of the
instruction, laid out by the assembler, are 50 02 then the 8-byte displacement 08 00 00 00 00 00 00 00.
Five of the six rows carry a concrete value, and every one is forced by an earlier
row or by prior state: valC = 0x8 came out of the instruction bytes, valB = 0x200
was already in %rdx, valE = 0x208 is the one addition the ALU performs, and
valM = 0x2a is what memory returned at that address. Nothing in the table is a
choice; the only decisions — which register feeds srcB, which ALU function runs,
whether memory reads — belong to the control logic of the next lesson. The stage
table is what the control logic must arrange to be true.
Stack instructions: pushq and popq
The stack instructions are where the ALU does an off-to-the-side adjustment of %rsp.
pushq rA decrements %rsp by 8, then stores R[rA] at the new top.popq rA reads the word at the current top, then increments %rsp by 8. The
order matters and the tables make it exact.
Both tables hide edge cases that surface exactly when rA is %rsp itself, and
both are resolved by reading the rows in order.
For every other rA, the two write ports of the register file are what let popq
update %rsp (from valE) and rA (from valM) in the same cycle, the reason
the register file was built with two write ports at all.
Control transfer: jXX, call, ret
The control instructions are the ones that make newPC interesting. Start with the
jumps: jXX Dest carries the destination in valC and needs neither the register
file nor the ALU. Its whole Execute row is the evaluation of the branch condition
from the condition codes
that some earlier OPq left behind.
The seven conditions (jmp always, then jle, jl, je, jne, jge, jg) are
Boolean functions of ZF, SF, OF, selected by ifun, the same Cond unit the
cmovXX family uses, derived in the
next lesson.
An untaken jump is a nop that cost nine bytes: it computes Cnd = 0 and falls
through to valP.
call Dest and ret complete the set. call pushes the return address valP and
jumps to valC; ret pops the return address into the PC.
call and pushq share their Execute and Memory shape — decrement %rsp, store at
the new top — but call stores valP (the return address) rather than a register
value, and its newPC is valC, not valP. ret and popq are the mirror image:
read from the old top, increment %rsp.
ret deserves a slower look, because one byte of encoding drives more machinery
than any other instruction its size. It names no register, yet it uses %rspthree times: Decode reads it twice ( supplies the memory address,
the value to increment), and Write-back stores the incremented
pointer through dstE. On top of that it reads data memory at the old top
(valA, not valE — read the Memory row carefully) and commandeers the PC mux.
The single difference between ret and popq is the last row: ret sends valM
to the PC, while popq sends it to a register and falls through to valP. That one
mux input is the entire difference between return from a function
and pop a value.
The vertical view: six stages, every instruction
Stacking the stages vertically, every instruction is the same column of six boxes — only the contents of the boxes change. This shape mirrors how lesson 4 lays out the complete datapath: six horizontal bands, Fetch at the bottom, PC update at the top.
Stage tables as the ancestor of micro-ops
A stage table is a claim that every instruction, however different its surface syntax, decomposes into the same small vocabulary of primitive steps: a register read, one ALU operation, at most one memory access, a register write, a PC choice. That decomposition is also how real high-performance processors run a complicated instruction set.
Modern x86-64 cores do not execute their sprawling CISC instructions directly.
Instead, the front end cracks each architectural instruction into a sequence of
uniform internal operations, the micro-operations
(µops) mentioned in CS:APP's RISC
versus CISC aside (Bryant & O'Hallaron, CS:APP §4.1) and developed in its treatment
of modern processors (§5.7). A Y86-64 pushq already shows the pattern in miniature:
its stage table is subtract 8, then store,
which is exactly two µops' worth of work
riding on one instruction. On a real x86 machine, an instruction with a
memory-operand-and-arithmetic form similarly expands into a load µop, an ALU µop, and
sometimes a store µop — each of which looks like one filled-in row of a stage table.
The back end then schedules these uniform µops out of order across duplicated
functional units. The reason the translation is even possible is the reason stage
tables are worth writing: once you have reduced every instruction to a fixed sequence
of primitive stage operations, the hardware that carries out any one primitive can be
built once and shared by all. The SEQ tables are that reduction done by hand, for a
small enough ISA that the whole vocabulary fits on a page.
With every instruction's stage table fixed, the
next lesson
derives the control signals — srcA, dstE, the ALU function, the memory
controls, newPC — that select among these computations from icode alone.
╌╌ END ╌╌