Gates → GPUs · Notes
Unit 2 · Lessons 5–7

Registers, programs & execution contexts

Unit 1's machine can calculate, but its one accumulator forces a working value out whenever the next calculation needs that space. We start by giving the machine registers, then use a real instruction set (RISC-V) to explore three layers: the contract a program relies on, the hardware that carries it out, and the conventions that let programs call functions and pause threads.

You'll be able to
Trace where every value lives in a register machine, show that one instruction set can have very different hardware, and explain exactly what must be saved to return from a function or to resume a paused thread.
Carries over
Everything from Unit 1: state, clock edges, fetch–decode–execute, PC, and the idea of a saved context.
A note on models
The steppers below advance one whole instruction at a time. They're small teaching models of a few RISC-V instructions, not full emulators. Only the Lesson 6 comparison models elapsed time.
Lesson 5

Keep working values in registers

LimitationOne accumulator can retain only one working result. Keeping another result alive while computing something else may require temporary memory.
New mechanismA register file, plus a load/store instruction set where arithmetic only touches registers.
New state32 architectural registers (x0 is always 0), named by number in each instruction.
CostStorage, read/write ports, wiring, and instruction bits to name registers. Registers are still finite, so values sometimes spill to memory.

First, count what the one-accumulator machine has to do. Then we can see exactly which memory trips a register machine avoids.

Preserving two intermediate results with one accumulator

Take a small calculation with values a = 10, b = 6, c = 3:

y = (a + b) + (a − c) = 16 + 7 = 23

Suppose we evaluate the two parenthesized parts separately. After a + b, ACC holds 16. But computing a − c will overwrite ACC. Where can 16 wait? NIB-8 must store it in a temporary memory location, t, and read it back for the final ADD. The names a, b, c, t and y stand for distinct memory addresses; a real NIB-8 program would use their four-bit addresses.

0LOAD aACC = 10
1ADD bACC = 16
2STORE tsave the first partial result, or it's lost
3LOAD are-read a, which we just had
4SUB cACC = 7
5ADD tACC = 7 + 16 = 23
6STORE ywrite the answer

Count the data accesses: LOAD a, ADD b, the second LOAD a, SUB c and ADD t make five reads; the two STORE instructions make two writes. Three of those accesses—STORE t, the second read of a, and ADD t—come from having only one place for a working value.

This is a comparison of storage choices for this evaluation order. Reordering the arithmetic as a + b + a − c could avoid the temporary. We keep the parenthesized parts separate so the cost of preserving both results is easy to see.

A register file

Give each intermediate result its own nearby place to wait. A register file is a small array of fast registers, each selected by number. In add x28, x5, x6, x5 and x6 name the source registers (rs1 and rs2); x28 names the destination (rd). These numbers identify registers, not the values in them. If x5 holds 10 and x6 holds 6, the result written to x28 is 16.

Follow that instruction through the figure. Two read ports—independent paths out of the file—supply 10 and 6 to the ALU together. A write port carries 16 back into x28 at the clock edge. Real register files can have different port counts, but this two-read, one-write arrangement explains how an arithmetic instruction can use two register operands without reading data memory.

Register file read reg 1 (rs1 = 5) read reg 2 (rs2 = 6) write reg (rd = 28) write data (at clock edge) value 10 value 6 ALU 16 goes back to x28
Two read ports and one write port. In this datapath, add reads its two source registers through separate ports and writes the result through the write port. A small register file and short connections to the ALU can make these accesses fast. Keeping an intermediate result in a register avoids the data-memory access needed to retrieve it.

Why not add hundreds of registers? Each one needs storage and wiring, and instructions need bits to name them: 32 choices require 5 bits in each register field. A larger file, especially one with more ports, can also be slower to access. RV32I chooses 32 architectural register names, x0–x31. They are the names instructions can mention; the hardware may use a larger physical register file internally, as Unit 3 will show.

Stepping onto a real instruction set: RV32I

To make the comparison concrete, we now move from the toy NIB-8 machine to RISC-V, specifically its 32-bit base integer instruction set, RV32I. An instruction set architecture (ISA) describes the instructions and visible machine rules a program can rely on. RV32I is a real, openly documented ISA; Lesson 6 will look at the hardware choices that can implement it. Here are the rules that change:

FeatureNIB-8RV32I
Working registers1 (ACC)32, named x0–x31. x0 always reads 0 and ignores writes.
Register width8 bits32 bits
An address names…one 8-bit wordone byte. A 32-bit word takes 4 consecutive addresses.
Instruction size1 byte4 bytes (so sequential fall-through advances PC by 4; a taken branch or jump sets another PC)
Arithmetic operandsACC and one memory wordregister operands and, for some instructions (like addi), an immediate constant; no data-memory operands in arithmetic (load/store design)

