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.
Keep working values in registers
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.
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.
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:
| Feature | NIB-8 | RV32I |
|---|---|---|
| Working registers | 1 (ACC) | 32, named x0–x31. x0 always reads 0 and ignores writes. |
| Register width | 8 bits | 32 bits |
| An address names… | one 8-bit word | one byte. A 32-bit word takes 4 consecutive addresses. |
| Instruction size | 1 byte | 4 bytes (so sequential fall-through advances PC by 4; a taken branch or jump sets another PC) |
| Arithmetic operands | ACC and one memory word | register 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:
| Instruction | Meaning |
|---|---|
| add rd, rs1, rs2 | rd ← rs1 + rs2 |
| sub rd, rs1, rs2 | rd ← rs1 − rs2 |
| addi rd, rs1, imm | rd ← 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, label | jump to label if rs1 = rs2; otherwise continue at the next instruction (PC + 4) |
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.
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.
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 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.
- 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 x30and theswstill have to run before y is written. - What happens to x0 after
addi x0, x0, 9? What doesaddi x5, x0, 9put 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.
- 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.
- 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.
- 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.
One contract, different hardware
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.
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.
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.
| Instruction | Phases used | Cycles | Why it skips some |
|---|---|---|---|
| lw | F D E M W | 5 | Uses everything: address in E, read in M, write register in W |
| add / sub / addi | F D E W | 4 | No data-memory access |
| sw | F D E M | 4 | No register to write |
| beq | F D E | 3 | E 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Calls, stacks & saved contexts
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.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.
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:
| Name | Register | Role by convention | Preserved across a call? |
|---|---|---|---|
| ra | x1 | return address | No: each call overwrites it |
| sp | x2 | stack pointer | Yes: restored before returning |
| a0–a7 | x10–x17 | arguments; a0 (and a1) also carry return values | No |
| t0–t6 | x5–7, x28–31 | temporaries: the caller saves them if it still needs them | No |
| s0–s11 | x8–9, x18–27 | saved: the callee must restore them if it modifies them | Yes |
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.
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.
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.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
ecallinstruction.
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.
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.
| Term | What it is in our model |
|---|---|
| Program | Code and data definitions: a description of work, e.g. a file on disk. |
| Process | An OS-managed instance of a program: an address space plus resources. Its threads may be running, ready or blocked. |
| Software thread | One sequence of execution: its own PC, register values, stack and continuation. |
| Hardware context | The live architectural state the processor provides to run one instruction stream. |
| Physical core | The hardware that executes instructions. It provides one hardware context, or several with SMT. |
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.
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.
| Event | Why control moves | Does the running thread change? |
|---|---|---|
| Function call / return | Run another routine, then come back | No |
| Trap: syscall, exception, interrupt | Request 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 switch | The scheduler gives the hardware context to a different thread | Yes |
The first table tells us whether control returns to the same thread. The next tells us what state must survive each kind of transfer:
| Event | What gets saved | Why that set |
|---|---|---|
| Function call | The 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 handler | The 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 switch | The outgoing thread's resumable architectural state, including PC, ordinary registers, sp and any other state it uses, in OS-managed storage | Another thread will use the hardware context; this one must later resume with its own values and memory mapping |
- Right after the first
jal ra, doubleat 0x88 entersdouble, 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.
doublemust return to 0x8C insidef, and laterfmust return to 0x48 insidemain. One ra can hold only one of them, so the outer one waits in memory. - 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.
- With the save and restore removed, where does f's final
jalrjump, 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. - 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. - 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 oneaddi(x5 50 → 49). Switching back restores PC = 0x44, x5 = 2, so A executes itsjal x0, loop_Anext. Its pending instruction is neither skipped nor replaced. - In thread A's listing,
jal x0, loop_Asits at 0x44 andloop_Ais 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
jalis 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. - 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.
- Transfer. A caller needs the value in
t0after a call. What obligation does it have?Show answer
t0is 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. - 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. - 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.
- 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.
- 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.
Putting it together
| Lesson | Mechanism | State it adds | Who defines it | Limitation that remains |
|---|---|---|---|---|
| 5 | Register file, load/store ISA | 32 architectural registers | ISA | More simultaneously live values than available registers can require spills. |
| 6 | ISA vs. microarchitecture; multicycle | Internal registers (not named by instructions) | Hardware designer | Still one instruction at a time |
| 7 | Calls, stacks, traps, context switches | ABI roles for x1/x2 (ra/sp), frames, saved contexts, privilege | ABI and OS conventions + ISA trap rules | Switching 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.
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.