4.1 CPU, ISA, and RISC-V
Logic gates and registers combine into a CPU that fetches, decodes, and executes instructions. Whether a Java program runs through interpretation or just-in-time compilation, machine instructions and the runtime ultimately implement its behavior.
Digital logic provides gates, registers, and a clock. This chapter first reads the ISA (a visible contract between software and hardware) before the next lesson applies it to a teaching-oriented single-cycle data path.
Separate the ISA, Microarchitecture, and ABI
The ability to add values is not enough to run a program. Software must know which registers exist, how instructions are encoded, how memory access behaves, and what state exceptions preserve. The instruction set architecture (ISA) defines these observable rules.
Three layers are often conflated:
- ISA defines the semantics of machine instructions and the software-visible state, such as RISC-V, x86-64, and AArch64;
- Microarchitecture specifies how a particular processor implements the ISA, including pipeline depth, cache design, out-of-order execution windows, and the number of execution units;
- ABI establishes how programs cooperate with each other, including which registers hold parameters, who is responsible for preserving registers, stack alignment, and object-file formats.
The same ISA can be implemented by completely different microarchitectures. Even identical machine instructions may exhibit different execution latencies across two CPUs, provided the observable results conform to the contract. Software should not depend on internal implementation details. The ABI sits atop the ISA: a processor recognizes add and register x10, but using a0 (x10) for the first ordinary integer argument is a calling-convention rule, unrelated to the C parameter’s name.
A C Statement Does Not Correspond to Fixed Assembly
Targeting RV32I and common RISC-V psABI, an optimized simple function might look like this:
int add(int left, int right) {
return left + right;
}add:
add a0, a0, a1
reta0 and a1 receive the first two integer arguments, while a0 also holds the return value. ret is a pseudo-instruction that assemblers typically expand into jalr x0, 0(ra).
Several conditions must be preserved:
- If the target is RV64, the C
intis still typically 32 bits; compilers use instructions likeaddwto maintain language semantics; - Debug builds, instrumentation, different optimization levels, or calling conventions can produce distinct assembly outputs;
- C signed overflow is undefined behavior. Compilers may optimize on that basis; machine-level wrapping does not make overflowing C addition defined;
- If a function requires a stack frame, the standard ILP32/LP64 calling convention keeps the stack pointer 16-byte aligned throughout procedure execution. Subtracting 8 bytes is not a compliant prologue; E-family ABIs have separate rules.
A single C statement may vanish, be expanded into multiple instructions, or be relocated to a position different from its source location. Compilers preserve observable language behavior, not a literal line-by-line translation ritual.
Java class files contain JVM bytecode. An interpreter executes machine code implementing bytecode operations; a JIT can compile hot bytecode into target machine code; AOT tools may take alternative paths. CPython also maintains its own bytecode and interpretation loop. They are not "without instructions", rather, the source code passes through an additional runtime abstraction layer before reaching machine execution.
RISC-V Integer Registers
The RV32I/RV64I base integer ISAs discussed here expose 32 integer registers x0 to x31. Register x0 reads as zero and writes are discarded; all other registers are XLEN bits wide: 32 bits in RV32 and 64 bits in RV64.
The embedded RV32E/RV64E subsets have only 16 integer registers, so this table does not apply unchanged to them.
The ABI assigns human-readable aliases to these registers:
| ABI Name | Register | Common Use |
|---|---|---|
zero | x0 | Constant zero |
ra | x1 | Return address |
sp | x2 | Stack pointer |
a0–a7 | x10–x17 | Arguments; the first two also hold return values |
t0–t6 | Several x registers | Caller-saved temporary values |
s0–s11 | Several x registers | Callee-saved preserved values |
"Caller-saved" does not mean the register is destroyed after a function call; it means that if the caller needs to preserve its value, it must do so explicitly. "Callee-saved" means the called function must restore the value after use. Functions can entirely avoid using the stack.
The hardware implementation of the register file is not part of the ISA specification. Smaller cores may use flip-flop arrays or SRAM-style structures, while high-performance cores may implement physical register renaming. Software only sees architectural registers and their specified behavior.
How Basic Instructions Are Encoded
The RV32I base instruction is 32 bits, with the low 7 bits opcode determining the primary category. For example: add x1, x2, x3
31 25 24 20 19 15 14 12 11 7 6 0
+----------+--------+--------+------+--------+---------+
| funct7 | rs2 | rs1 |funct3| rd | opcode |
| 0000000 | 00011 | 00010 | 000 | 00001 | 0110011 |
+----------+--------+--------+------+--------+---------+
Machine code: 0x003100B3The fields mean:
opcodeselects the OP category;rd=x1is the destination register;rs1=x2andrs2=x3are two source registers;funct3andfunct7distinguish ADD, SUB, and other operations under the same opcode.
You can directly verify the bit-field encoding with Python:
funct7 = 0b0000000
rs2 = 3
rs1 = 2
funct3 = 0b000
rd = 1
opcode = 0b0110011
instruction = (
(funct7 << 25)
| (rs2 << 20)
| (rs1 << 15)
| (funct3 << 12)
| (rd << 7)
| opcode
)
assert instruction == 0x003100B3RISC-V's 32-bit instruction set is commonly categorized into six layouts: R, I, S, B, U, and J. These layouts maintain rs1, rs2, and rd fields in consistent positions, while immediate values are distributed across the remaining bits. The arrangement of B/J immediate values may appear scattered, but this is designed to reuse fields and hardware connections; after decoding, the values must be realigned, sign-extended, and the least significant bit is implicitly zeroed.
“All RISC-V instructions are exactly 32 bits” is incorrect. Base instructions are typically 32 bits; the C extension provides 16-bit compressed instructions, and the encoding space reserves rules for longer instructions as well. When the C extension is supported, instruction addresses are usually only required to be 2-byte aligned, and the next sequential instruction address does not necessarily have to be PC + 4.
Six Layouts for Different Data Flow Patterns
| Layout | Representative Instructions | Primary Operands |
|---|---|---|
| R | add rd, rs1, rs2 | Two register sources, one register destination |
| I | addi, lw, jalr | One register source, an immediate value, often with a destination |
| S | sw rs2, imm(rs1) | Base register, data to store, offset; no rd |
| B | beq rs1, rs2, label | Two comparison sources and a PC-relative offset |
| U | lui, auipc | High-bit immediate and destination |
| J | jal rd, label | PC-relative offset and link target |
Not all extended instructions adhere to the rule of "at most two sources and one destination." For example, floating-point fused multiply-add uses an additional source register and employs R4-type encoding. These six layouts serve as the entry point for understanding fundamental integer instruction encoding in RISC-V; they are not the upper limit of the RISC-V ecosystem.
The semantics of several RV32I instructions can be expressed as:
add t0, t1, t2 # t0 = t1 + t2
addi t0, t1, -16 # Add a sign-extended 12-bit immediate
lw t0, 8(sp) # Read 32 bits from address sp + 8
sw t0, 12(sp) # Write 32 bits to address sp + 12
beq t0, t1, equal # Branch to the PC-relative target if equal
jal ra, function # ra = return address; jump to target
jalr x0, 0(ra) # Return via ra; discard the new link addressRISC-V is a load/store architecture: integer arithmetic instructions typically operate on registers, with memory data loaded first, processed, and then stored as needed. Memory access must also respect alignment, permissions, address translation, memory models, and extension rules. These comments describe successful execution. jalr adds its sign-extended offset to the base, clears bit zero, and checks instruction alignment; the return example assumes a valid return address in ra.
Instruction Semantics Do Not Require Five Fixed Steps
“Fetch, decode, execute, memory access, write-back” is a useful decomposition of data flow, but it does not mean that every instruction must progress through these five stages sequentially in five clock cycles as required by the ISA.
- A single-cycle teaching CPU completes an entire instruction within one long cycle;
- A multi-cycle CPU can reuse hardware, allowing different instructions to go through different stages;
- A five-stage pipeline overlaps the stages of multiple instructions, enabling parallelism;
- High-performance processors may translate instructions into micro-operations, execute them out of order, and then commit them according to architectural rules.
Even the "memory access stage" is not required for every instruction. Treat these five names as a perspective for analyzing processor components, not as a mandatory sequence of clock phases present in all CPUs.
Typical architectural state includes integer registers, the program counter (PC), privilege state, and visible memory effects. Microarchitectural elements such as pipeline registers, branch predictors, and register renaming tables are not part of the standard program-visible ISA state.
RISC and CISC Are Not Processor Rankings
RISC architectures favor regular encoding, load/store organization, and fewer addressing modes; x86 retains variable-length encoding, rich addressing capabilities, and long-term backward compatibility. Modern implementations have blurred the early boundaries between these categories: RISC-V can support complex extensions and out-of-order cores, while x86 can translate intricate instructions into internal operations and employ extensive hardware-level optimizations.
Therefore, the following assumptions are unreliable:
- Fewer instructions necessarily mean better performance;
- Simpler decoding necessarily results in a smaller chip footprint;
- Internal micro-operations resembling RISC imply that x86 is merely a "shell" around a RISC design;
- Fixed-length encoding inevitably leads to smaller programs.
Performance depends on workload characteristics, compiler quality, memory subsystems, implementation width, clock frequency, and power constraints. The instruction set architecture shapes the design space, but it does not alone determine the final outcome.
rep movsb and Explicit Loops
On x86-64, rep movsb operates by counting in RCX, copying one byte from [RSI] to [RDI]; the direction of pointer advancement is determined by the direction flag. Modern implementations may opt for a dedicated hardware path, but the instruction set architecture (ISA) only specifies observable semantics.
This RV64I loop fragment assumes valid, nonoverlapping source and destination ranges and an unsigned XLEN-bit count. It changes a0, a1, and a2, has no function prologue or return, and does not preserve the original destination pointer required as memcpy’s return value. The zero-length check avoids an initial access to an empty range:
# a0 = destination, a1 = source, a2 = count
beq a2, zero, done
loop:
lbu t0, 0(a1)
sb t0, 0(a0)
addi a1, a1, 1
addi a0, a0, 1
addi a2, a2, -1
bne a2, zero, loop
done:Executing more instructions than a single rep movsb does not necessarily make the loop slower; conversely, the presence of a dedicated instruction does not automatically imply superiority. Cache hits, length, alignment, microarchitectural optimizations, and library implementations all influence the outcome. A real memcpy may select strategies by vector width, length, and CPU, but its source and destination must not overlap. Use memmove when overlap is allowed; this forward-only loop does not handle every overlap direction.
Function Calls Follow the ABI
A single jal ra, function merely performs a jump and saves the return address. A complete call requires:
- The caller places parameters in the agreed-upon locations;
- If necessary, saves caller-saved registers;
- The callee establishes an aligned stack frame for local storage and register preservation;
- The return value is written to agreed-upon locations such as
a0ora1; - The callee restores callee-saved registers and returns via
ra.
Recursion is not specially supported by JAL. Each layer of a recursive call can simply save the remaining return address and local state into a separate stack frame, enabling a call to itself. Leaf functions, which do not call other functions, often leave ra in a register without pushing it onto the stack.
Hands-on: Reading an ISA Contract
- Compute the R-type field of
sub x5, x6, x7and explain how it distinguishes it from an ADD instruction usingfunct7. - Explain how
x0enablesaddi rd, x0, immto function as a load for a small immediate value. - Write an RV32I assembly implementation of
max(int, int), specifying whether it uses signed or unsigned branching. - Determine what happens if the first
beqis omitted from the above copy loop when count equals zero. - Differentiate between ISA, microarchitecture, and ABI: stack alignment, L1 cache size, and the overflow behavior of
addeach belong to which layer. - Explain why the rule "PC always increments by 4" no longer holds after enabling the compressed extension.
Wire the Instruction Contract into a Data Path
By now, we understand what architectural effects each machine instruction should produce, but we still haven't decided how to wire together the registers, ALU, and memory ports. In the next lesson, we'll trace add, lw, sw, and beq through a deliberately simplified single-cycle RV32I data path, and explain where the teaching model falls short in representing modern CPUs.