Here rd means destination register, rs1 and rs2 mean source registers, and imm is an immediate constant encoded in the instruction itself. In a load or store, base names a register whose contents are added to the offset; label marks a destination in the program, and the assembler encodes the displacement to it. We will use just these instructions:

InstructionMeaning
add rd, rs1, rs2rd ← rs1 + rs2
sub rd, rs1, rs2rd ← rs1 − rs2
addi rd, rs1, immrd ← rs1 + imm, where imm is a constant stored inside the instruction
lw rd, offset(base)rd ← the 32-bit word at byte address base + offset
sw rs2, offset(base)store rs2's value at byte address base + offset
beq rs1, rs2, labeljump to label if rs1 = rs2; otherwise continue at the next instruction (PC + 4)
RV32I rules these examples rely on. Arithmetic works on 32-bit values; add, sub and addi keep the low 32 bits of the result and don't trap on integer overflow. The addi immediate and the load/store offsets are signed 12-bit constants, −2048 to 2047. Offsets count bytes. The examples assume aligned, little-endian word accesses.

A signed immediate can be negative: addi sp, sp, -16 subtracts 16 from the stack pointer, as the call example in Lesson 7 will do. In contrast, lw x6, 4(x10) adds a positive offset to its base register. Both use a number encoded in the instruction; neither gets that number by reading data memory.

Follow the register names, offset, address, and loaded value

What does lw x6, 4(x10) actually load? Suppose x10 holds 0x100. The offset 4 is a decimal byte count, so 0x100 + 4 = 0x104. That sum is an address, not the data. The load reads the word stored there and puts it in x6. Follow each number's role in the figure.

lwx6,4(x10) 6destination register # 4immediate offset x10 = 0x100base (register contents) + address = 0x104 6M[0x104] …and the loaded value 6 goes into x6
The register numbers 6 and 10 identify registers inside the CPU. The offset 4 is encoded in the instruction. Register x10 contains the base address 0x100; adding the offset produces the effective address 0x104. Memory contains the word 6 at that address, and the load writes that word into x6.

The load reads four byte addresses because a 32-bit word contains four bytes. Which byte goes at the first address? In the little-endian data-memory layout used here, it is the least significant byte. Storing 0x12345678 at 0x104 therefore places 0x78 at 0x104, 0x56 at 0x105, 0x34 at 0x106 and 0x12 at 0x107.

Read the four bytes back as one word, and the value is still 0x12345678. Endianness changes the order of bytes in memory, not the value of the assembled word. RISC-V instruction fetch is little-endian; systems may choose either byte order for data, so these examples explicitly use little-endian data.

Little-endian layout of the word 0x12345678 at 0x104: bytes 78 56 34 12. The byte at the lowest address is the least significant; read from the highest address down to recover the value. (The word b = 6 in the load example above is stored the same way: 06 00 00 00.)

Try it: the same calculation, two machines

Before stepping, predict which accesses disappear if a and both partial results can stay in registers. Then watch the data-memory counters. The register version keeps a in x5, so it never re-reads it. The other side models NIB-8's one-accumulator behavior with larger, RV32I-like addresses so both versions can show the same data locations. It is a comparison model, not a literal run on NIB-8's 16-word memory.

Registers

Data memory (words)

