Tracing a Program
To close the module, we take a complete Y86-64 program — a loop that sums 1 through 3 — and run it through SEQ one cycle at a time, recording the PC, the fetched instruction, every stage computation, and the registers, condition codes, and memory after each cycle. Then we examine single cycles in detail: every named signal of an OPq in concrete hex, and a second program whose call and ret we trace through the stack.
╌╌╌╌
We have a complete datapath and we have proven it correct on two instructions in isolation. The last thing to do is the thing the whole module was for: run a real program and watch state evolve. Correct stages in isolation are not enough: a processor must, started at an address, drive itself through a program by the loop we built, cycle after cycle, with no external help. This lesson takes a short Y86-64 program, sets it on SEQ, and traces every cycle until it halts. If the registers hold the right answer at the end, the assembled datapath and control logic are a working CPU. They do.
The program
We compute with a countdown loop: start a counter at 3, add it to a running sum, decrement, and repeat until the counter hits zero.
irmovq $3, %rdi # rdi = n = 3 (loop counter)
irmovq $0, %rax # rax = sum = 0
irmovq $1, %r8 # r8 = 1 (decrement constant)
loop:
addq %rdi, %rax # sum += n
subq %r8, %rdi # n -= 1
jne loop # if n != 0, repeat (tests ZF)
halt
The assembler lays this out from address 0x00. Each irmovq is 10 bytes, each
OPq is 2, the jne is 9, and halt is 1, giving the byte addresses below, which
is what the PC steps through.
How to read one cycle
SEQ executes one instruction per clock cycle: in a single tick it fetches, decodes, executes, touches memory, writes back, and updates the PC, then the new register and PC values clock in at the rising edge, ready for the next cycle. So one row of the trace is one cycle is one instruction. For each we record the PC, the instruction fetched there, the salient stage computations, and the resulting state.
The three irmovq cycles are the warm-up. Each fetches at its address, passes the
immediate through the ALU (valE = 0 + valC), and writes valE to its register; none
touches memory or the condition codes, and newPC = valP.
After cycle 3 the state is , , , condition codes unset, and — the loop entry. Now the interesting part.
The loop, cycle by cycle
Each iteration is three cycles: addq, subq, jne. The addq updates the sum and
sets the condition codes; the subq decrements the counter and sets them again; the
jne reads ZF to decide whether newPC is the loop target valC = 0x1e (taken) or
the fall-through valP = 0x2b (not taken). Watch %rax climb and %rdi fall.
Three iterations and the loop is done. The first addq adds 3, the second 2, the
third 1, so %rax walks . Each subq drops %rdi by one,
; the moment it reaches 0, that subq sets ZF = 1, and the
following jne reads ZF = 1, so its branch condition Cnd is false and newPC
falls through to 0x2b instead of looping. Cycle 13 fetches halt at 0x2b, which
sets Stat = HLT, and the machine stops with — the sum
, exactly as the program intends.
Laying out the register state across the twelve loop cycles as one table makes the convergence visible.
Read down the %rax column and the program's whole purpose is visible: 3, 3, 3, 5, 5,
5, 6, 6, 6, 6 — the sum settling as each iteration folds in one more term. Read down
ZF and the loop's exit condition is visible: it is 0 until cycle 11's subq zeroes
%rdi, and the cycle-12 jne that reads it is the one that finally falls through.
Cycle 4 under the microscope
The trace above records outcomes. To connect it back to the
control logic,
freeze one cycle and write down every named signal: the full contents of the
stage tables and HCL, evaluated with real numbers. Take cycle 4, the first addq %rdi, %rax, fetched at 0x1e with %rdi = 3 and %rax = 0.
Every entry is forced. The bytes 60 70 are forced by the assembler; the selects
srcA = rA, srcB = rB, dstE = rB are the OPq lines of the HCL; the values
0x3 and 0x0 are what cycles 1 and 2 left in the registers; ZF = 0 because
0x3 is nonzero. Each signal traces back to the matching case expression in
lesson 3; this table is that logic, evaluated once.
A snapshot mid-flight
The same cycle, drawn on the datapath. The figure shows the live values on the key wires as the instruction climbs the bands.
Every value on that snapshot was derived, not asserted: valA = 3 because Decode read
%rdi, which cycle 1's irmovq set; valE = 3 because the ALU added valB = 0 and
valA = 3; ZF = 0 because the result is nonzero; newPC = 0x20 = 0x1e + 2 because
addq is two bytes and is not a branch. The control logic of
lesson 3 chose
every mux input; the stage tables of
lesson 2 defined every
computation; the datapath
carried the bits. Nothing outside the machine intervened.
A second program: call and ret through the stack
sum.ys never touches memory. To watch the stack machinery — the %rsp bookkeeping,
the memory write of a return address, the valM-into-PC wire — run a second program,
five instructions long.
irmovq $0x100, %rsp # 0x000: set up the stack
call sum3 # 0x00a: push 0x013, jump to sum3
halt # 0x013: after the return
sum3:
irmovq $6, %rax # 0x014: the function body
ret # 0x01e: pop 0x013 into the PC
The layout: irmovq (10 bytes) at 0x000, call (9 bytes) at 0x00a, halt at
0x013, then sum3 at 0x014 with its irmovq and the ret at 0x01e. Five
cycles run: the two irmovqs bracket the call (cycles 1–3), then ret (cycle 4)
and halt (cycle 5). The two cycles worth tracing in full are the call and the
ret.
The two tables are mirror images through the stack. call writes 0x013, an
address it computed as valP (the byte after its own 9-byte body), into memory at
the decremented %rsp, and that number sits inert at 0xf8 for two cycles while
sum3 runs. Then ret, knowing nothing about who called, reads whatever the top of
the stack holds, and the machine lands back at halt. The trace confirms what
the procedures lesson
claimed: a return address is just a word in memory, trusted absolutely.
Check the arithmetic that has to line up for this to work, because a single wrong
constant would break it. The call sits at 0x00a and is 9 bytes long, so its valP
is — the address of the halt, the point
where control should resume. Execute decrements the stack pointer by 8, so
, and Memory stores 0x013 there. When ret runs,
its Execute adds 8 back (, restoring %rsp), but
its address is valA = 0xf8, the pre-increment top, so it reads the word 0x013
that call left — not the word above it. The off-by-eight bug flagged in
lesson 3's memory control
amounts to using valE = 0x100 as the read address instead of
valA = 0xf8; the trace shows the correct choice landing back on 0x013 and the
halt, and any other choice would return into garbage.
Counting cycles and the CPI that motivates pipelining
The trace also lets us measure SEQ, and the measurement motivates the next module. sum.ys executed 13 instructions in 13 cycles: three irmovq, three
loop iterations of three instructions each, and one halt. SEQ runs exactly one
instruction per cycle, so its cycles per instruction (CPI) is exactly 1 — you
cannot do better than one instruction every clock tick if the whole machine is one big
combinational settle per instruction.
CPI is only half of what determines speed. The usual accounting (Bryant & O'Hallaron, CS:APP §4.3 and §5.7) writes a program's running time as
and SEQ wins the middle factor (CPI = 1) only by paying dearly on the third. The clock
period had to cover the entire critical path — instruction fetch, register read, ALU,
data memory, and the New-PC mux in series — so on the illustrative delays of
the previous lesson that
period was near 390 ps. Every one of sum.ys's 13 cycles was that long, even the
addq cycles that needed no memory at all.
Pipelining attacks the third factor without giving up much of the first. If the six
stages each get their own clock band and several instructions occupy different stages
at once, the clock period drops toward the delay of the single slowest stage (roughly
the 120 ps of a memory access) rather than the sum of all of them. The ideal is CPI
still near 1 but a clock several times faster — which, on a straight-line program like
sum.ys, is a several-fold speedup for free. The catch is the loop: sum.ys's jne
does not know its target until it has been executed, and a pipeline that has already
begun fetching past the branch may have guessed wrong. Resolving those hazards — data
dependencies between overlapping instructions, and control dependencies at branches —
is the entire subject of the pipelining
module, and this trace is the
baseline it improves on: correct, simple, and one instruction slow at a time.
What we have built
The traces close the loop the module opened. Twelve cycles of pure combinational choices, clocked one instruction at a time, took three registers from an initial state to the correct answer and stopped at the right place; five more cycles pushed a return address, ran a function, and came back through it, and at no point did anything direct the machine but its own PC and the bytes it pointed at. That is the entire claim of the stored-program computer, realized in logic we specified gate by gate.
The course has now gone from bits and bytes, through an instruction set, through gates and memory, to a circuit that runs programs. From here the questions become how fast — the same six stages, overlapped in a pipeline so several instructions are in flight at once — and how to keep the data close, with the memory hierarchy and caches.
╌╌ END ╌╌