Anatomy of an Instruction Set
August 3, 2026·30 min read·intermediate
Every program that runs on a processor communicates through a precisely defined contract. The contract specifies which instructions exist, how they are encoded in binary, which registers they can name, what…
Every program that runs on a processor communicates through a precisely defined contract. The contract specifies which instructions exist, how they are encoded in binary, which registers they can name, what happens when something goes wrong, and in what order stores become visible to other cores. This contract is the instruction set architecture, or ISA. It is the single most stable layer of the computing stack introduced in Chapter 1. The x86 ISA has survived since 1978. The ARM A32 encoding has survived since 1985. Programs compiled against those encodings still run on hardware built decades later. The ISA persists because the cost of breaking it exceeds the cost of carrying its legacy forward.
This chapter takes the ISA apart. It examines each element of the contract in turn: the instruction format that encodes an operation into a fixed or variable number of bits, the register file that supplies operands, the addressing modes that compute memory addresses, the exception model that handles faults and interrupts, the memory ordering model that governs multicore visibility, and the calling conventions that let independently compiled functions cooperate. Throughout, the chapter compares the three ISA families that thread through the rest of the book: RISC-V, ARM A64, and x86-64.
01.The ISA as Hardware-Software Contract
An ISA is not a piece of hardware. It is a specification. Two processors that implement the same ISA can have completely different pipeline depths, cache sizes, and execution unit counts. A program cannot tell the difference as long as every instruction produces the architecturally specified result. This is the meaning of the word contract: the ISA defines what the hardware must do, not how it does it.
The contract has two sides. On the software side, the compiler generates a sequence of binary-encoded instructions that obeys the ISA’s rules. On the hardware side, the processor fetches, decodes, and executes those instructions and produces the results the ISA promises. When the two sides agree, any legal program runs correctly on any compliant implementation. When they disagree, the result is a bug, and the question of who is wrong (the compiler or the chip) is answered by reading the ISA specification.
This separation between specification and implementation is the reason a single ISA can span decades of hardware generations. Intel’s Pentium of 1993 and Intel’s Raptor Lake of 2023 implement the same base x86 ISA. The Pentium has a 5-stage pipeline and a 16 KiB cache. Raptor Lake has a pipeline over 20 stages deep, multiple levels of cache totaling tens of megabytes, and an out-of-order execution engine that can have over 500 instructions in flight at once. Raptor Lake still runs the 32-bit binaries compiled for the Pentium three decades earlier because both honor the same contract.
02.Instruction Format and Encoding
Every instruction must be represented as a sequence of bits that the hardware can decode unambiguously. The encoding must answer three questions. First, which operation? Second, where are the operands? Third, where does the result go? The answers are packed into fields within the instruction word. The two major design choices are the width of the instruction word and the layout of those fields.
Fixed-width encoding
A fixed-width encoding uses the same number of bits for every instruction. RISC-V uses 32 bits for all base instructions (the C extension adds 16-bit compressed forms, but those are a separate, optional layer). ARM A64 also uses a uniform 32-bit encoding. Fixed width simplifies the decoder considerably. The hardware knows that every instruction starts exactly 4 bytes after the previous one. Fetching four instructions per cycle means reading a 16-byte aligned block and splitting it into four equal slices. There is no need to scan the byte stream to find instruction boundaries.
The cost of fixed width is encoding pressure. Every instruction, whether it needs one operand or three, whether it carries a 12-bit immediate or none at all, must fit in the same 32 bits. Fields that are unnecessary for a particular instruction type are wasted. RISC-V addresses this by defining six distinct instruction formats (R, I, S, B, U, J), each of which rearranges the 32 bits to suit the instruction class while keeping the opcode and register-specifier fields in consistent positions across formats. Chapter 15 diagrams each format in detail.
Variable-width encoding
A variable-width encoding allows instructions to occupy different numbers of bytes. x86-64 is the canonical example: instructions range from 1 byte (a single-byte NOP) to 15 bytes (a heavily prefixed AVX-512 instruction with a displacement and an immediate). The encoding uses prefix bytes to modify operand size, address size, and vector length, followed by an opcode of one to three bytes, an optional ModR/M byte that encodes the addressing mode, an optional SIB (scale-index-base) byte, an optional displacement of 1, 2, or 4 bytes, and an optional immediate of 1, 2, or 4 bytes (8 bytes for the movabs form of MOV).
The advantage of variable width is density. Simple, common instructions are short. Complex, rare instructions are longer. Code compiled for x86-64 is typically 20 to 30 percent smaller than equivalent RISC-V code because the variable encoding avoids wasting bits on instructions that do not need them.
The cost is decoder complexity. The hardware cannot know where the next instruction starts until it has decoded the current one. Modern x86-64 processors solve this with a dedicated pre-decode stage that scans the byte stream, marks instruction boundaries, and feeds fixed-width internal representations (micro-operations) to the rest of the pipeline. The pre-decode logic consumes meaningful area and power, and it is one of the reasons x86-64 decoders are substantially larger than RISC-V or ARM decoders.
The opcode field
The opcode identifies the operation. In RISC-V, a 7-bit opcode field at bits [6:0] of the 32-bit word selects the major instruction class (load, store, arithmetic, branch, and so on). A secondary funct3 field at bits [14:12] and sometimes a funct7 field at bits [31:25] refine the selection within a class. The three-level hierarchy lets the decoder route instructions quickly: the 7-bit opcode narrows the space to a small family, and the subsidiary fields pick the exact operation.
In ARM A64, a 4-bit field near the top of the word, bits [28:25], identifies the major encoding group, and subsequent bit fields within each group select the specific instruction. The encoding is less regular than RISC-V’s but still fully fixed-width.
In x86-64, the opcode can be 1, 2, or 3 bytes long, preceded by up to four prefix bytes and optionally a REX or VEX or EVEX prefix that extends the register namespace and the vector length. The opcode alone does not uniquely determine the instruction. The ModR/M byte’s reg field sometimes serves as an opcode extension, a practice inherited from the 8086’s need to pack a large instruction set into a single opcode byte.
Immediate encoding
An immediate is a constant encoded within the instruction itself. The width of the immediate field determines the largest constant a single instruction can express. In RISC-V, a standard I-type instruction carries a 12-bit signed immediate, which can represent values from to . The U-type format carries a 20-bit upper immediate, which the LUI instruction places into the upper 20 bits of a register. A two- instruction sequence of LUI followed by ADDI can therefore load any 32-bit constant.
ARM A64 takes a different approach. Many data-processing instructions accept a 12-bit unsigned immediate, optionally shifted left by 12 bits. The MOVZ, MOVK, and MOVN instructions can build an arbitrary 64-bit constant in up to four steps by loading 16 bits at a time. ARM also supports a “bitmask immediate” encoding that represents certain repeating bit patterns in a compact form, covering many constants that would otherwise require multiple instructions.
x86-64 instructions can carry immediates of 8, 16, or 32 bits directly in the instruction stream. A 64-bit immediate is available only for the special MOV to a register (the movabs form). For most instructions, the maximum immediate width is 32 bits, which is sign-extended to 64 bits before use. This means that loading a 64-bit constant that does not fit in 32 sign-extended bits requires either the movabs form or a pair of instructions that load the two halves separately.
03.The Register File
The register file is the fastest storage in the processor. A register read takes a fraction of a nanosecond. A cache read takes several nanoseconds. A main-memory read takes tens of nanoseconds. Every ISA defines a set of architectural registers that instructions can name. The number, width, and conventions governing these registers are among the most consequential ISA decisions.
RISC-V registers
RISC-V defines 32 integer registers, each 32 bits wide in RV32 and 64 bits wide in RV64. Register x0 is hardwired to zero. Reading x0 always returns zero, and writing to x0 is silently discarded. This dedicated zero register simplifies many operations. A move from register x5 to register x6, for instance, is encoded as add x6, x5, x0, which adds zero to the source. No separate MOV instruction is needed.
The program counter is not one of the 32 general-purpose registers. It is architecturally visible only through PC-relative addressing and the AUIPC instruction, which adds an upper immediate to the current PC and stores the result in a general-purpose register.
ARM A64 registers
ARM A64 defines 31 general-purpose registers, X0 through X30, each 64 bits wide. The lower 32 bits of each register are accessible as W0 through W30. Register X30 doubles as the link register (the return address for subroutine calls). Unlike RISC-V, ARM A64 does not have a hardwired zero register in the general-purpose file. Instead, a special encoding that would otherwise name a 31st register is interpreted as either the zero register XZR or the stack pointer SP, depending on the instruction. The program counter is not directly accessible as a general- purpose register.
x86-64 registers
x86-64 defines 16 general-purpose registers: RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, and R8 through R15. Each is 64 bits wide, with sub-register aliases for the lower 32, 16, and 8 bits (EAX, AX, AL, and so on for each). The original 8086 had only 8 registers. AMD’s x86-64 extension doubled the count to 16 by adding a REX prefix byte that supplies one extra bit per register specifier. Intel’s APX extension, ratified in 2024, doubles the count again to 32 by introducing the REX2 prefix.
The smaller register file of legacy x86-64 puts pressure on the compiler’s register allocator. Spilling a value to the stack because no register is free is more common in x86-64 code than in RISC-V or ARM code, and each spill costs a store followed later by a load. The 16 extra APX registers reduce spilling noticeably for register-hungry workloads.
Table 1. Architectural register files compared across three ISA families.
| Feature | RISC-V (RV64I) | ARM A64 | x86-64 |
|---|---|---|---|
| Integer registers | 32 (x0 hardwired zero) | 31 + XZR/SP | 16 (32 with APX) |
| Register width | 64 bits | 64 bits | 64 bits |
| Sub-register aliases | No | W0–W30 (32-bit) | EAX, AX, AL, etc. |
| Zero register | x0 | XZR encoding | None |
| Link register | x1 (by convention) | X30 | None (uses stack) |
| Program counter | Not a GPR | Not a GPR | RIP (not a GPR) |
04.Addressing Modes
An addressing mode is a rule for computing the effective address of a memory operand. The number and complexity of addressing modes is one of the sharpest differences between RISC and CISC instruction sets.
Base plus offset
The most common addressing mode across all three families is base plus offset. The instruction names a base register and an immediate offset. The effective address is the sum of the register’s contents and the sign-extended offset. In RISC-V, lw x5, 8(x10) loads a 32-bit word from address . In ARM A64, LDR X5, [X10, #8] does the same. In x86-64, mov eax, [rbx + 8] does the same.
Register indirect
Register indirect is the special case of base plus offset with a zero offset. RISC-V writes it as lw x5, 0(x10). ARM writes LDR X5, [X10]. The assembler syntax differs, but the hardware behavior is identical: the effective address is the contents of the base register.
PC-relative addressing
PC-relative addressing computes the effective address as the program counter plus a signed offset. All three ISAs use it for branch targets. RISC-V also uses it for data access through the AUIPC + LW two-instruction idiom, which is how position- independent code loads global variables. ARM A64 provides a dedicated ADRP instruction that forms a 4 KiB-aligned PC-relative address, paired with a subsequent load that adds the page offset. x86-64 introduced RIP-relative addressing in the 64-bit extension, making PC-relative data access a single instruction.
Scaled index (x86-64)
x86-64 supports a scaled index mode in which the effective address is , where the scale is 1, 2, 4, or 8. This mode is encoded through the SIB byte. It maps directly to array element access when the element size is a power of two: mov eax, [rbx + rcx*4] loads the rcx-th 4-byte element of an array whose base is in rbx. RISC-V has no single-instruction equivalent and requires a shift followed by an add to compute the same address. ARM A64 offers a shifted-register offset mode (LDR X5, [X10, X11, LSL #2]) that covers most of the same cases in one instruction, though it lacks x86-64’s independent displacement.
Pre-index and post-index (ARM A64)
ARM A64 supports pre-index and post-index modes that update the base register as a side effect of the load or store. In pre-index mode, the base is updated before the access: LDR X5, [X10, #8]! adds 8 to X10, then loads from the new address. In post-index mode, the base is updated after the access: LDR X5, [X10], #8 loads from the original X10, then adds 8 to X10. These modes are useful for walking through arrays and for stack operations. RISC-V deliberately omits them to keep the microarchitecture simpler (a load that also writes back to the base register requires two write ports to the register file in a single cycle).
05.Condition Codes and Predication
When a program must branch based on the result of a comparison, the ISA must provide a mechanism for testing that result. Two fundamentally different approaches exist, and the three ISA families in this book illustrate both.
Condition codes in ARM A64 and x86-64
ARM A64 and x86-64 both use condition codes: a small set of single-bit flags that arithmetic instructions update as a side effect of computing their result. The four standard flags are N (negative), Z (zero), C (carry), and V (overflow). A conditional branch tests a Boolean combination of these flags. For example, B.EQ label in ARM A64 branches if the Z flag is set, which happens when the most recent flag-setting instruction produced a zero result.
In x86-64, nearly every arithmetic and logical instruction updates the flags register (EFLAGS). In ARM A64, most data-processing instructions update the flags only when the S suffix is present (ADDS, SUBS), giving the programmer control over when flags change. This distinction matters for out-of-order execution: when every instruction updates flags, the flag register becomes a bottleneck because later instructions that read flags must wait for earlier instructions that write them. ARM’s selective flag update reduces that pressure.
Comparison and branch in RISC-V
RISC-V takes a different path. It has no condition-code register. Instead, conditional branches compare two registers directly. The instruction beq x5, x6, label branches if registers x5 and x6 are equal. The comparison and the branch happen in the same instruction. There is no separate compare step and no shared flag register. This eliminates the flag-register bottleneck entirely and simplifies the out-of-order rename logic, because there is no implicit output register that every arithmetic instruction writes.
The trade-off is that RISC-V cannot express “branch if the previous addition overflowed” in a single instruction. Detecting overflow requires an explicit sequence: perform the addition, then use a comparison instruction to check whether the result is smaller than one of the operands (for unsigned overflow) or whether the sign of the result is inconsistent with the signs of the operands (for signed overflow). In practice this is a rare code pattern, and the RISC-V designers judged that avoiding the flag register was worth the occasional extra instruction.
06.The Exception Model
Not every instruction completes normally. A division by zero, an access to an unmapped address, a page fault, a timer interrupt, or a debug breakpoint all divert the processor from the normal fetch-decode-execute sequence. The ISA must specify precisely what happens in each case: which register saves the faulting address, where the handler starts, what state the handler can inspect, and how the handler returns to normal execution.
Synchronous exceptions and asynchronous interrupts
An exception can be synchronous or asynchronous. A synchronous exception, also called a trap or fault, is caused by the execution of a specific instruction. Division by zero is synchronous: the exception is tied to the DIV instruction that attempted the division. A page fault is synchronous: the exception is tied to the load or store that accessed the unmapped page.
An interrupt is asynchronous. It is caused by an external event (a timer, a disk controller, a network interface) that has nothing to do with the currently executing instruction. The processor checks for pending interrupts between instructions and diverts to the handler when one is found.
Exception handling in RISC-V
RISC-V defines a set of control and status registers (CSRs) that manage exceptions. When an exception occurs in machine mode, the processor writes the address of the faulting or interrupted instruction to mepc, writes a cause code to mcause, and jumps to the address stored in mtvec. The handler inspects mcause to determine the exception type, handles it, and executes MRET to return to the interrupted instruction. The privileged specification defines analogous registers for supervisor mode (sepc, scause, stvec) and, optionally, for hypervisor mode.
Exception handling in ARM A64
ARM A64 uses four exception levels, EL0 through EL3, with EL0 being the least privileged (user applications) and EL3 being the most privileged (secure monitor firmware). When an exception occurs, the processor saves the return address in ELR_ELn (Exception Link Register) and the saved processor state in SPSR_ELn (Saved Program Status Register), then jumps to the vector table base address stored in VBAR_ELn plus an offset that depends on the exception type and the originating exception level. The handler executes ERET to return.
Exception handling in x86-64
x86-64 uses a different mechanism rooted in the 8086’s interrupt descriptor table. When an exception or interrupt fires, the processor looks up the handler address in the Interrupt Descriptor Table (IDT), pushes the return address and flags onto the kernel stack, and jumps to the handler. The IRET instruction pops the saved state and returns. Protection levels (rings 0 through 3, though modern operating systems use only ring 0 for the kernel and ring 3 for user space) govern which code can execute which instructions and access which memory regions.
Table 2. Exception model comparison across three ISA families.
| Feature | RISC-V | ARM A64 | x86-64 |
|---|---|---|---|
| Privilege levels | M, S, U (+ optional H) | EL0–EL3 | Ring 0–3 (2 used) |
| Saved PC register | mepc / sepc | ELR_ELn | Pushed to stack |
| Cause register | mcause / scause | ESR_ELn | Error code on stack |
| Vector table | mtvec / stvec | VBAR_ELn | IDT |
| Return instruction | MRET / SRET | ERET | IRET |
07.Memory Ordering
When a program runs on a single core, loads and stores appear to execute in the order the program specifies. When two or more cores share memory, the question of ordering becomes subtle. If core A writes a value and then writes a flag, does core B necessarily see the value before the flag? The ISA’s memory ordering model answers this question.
Sequential consistency
The strongest model is sequential consistency, defined by Leslie Lamport in 1979. Under sequential consistency, the result of any execution is the same as if the operations of all cores were interleaved in some total order, and the operations of each core appear in the order specified by the program. This is the model that matches the programmer’s natural intuition. It is also the most expensive to implement, because the hardware cannot reorder any memory access past any other.
Total store ordering (TSO)
x86-64 implements total store ordering (TSO). Under TSO, each core sees its own stores in program order, and stores from all cores appear in a single total order that every core agrees on. Loads, however, may be reordered ahead of earlier stores to different addresses. The practical consequence is that a store-then-load sequence to different addresses may appear reordered to another core. The MFENCE instruction or a locked instruction (LOCK XCHG, LOCK CMPXCHG) enforces sequential ordering when the program needs it.
TSO is close enough to sequential consistency that most programmers never notice the difference. The one case that matters is the classic store buffer forwarding pattern, where core A writes x = 1 then reads y, and core B writes y = 1 then reads x. Under TSO, both cores can read zero, because each core’s load can bypass its own pending store. Under sequential consistency, at least one core must see the other’s store.
Relaxed ordering
ARM A64 and RISC-V both specify a relaxed base memory model in which the hardware is free to reorder loads and stores in almost any way, as long as single-core program order is maintained from that core’s own perspective. The programmer must insert explicit fence instructions (FENCE in RISC-V, DMB in ARM A64) to enforce ordering between specific pairs of accesses. This places a greater burden on the programmer and the compiler but gives the hardware more freedom to reorder memory operations for performance.
RISC-V also provides acquire and release annotations on atomic instructions (the A extension). A load-acquire prevents subsequent loads and stores from moving before it. A store-release prevents earlier loads and stores from moving after it. Together, acquire and release form a one-directional fence that is cheaper than a full bidirectional fence.
08.Calling Conventions
The ISA defines the registers. The calling convention defines how independently compiled functions use them. Without a shared convention, a caller cannot know which registers the callee will preserve and which it will overwrite. The convention is not part of the ISA specification itself. It is part of the application binary interface (ABI), which sits one layer above the ISA and is enforced by the compiler and the operating system.
Caller-saved and callee-saved registers
The convention divides registers into two classes. Caller-saved (also called volatile) registers are not preserved across a function call. If the caller needs the value after the call returns, it must save the register to the stack before the call and restore it afterward. Callee-saved (also called non-volatile) registers are preserved: the callee must save them at function entry and restore them at function exit if it intends to use them.
In RISC-V, registers x5–x7 and x10–x17 (the t and a registers in the standard ABI naming) are caller-saved. Registers x8–x9 and x18–x27 (the s registers) are callee-saved. In ARM A64, X0–X18 are caller-saved (with X18 reserved on some platforms), and X19–X28 are callee-saved. In x86-64 under the System V ABI, RBX, RBP, and R12–R15 are callee-saved, and the remaining registers are caller-saved.
Argument passing
All three conventions pass the first several arguments in registers. RISC-V uses a0–a7 (up to 8 arguments). ARM A64 uses X0–X7 (up to 8). x86-64 under the System V ABI uses RDI, RSI, RDX, RCX, R8, and R9 (up to 6 integer arguments). Arguments beyond the register limit are passed on the stack. The return value goes in a0 (RISC-V), X0 (ARM), or RAX (x86-64).
The stack frame
When a function needs local storage beyond what registers provide, it allocates space on the stack. The stack grows downward in all three ISAs. The calling convention specifies the alignment of the stack pointer (16 bytes in all three) and the layout of the saved registers, the return address, and the local variables within the stack frame. A consistent layout makes it possible for debuggers, profilers, and exception unwinders to walk the chain of stack frames and reconstruct the call sequence at any point during execution.
09.Putting It Together: The ISA Checklist
Every ISA, together with the ABI built on top of it, must answer the same set of questions. The answers differ across RISC-V, ARM A64, and x86-64, but the questions are universal.
-
Instruction encoding. Fixed or variable width? How many bits? How is the opcode distinguished from operands?
-
Register file. How many registers? How wide? Is there a hardwired zero register? A dedicated link register?
-
Addressing modes. Which modes are supported? Base plus offset? Scaled index? Pre/post-index?
-
Data types. Which integer and floating-point widths does the hardware operate on natively?
-
Control flow. Condition codes or direct comparison? How are branches, jumps, and calls encoded?
-
Exception model. How are faults and interrupts vectored? What state is saved? How does the handler return?
-
Memory ordering. Sequential consistency, TSO, or relaxed? What fence instructions are available?
-
Calling convention (ABI layer). How are arguments passed? Which registers are preserved? How does the stack grow?
The next four chapters examine these ISAs through this lens. Chapter 14 places the ISAs in historical context by tracing the RISC-versus-CISC debate. Chapter 15 dissects RISC-V in detail. Chapter 16 does the same for ARM A64. Chapter 17 does the same for x86-64.
10.Worked Examples
11.Exercises
References
- [1]Waterman, Andrew and Asanovi\'c (2024). “The RISC-V.”
- [2]Waterman, Andrew and Asanovi\'c (2024). “The RISC-V.”
- [3](2024). “ARM.”
- [4](2024). “Intel.”
- [5](2024). “AMD64.”