Same answer, less memory traffic: 3 reads and 1 write instead of 5 and 2. That shows one benefit of registers. It doesn't prove a particular speedup, because the two machines differ in instruction width, clock and memory system, and a "memory access" later won't even mean "a trip to DRAM" (Unit 3's caches).

Registers are not variables

Source-code variable names do not appear in the ISA. The compiler decides where each value lives. In our calculation, x5 holds a; once a is no longer needed, the same register could hold an unrelated value. A variable might also live in memory, be copied in both places, or disappear if the compiler can compute the answer without storing it.

There is still a limit. If the program needs more values alive at the same time than it has available registers, they cannot all stay in registers. Saving one to memory for later use is called a spill; using it again requires a load. Compilers try to reduce this pressure by reordering work or recomputing a cheap value. The figure shows why a register machine can still create data-memory traffic.

Register pressure. Ten values are needed at once, but only four registers are free. With a fixed computation order and no recomputation, six must reside in memory. Using them later requires loads; saving a newly produced value may first require a store. More registers or fewer simultaneously live values reduce this pressure. GPUs face the same problem in a later unit.
PredictUse the stepper only to check.
  1. In the RV32I version, what are x5, x28, x29 and M[0x10C] immediately after the instruction at 0x50?
    Show answer

    x5 = 10, x28 = 16, x29 = 7, and M[0x10C] is still 0. The add x30 and the sw still have to run before y is written.

  2. What happens to x0 after addi x0, x0, 9? What does addi x5, x0, 9 put in x5?
    Show answer

    x0 stays 0, because writes to it are discarded. The second instruction puts 9 in x5: 0 + 9. This is how RISC-V loads small constants without a special instruction.

  3. With x10 = 0x100, which byte addresses does lw x6, 4(x10) read? If they hold 06, 00, 00, 00 (lowest address first), what arrives in x6?
    Show answer

    0x104, 0x105, 0x106 and 0x107. Little-endian means the lowest address is the least significant byte, so the value is 0x00000006 = 6.

  4. The data accesses dropped from 7 to 4. Why doesn't that mean the register version runs 7/4 = 1.75× faster?
    Show answer

    Run time depends on instruction count, cycles per instruction and clock period, not just data accesses. Instruction fetches still happen, the machines have different clocks and widths, and how much each access costs depends on the memory system (a cache hit can be much cheaper than a DRAM access). Counting accesses isolates one effect. It isn't a timing model.

  5. A calculation needs 10 values alive at once but only 4 registers are free. Why does having registers still leave memory traffic?
    Show answer

    If all ten values must be kept for later use, only four can occupy those registers at once. The others need another home, usually memory, and will be loaded when needed; a newly computed value may first have to be spilled there. Reordering the work or recomputing a cheap value can sometimes reduce these accesses. Register count alone does not determine the traffic.

For any instruction, identify each number's job: register name, immediate constant, byte address, or data stored at an address. Then ask which working values must survive the next instruction. That question leads from registers to the execution rules in Lesson 6.

Lesson 6

One contract, different hardware

LimitationIf every instruction must finish in one clock cycle, the whole machine runs at the speed of its slowest instruction.
New mechanismSeparate the contract (ISA) from its implementation. For example, a multicycle design reuses hardware across short phases.
New stateInternal registers (instruction, operands, ALU result, loaded data) that instructions cannot name.
CostMore registers, muxes and control. The per-cycle overhead can erase the gain.

RV32I tells us what an instruction must accomplish, while leaving room for different hardware designs. Compare two cores that obey the same rules but organize the work differently.

What the ISA promises, and what it doesn't

The ISA is a contract between a program and a processor. It defines instructions, registers and their effects on visible state: register values, memory, PC and exceptions. For instance, lw x6, 4(x10) must put the word at the computed byte address into x6. A program can rely on that result.

The ISA does not say which circuits fetch the word or how many cycles they take. Those are microarchitectural choices. The figure shows three designs that can run the same instructions. Their internal steps and timing differ, while their defined effects on the program's state agree.

RV32I contract"add x28, x5, x6 sets x28 to x5 + x6, then PC += 4" single-cycle1 long cycle per instruction multicycle3–5 short cycles, reused parts pipelined / out-of-orderUnit 3: many in flight at once
Same contract, different machines. A tiny microcontroller and a high-end core can run the same binary if both implement its required instructions and their surrounding systems provide compatible software conventions and services. Lesson 7 introduces the calling conventions that let separately compiled functions cooperate. The processors' internal steps and execution times can differ while their defined effects on program-visible state agree. For the simplified machines in this unit, we compare that state after each instruction.

Design 1: single-cycle

A single-cycle design finishes one whole instruction between two clock edges. A load must fetch and decode its instruction, read a base register, calculate an address, read data memory and write the result. In the model below, those delays add to 1.0 + 0.8 + 1.2 + 1.2 + 0.8 = 5 ns. The clock period must be long enough for that slowest instruction.

Now look at the add row. It has no data-memory read, so its result becomes stable before the load's result. The destination register still captures that value at the common 5 ns clock edge. The hatched tail shows the time between the result becoming stable and that edge. Every instruction therefore occupies a 5 ns cycle in this design. A load also needs to fetch its instruction and read its data in that one cycle, so this design needs separate instruction and data paths, or two memory ports.

Unused time before capture. Hatched regions show where an instruction's computed values have settled before the clock edge that updates state. A single-cycle design also needs separate instruction and data memories (or two ports), because a load needs both in the same cycle.

Design 2: multicycle

Could we stop making an add wait for a load's full path? A multicycle design splits an instruction into short phases, one per clock cycle, and uses only the phases that instruction needs: F (fetch), D (decode and read registers), E (ALU work or address calculation), M (data-memory access) and W (write a register). In the table below, an add skips M and a store skips W.

The result of one phase must survive until the next. Internal registers capture the instruction, operands, address or loaded data at the clock edge ending a phase. They hold those values while the next phase runs. Instructions cannot name these registers. Since fetch and data access occur in different cycles, one memory port can serve both in this teaching design.

Shorter phases permit a shorter clock, but the period must still fit the slowest phase plus register and control overhead. Reusing the parts from the single-cycle figure would leave E and M at 1.2 ns each, making the new period at least 1.2 ns plus overhead—not 1.0 ns. Shorter cycles do not necessarily make an instruction faster. A load now requires five cycles rather than one, so its elapsed time is five times the multicycle clock period. The load trace below shows why each of those cycles is needed.

Five phases with internal registers between them. The blocks are successive clock cycles of one instruction, not a pipeline: the next instruction doesn't start until this one finishes. IR, A/B, ALUout and MDR are microarchitectural state. No RISC-V instruction can read them, and a different implementation might not have them at all. The one memory can serve fetch in F and data in M, because they happen in different cycles.
InstructionPhases usedCyclesWhy it skips some
lwF D E M W5Uses everything: address in E, read in M, write register in W
add / sub / addiF D E W4No data-memory access
swF D E M4No register to write
beqF D E3E compares and updates the PC if taken

Trace lw x6, 4(x10) with x10 = 0x100 and M[0x104] = 6. Each named register holds a different kind of value:

F: IR receives the instruction. D: A receives x10's value, 0x100. E: ALUout receives 0x100 + 4 = 0x104, the address. M: MDR receives M[0x104] = 6, the data. W: x6 receives 6. PC advanced by 4 in F; x6 changes only in W. Notice how the address must exist before the memory can supply the data.

Compare elapsed time, not cycle counts

The shorter clock pays for the extra cycles only for some instruction mixes. The default sequence—lw, add, add, sw, beq—takes five 5 ns cycles in the single-cycle design, or 25 ns. In a hypothetical multicycle design with a 1.0 ns period, it takes 5 + 4 + 4 + 4 + 3 = 20 cycles, or 20 ns. The bars below show both elapsed times.

That 1.0 ns multicycle period assumes a different, faster implementation from the unmodified 1.2 ns-phase circuit above. Change the sequence or the period to see when the advantage disappears. The useful measure is cycle count × clock period, not cycle count alone.

Add: Presets:

Synthetic teaching timings. The 1.0 ns period belongs to a hypothetical balanced multicycle design, not the unmodified 1.2 ns phases above. Both periods include register and control overhead, assume memory answers within its phase, and model one instruction at a time. The default sequence takes 25 ns versus 20 ns; at a 1.3 ns multicycle period it takes 26 ns.
A multicycle core still finishes one instruction before starting the next. It splits one instruction's work across time; pipelining (Unit 3) overlaps different instructions. F, D, E, M and W are a teaching model, not a claim that every real CPU has exactly these five steps.
PredictCalculate first, then use the comparator.
  1. After phase E of lw x6, 4(x10) (x10 = 0x100), does ALUout hold 0x104 or 6? Has x6 changed yet?
    Show answer

    ALUout holds the address 0x104. Memory supplies the data, 6, during M; x6 receives that data only during W.

  2. For the sequence lw, add, add, sw, beq, compute each design's total time and average CPI (single 5 ns, multicycle 1 ns).
    Show answer

    Single-cycle: 5 instructions × 5 ns = 25 ns, CPI 1. Multicycle: 5 + 4 + 4 + 4 + 3 = 20 cycles × 1 ns = 20 ns, CPI 20/5 = 4. The design with four times the CPI is faster, because its cycle is five times shorter.

  3. For five loads, does the multicycle design's 5× faster clock give a 5× speedup?
    Show answer

    No, there's no speedup at all: single-cycle 5 × 5 = 25 ns, multicycle 5 × 5 × 1 = 25 ns. Loads use every phase, so there's nothing to skip.

  4. If the extra registers and muxes push the multicycle period to 1.3 ns, how long does the mixed sequence take?
    Show answer

    20 × 1.3 = 26 ns, which is slower than the single-cycle design's 25 ns. For this sequence, the additional cycles outweigh the benefit of the shorter clock period.

  5. If one phase needs 1.2 ns, can this implementation safely use a 1.0 ns clock without changing that phase?
    Show answer

    No. Every phase must finish before the receiving register's setup deadline. If the logic alone needs 1.2 ns, the period must exceed 1.2 ns once register and control overhead are included. At 1.0 ns the register could capture an unsettled value. A faster clock needs that phase shortened or divided into smaller phases.

  6. A CPU's documentation says it uses F–D–E–M–W internally. Does that prove it's pipelined? What would?
    Show answer

    No. A multicycle machine uses those phases one instruction at a time. Pipelining is shown by overlap: a second instruction being fetched while the first is still in a later phase, so instructions complete more often than one per full phase sequence.

Describe lw x6, 4(x10) three ways: what it changes in program-visible state, what the multicycle hardware does between F and W, and how long it takes on each design. Unit 3 starts the next instruction before this load finishes.

Lesson 7

Calls, stacks & saved contexts

LimitationA plain jump changes PC without saving a return address. An interrupt handler may overwrite register values that the interrupted code still needs.
New mechanismLink registers for returning, a stack by convention, trap entry into privileged handlers, and saved thread contexts.
New stateRoles for existing registers (the ABI names x1 ra and x2 sp; no registers are added), stack frames in ordinary memory, the OS's saved contexts (one per thread), and trap/privilege registers.
CostSaving and restoring takes instructions and memory traffic. Every context switch has overhead.

A function call, an interrupt and a thread switch can all stop one sequence of instructions and later continue it. The difference is who arranged the stop and how much state must survive. Start with the simplest case: a function that knows it will return.

Saving a return address

A plain jump changes PC without preserving a return address. RISC-V's jal rd, target ("jump and link") records the address of the next instruction, PC + 4 in RV32I, in register rd before jumping. At 0x44 in the figure, that saved address is 0x48. By convention, a function call chooses ra (x1) as the destination, making it the return address register.

The return instruction, jalr x0, 0(ra), jumps to the address currently in ra. It discards its own link value by writing it to x0; the assembler calls this instruction ret. A target such as f is written as a label in assembly, and the assembler encodes its displacement from the call's PC. jal does not store an absolute target address.

main: 0x40 addi a0, x0, 5 0x44 jal ra, f 0x48 … continues here f: 0x80 …… (f's work) 0x9C jalr x0, 0(ra) jal effect: PC ← target (0x80) ra = 0x48 jal effect: ra ← old PC + 4 later, jalr: PC ← ra
jal does two things at once: it records the return address in ra and jumps. Both effects belong to the same instruction and both use the old PC (0x44), so ra = 0x44 + 4 = 0x48. jalr later changes PC using the saved register value. Software uses these instruction effects for function calls and returns. The calling convention determines where arguments, results and saved values go.

The calling convention: a software agreement

Separately compiled pieces of code can only cooperate if they agree on which registers carry arguments, which hold the return address and stack pointer, and which a called function may freely overwrite. That agreement is part of the ABI (application binary interface), and it's layered on top of the ISA:

NameRegisterRole by conventionPreserved across a call?
rax1return addressNo: each call overwrites it
spx2stack pointerYes: restored before returning
a0–a7x10–x17arguments; a0 (and a1) also carry return valuesNo
t0–t6x5–7, x28–31temporaries: the caller saves them if it still needs themNo
s0–s11x8–9, x18–27saved: the callee must restore them if it modifies themYes

Who preserves what. Suppose main has a value in t0 that it still needs after calling g. The convention lets g overwrite t0, so main, the caller, must save the value before the call and reload it afterwards.

Now suppose g wants to change s0. The convention promises main that its s0 will survive, so g, the callee, saves the incoming value and restores it before returning. If g only reads s0, it has nothing to restore.

The stack is a region of ordinary memory managed by convention. It grows toward lower addresses, and sp stays 16-byte aligned. A function needing space subtracts from sp on entry, reserving a stack frame for saved registers or local data. Moving sp from 0x200 to 0x1F0, for example, reserves the 16 bytes from 0x1F0 through 0x1FF.

Before returning, the function restores sp. That releases its claim on the frame; it does not erase the bytes. The hardware sees memory and a register called x2. Software gives that register the role of stack pointer and decides where each frame begins and ends.

Nested calls overwrite ra, so save it

Our example computes f(5) = double(double(5)) + 1 = 21. At address 0x44, main calls f, putting its return address 0x48 in ra. But f itself calls double, and that next jal needs ra for its return address. One register cannot hold both addresses. So f reserves a stack frame and saves 0x48 there before making either nested call.

f's 16-byte frame. addi sp, sp, -16 reserves it and sw ra, 12(sp) writes 0x48 to 0x1FC. Only 4 of the 16 bytes are used, but the convention keeps sp 16-byte aligned. double needs no frame in this example: it makes no calls and needs no stack storage or saved registers. Being a leaf (calling nobody) isn't enough by itself: a leaf that needs a local array or has to spill values would still reserve a frame.

The figure shows where main's return address, 0x48, is stored while f runs. At 0x88, jal saves 0x8C in ra and jumps to double. There, add a0, a0, a0 changes a0 from 5 to 10, and jalr returns to 0x8C. The second call, at 0x8C, saves 0x90 in ra; double changes a0 from 10 to 20 and returns to 0x90. After adding 1, f has a0 = 21. It reloads 0x48, restores sp, and returns to main. Saving the outer return address on the stack made all three returns possible with one live ra register.

Try it: trace the calls, then break them

Step through and watch ra, sp and the stack. Then switch on Remove save/restore, which turns sw ra and lw ra into no-ops, and step again to see exactly where things go wrong.

Initial state: PC = 0x40, sp = 0x200, ra = 0, a0 = 0

Registers

Stack memory

The timeline shows which function is running after each instruction. With the save and restore removed, f's final return jumps to 0x90 inside f. The frame has already been released. Each repeated pass adds 1 to a0, advances sp by another 16, and returns to 0x90 again.
Releasing a frame (addi sp, sp, 16) just moves the boundary. It doesn't erase anything. The old 0x48 is still sitting at 0x1FC afterwards, and it will be overwritten by whoever uses that stack space next.

Interrupts: when the running code didn't agree to stop

A function call is planned: the caller knows the callee's register obligations. An interrupt can arrive between any two instructions, so the running code has arranged nothing with its handler. Two related events also transfer control to a handler:

  • Synchronous exception: caused by the current instruction, for example an illegal instruction or a bad memory access.
  • Interrupt: asynchronous, caused by something outside the instruction stream, such as a timer tick or a network packet.
  • System call: an intentional request for OS service. On RISC-V that's the ecall instruction.

All three events enter through a trap: control goes to a configured handler. Hardware records a small amount of control information in special control and status registers (CSRs)—the relevant PC, the cause, and status such as previous privilege and interrupt-enable state. A trap from user code enters a more privileged mode so the handler can perform protected work. Recording these control values is only the first step; it is not a copy of the thread's whole context.

Handler software must also preserve any ordinary registers it changes if the interrupted computation is to continue unchanged. RISC-V does not automatically save all 31 writable integer registers. The handler may return to the same thread, or the OS may save that thread's resumable state and run another. Entering a trap alone does not make a thread switch happen.

The return address depends on the event. A transparent interrupt normally resumes at the saved PC with the interrupted register values restored. A completed system call may deliberately return a result in a register and resumes after its ecall. A recoverable fault may retry the instruction that caused it; an unrecoverable one may terminate the thread. Exact CSRs and privilege transitions depend on how the trap is configured (see the RISC-V privileged specification).

How a system call gets past its ecall. RISC-V records the address of the ecall instruction itself in the handler's exception-PC register (sepc for a supervisor-mode handler). The trap-return instruction resumes at whatever that register holds; it doesn't add 4 on its own. So for an ordinary completed system call, the handler advances the saved PC:

ecall at 0x100 → saved exception PC = 0x100 → handler sets it to 0x104 → trap return resumes at 0x104

xv6, a small teaching operating system, advances this saved PC by 4 (p->trapframe->epc += 4 in trap.c). For an interrupt the saved PC is already the right place to resume, so nothing is added. For a recoverable fault, such as a page fault the OS fixes, the saved PC deliberately stays on the faulting instruction so it runs again.

Three transfers that are easy to confuse. Entering the kernel does not mean switching threads. Often the handler just deals with the event and returns to the same thread: to the saved PC after an interrupt, or past the ecall after a system call. A context switch happens only when the OS scheduler decides that a different thread should run.

Program, process, thread, hardware context

If the OS chooses another thread, what exactly is being replaced? A program is a description of code and data. A process is an OS-managed instance with an address space and resources. Inside it, each software thread has its own path through the instructions: its own PC, register values and stack. Two threads in one process can therefore be at different places in the same code.

The core's hardware context holds the live state for one instruction stream. With the single context shown in the figure, only one thread can run at a time. The OS gives another thread a turn by saving the outgoing thread's state in memory and loading the incoming thread's state into that live context.

Processan OS container: address space + resources shared: code · global data · heap · open files(one address space) Thread Aown PC + registersown stack (by convention)continuation: "what's next" Thread Bown PC + registersown stackcontinuation Physical core hardware contextlive PC + x1…x31(+ privilege, CSRs) loads A or BThe OS loads one thread's saved context into the hardware context. The other thread is ready but not running.
Layers of "who is running". Some cores support simultaneous multithreading (SMT), which provides several hardware contexts. The core shown here has one live hardware context, so it runs one thread at a time. The others are ready (runnable but not running) or blocked (waiting for an event). Their register context is saved in OS memory, while their stacks, other memory and scheduler records continue to exist.
TermWhat it is in our model
ProgramCode and data definitions: a description of work, e.g. a file on disk.
ProcessAn OS-managed instance of a program: an address space plus resources. Its threads may be running, ready or blocked.
Software threadOne sequence of execution: its own PC, register values, stack and continuation.
Hardware contextThe live architectural state the processor provides to run one instruction stream.
Physical coreThe hardware that executes instructions. It provides one hardware context, or several with SMT.
Two threads in one process have separate stacks, but the hardware doesn't protect one thread's stack from the other. They share the address space, so a thread with a valid pointer into another's stack can read or write it. Separate stacks are a software discipline, not isolation.

Try it: two threads, one hardware context

Thread A counts up by 2; thread B counts down by 1. Both use architectural register x5. Run one instruction of A, switch to B, run one of B, then switch back. The hardware context has room for only one live x5, so watch A's value leave for its saved context and return later. The same goes for its PC: A must resume at the instruction it was about to execute.

The switch button summarizes the kernel's work: handling the interrupt, choosing a thread, and saving and restoring the state needed to resume execution. Both threads here share one process, so switching between them leaves its address space unchanged. A switch between processes may select a different address space; it does not copy the process's memory.

Saved contexts in OS memory

Watch x5 and PC. Switch back to A and its saved PC and x5 become live again. A resumes at its next instruction, with neither an instruction skipped nor one repeated.

In xv6, trap entry saves user registers in a trapframe, while a separate scheduler routine saves the kernel state needed to resume the thread. Floating-point and vector state may need separate handling. The exact saved state depends on the OS and on what the thread has used.

EventWhy control movesDoes the running thread change?
Function call / returnRun another routine, then come backNo
Trap: syscall, exception, interruptRequest a service or handle an event: control goes to a configured trap handler (a trap from user code enters a more privileged mode)Not necessarily: often returns to the same thread
Context switchThe scheduler gives the hardware context to a different threadYes

The first table tells us whether control returns to the same thread. The next tells us what state must survive each kind of transfer:

EventWhat gets savedWhy that set
Function callThe caller saves caller-saved registers (t0–t6, a0–a7) whose values it needs after the call. The callee restores any callee-saved registers (s0–s11, sp) it modifies. A function that will overwrite its incoming ra (by making its own call) must preserve it to return.Both sides agreed on the convention in advance, so each saves only what it's responsible for
Interrupt handlerThe relevant PC, cause and status in special CSRs (by hardware), plus ordinary registers the handler must preserve (by software)The interrupted code agreed to nothing, so a transparent handler must restore what it disturbed
Thread switchThe outgoing thread's resumable architectural state, including PC, ordinary registers, sp and any other state it uses, in OS-managed storageAnother thread will use the hardware context; this one must later resume with its own values and memory mapping
PredictTrace addresses, not intentions.
  1. Right after the first jal ra, double at 0x88 enters double, what are ra, sp and the word at 0x1FC? Why do we need two different return addresses?
    Show answer

    ra = 0x8C, sp = 0x1F0, M[0x1FC] = 0x48. double must return to 0x8C inside f, and later f must return to 0x48 inside main. One ra can hold only one of them, so the outer one waits in memory.

  2. At the end of the correct run, what are a0, PC and sp? Is the saved 0x48 gone from 0x1FC?
    Show answer

    a0 = 21, PC = 0x48, sp = 0x200. The value 0x48 is still at 0x1FC: releasing the frame moved sp and erased nothing.

  3. With the save and restore removed, where does f's final jalr jump, and why?
    Show answer

    To 0x90. The second jal ra, double (at 0x8C) set ra = 0x90, and nothing restored main's 0x48. At 0x90 it adds 1 again, bumps sp by another 16, returns to 0x90 again, and loops forever.

  4. Does every interrupt cause a thread switch? How can the same thread resume?
    Show answer

    No. The handler saves the registers it will use, handles the event (say, a key press), restores them, and returns to the saved PC with a trap-return instruction. The same thread continues, unaware anything happened. (That's for an interrupt; a system call instead resumes after the ecall, with its result.) A switch occurs only if the OS decides another thread should run. Try "Interrupt, same thread resumes" in the explorer.

  5. In the two-thread explorer: run one instruction of A, switch, run one of B, switch back. What are the live PC and x5, and what does A execute next?
    Show answer

    After A's addi, A has x5 = 2 and PC = 0x44. After the switch, B runs one addi (x5 50 → 49). Switching back restores PC = 0x44, x5 = 2, so A executes its jal x0, loop_A next. Its pending instruction is neither skipped nor replaced.

  6. In thread A's listing, jal x0, loop_A sits at 0x44 and loop_A is at 0x40. What target does it jump to, and what displacement does the instruction actually encode? Why do assembly listings use labels rather than a bare number?
    Show answer

    Target 0x40; encoded displacement 0x40 − 0x44 = −4 bytes, because jal is PC-relative. The instruction never contains the address 0x40, so the same bytes placed elsewhere would jump somewhere else. A bare number is ambiguous (target or offset?), and assemblers differ in how they read it; a label lets the assembler compute the correct displacement.

  7. Two threads in one process have different sp values. Does that mean two cores, two protected address spaces, or two isolated stacks?
    Show answer

    None of these. They may share one core by taking turns, they share one address space, and their stacks are just different regions of it that either thread could access. What's truly distinct is each thread's register state and continuation.

  8. Transfer. A caller needs the value in t0 after a call. What obligation does it have?
    Show answer

    t0 is caller-saved, so the callee may overwrite it. The caller must save it before the call (or keep the value in a callee-saved register instead) and restore it afterwards.

  9. Transfer. A leaf function needs a local array. Can it require a stack frame?
    Show answer

    Yes. "Leaf" only means it makes no calls, so it needn't save ra. The array still needs memory, which comes from reserving a frame on the stack.

  10. Transfer. An interrupt handler returns without invoking the scheduler. Has a thread switch occurred?
    Show answer

    No. Control passed to the handler and came back to the same thread. A trap is a control transfer; a thread switch happens only if the OS chooses to run a different thread before returning.

  11. Transfer. A system-call handler leaves the exception PC pointing at the ecall. Where does trap return resume?
    Show answer

    At the ecall itself, which would make the same system call again, over and over. Trap return uses the saved PC as-is, so the handler must add 4.

  12. Transfer. Two CPUs implement the same ISA but have different internal registers. Must the OS save those registers when switching threads?
    Show answer

    No. The OS saves architectural state, which the ISA defines and both CPUs share. Internal registers are microarchitectural: the hardware finishes or discards their contents at instruction boundaries, so they carry nothing a thread needs to resume.

Draw a function return and a thread resumption side by side as two different uses of saved state. For each, say what lives in registers, what lives in memory, and which software convention gives the saved bits their meaning.

Unit 2 · Summary

Putting it together

LessonMechanismState it addsWho defines itLimitation that remains
5Register file, load/store ISA32 architectural registersISAMore simultaneously live values than available registers can require spills.
6ISA vs. microarchitecture; multicycleInternal registers (not named by instructions)Hardware designerStill one instruction at a time
7Calls, stacks, traps, context switchesABI roles for x1/x2 (ra/sp), frames, saved contexts, privilegeABI and OS conventions + ISA trap rulesSwitching costs time

Trace any instruction through three layers. The ISA determines its visible effect on registers, memory and PC. The microarchitecture determines the internal steps and time needed to produce that effect. The ABI and OS decide how programs use and preserve the visible state: a return address brings a function back to its caller, while a saved context brings a paused thread back to its next instruction.

Try explaining why two cores can run the same program at different speeds, and why two threads taking turns on one core do not execute simultaneously. The first question leads into Unit 3: once a single instruction's work is understood, a core can start overlapping work from different instructions.

Sources and model assumptions. The explorers on this page are small teaching models, not RISC-V or operating-system emulators. They implement a handful of RV32I instructions, use aligned little-endian 32-bit words, show each instruction only once it has completed, and summarize operating-system work (trap entry, scheduling, saving and restoring) as single steps. The real-architecture facts come from the RV32I base ISA specification (instruction formats, PC-relative jal, loads and stores, endianness), the RISC-V calling convention (ra, sp, argument and saved registers), and the RISC-V privileged specification (trap entry, saved exception PC and cause, trap return). For a concrete operating system's saved state, see xv6's process and context definitions, which distinguish RUNNABLE from RUNNING. Links checked September 2026.