Part II · Chapter 7
Instructions: From Tiny-8 to x86-64 and RISC-V
Tiny-8 taught us how a processor works. Now we meet the real thing. Real instruction sets answer the same questions Tiny-8 did (which registers, which operations, how to point at memory, how to pack it all into bits), but they answer them in very different ways.
The list of instructions a processor understands, together with its registers and the rules for encoding them, is called its instruction set architecture, or ISA. The ISA is a contract between hardware and software. Any chip that honors the contract can run the same programs. Intel and AMD build very different chips, but both implement x86-64, so the same Windows or Linux runs on either. Dozens of companies make RISC-V chips, and the same RISC-V program runs on all of them.
In this chapter we'll compare our two real ISAs on four questions: registers, operands, encoding, and the overall philosophy behind them.
Registers: more, and wider
Tiny-8 had one register. Real programs juggle many values at once, and registers are by far the fastest place to keep them, so real ISAs have plenty:
- x86-64 has 16 general-purpose 64-bit registers:
rax,rbx,rcx,rdx,rsi,rdi,rbp,rsp, plusr8tor15. The first eight have quirky names inherited from the 8088, where each had a special job:axwas the accumulator,cxthe counter for loops,spthe stack pointer. - RISC-V has 32 registers,
x0tox31, all equal in hardware.x0is always zero. The others have nicknames that describe how programs agree to use them:spfor the stack pointer,a0–a7for function arguments,t0–t6for temporary values, and so on.
x86 carries 45 years of history in its registers. When the 8086 went to 32 bits, ax grew into eax ("extended"), and when x86 went to 64 bits it grew again into rax. But the old names never went away: they're still there, as smaller pieces of the big register.
mov eax: writing the 32-bit part clears the upper half, while writing ax, al or ah leaves the other bytes alone.That odd rule for 32-bit writes was a deliberate choice by the designers of x86-64. If a write only changed part of a register, the processor would have to merge the new bits with the old ones, which forces it to wait for the old value. Clearing the top half makes the result independent of whatever was there before, so modern processors can run such instructions more quickly. The 16- and 8-bit registers kept their old behavior, for compatibility.
Where operands come from
An instruction's operands can come from three places, in both ISAs:
- A register:
add rax, rbx. - An immediate, a number written inside the instruction itself:
add rax, 1. It's called "immediate" because it's available immediately, with no need to fetch it from anywhere. Tiny-8'sLDI 5was one. - Memory, at some address. The rules for describing that address are called addressing modes, and this is where the two ISAs part ways.
x86 lets you build quite a fancy address inside a single instruction: [base + index × scale + displacement], where base and index are registers and scale is 1, 2, 4 or 8. That's tailor-made for arrays: [rbx + rcx*8] means "element number rcx of an array of 8-byte numbers starting at rbx". And x86 lets most instructions use memory directly. add rax, [rbx + rcx*8] reads memory and adds, all in one instruction.
RISC-V allows exactly one addressing mode: a register plus a small offset, written 8(sp). And only two kinds of instruction may touch memory at all: loads (lw, ld…) copy from memory into a register, and stores (sw, sd…) copy from a register into memory. Everything else, including add, works only on registers. This design is called a load/store architecture. (Tiny-8 was neither: its ADD could read memory, but only by address.)
Encoding RISC-V: neat boxes
Every RISC-V instruction is exactly 32 bits, divided into fixed fields, just like Tiny-8's two hex digits but with more of them. There are only six layouts, called formats. The opcode always sits in the lowest 7 bits, and when an instruction names a register, it's always in the same place: the destination rd in bits 7–11, the sources rs1 and rs2 in bits 15–19 and 20–24. Each register field is 5 bits, which is exactly enough to number 32 registers.
lw and sw, notice that the number moves to a different place, and try a number that's too big, like 5000.Because the number has to fit into a 12-bit field, addi can only add numbers from −2048 to 2047. A bigger number takes two instructions: lui ("load upper immediate") sets the top 20 bits, and addi fills in the rest. Assemblers do this for you when you write li a0, 5000.
The branch and jump formats look strange: their number's bits are scattered in an odd order. It's deliberate. It keeps the sign bit of every number in bit 31 and keeps as many bits as possible in the same position across formats, which makes the decoding hardware smaller. RISC-V's designers counted gates.
Encoding x86-64: layers of history
x86 instructions are anything from 1 to 15 bytes long, built from optional pieces in a fixed order:
- Prefixes, which modify the instruction (for example,
LOCK). - A REX byte, added for x86-64. It turns on 64-bit operation and supplies a fourth bit for register numbers, so they can reach
r8–r15. - The opcode: one, two or three bytes.
- A ModRM byte, which says what the operands are: two registers, or a register and memory.
- An SIB byte (scale, index, base), for fancy addresses.
- A displacement, the offset in an address, of 1 or 4 bytes.
- An immediate number, of 1, 2, 4 or even 8 bytes.
add rax, 1 and add r8, 1 differ in just one bit of the REX byte.The variable length makes x86 code compact: common instructions like ret and push take a single byte. But it makes decoding hard. The processor can't even know where the next instruction starts until it has worked out how long this one is. That's part of why modern x86 chips spend so much effort on their front end, and keep a cache of already-decoded instructions.
The same program, twice
Here's a small function in C that adds up an array of numbers:
long sum(long *a, long n) {
long s = 0;
for (long i = 0; i < n; i++)
s += a[i];
return s;
}
And here it is in both assembly languages, much as a compiler would produce it. By agreement, the array's address arrives in the first argument register (rdi on x86-64, a0 on RISC-V), the count in the second, and the answer goes back in rax or a0.
; x86-64 — 9 instructions, 22 bytes sum: xor eax, eax ; s = 0 test rsi, rsi ; is n ≤ 0? jle .done ; then skip the loop xor ecx, ecx ; i = 0 .loop: add rax, [rdi + rcx*8] ; s += a[i] (load AND add in one go) inc rcx ; i++ cmp rcx, rsi ; i < n ? jl .loop .done: ret
# RISC-V — 9 instructions, 36 bytes sum: li a2, 0 # s = 0 blez a1, .done # if n ≤ 0, skip the loop .loop: ld a3, 0(a0) # load a[i] (a separate instruction) add a2, a2, a3 # s += it addi a0, a0, 8 # move the pointer to the next 8-byte number addi a1, a1, -1 # one fewer to go bnez a1, .loop .done: mv a0, a2 # return s ret
Look at the inner loops. x86 needs 4 instructions, thanks to that powerful add with a scaled address. RISC-V needs 5, because loading and adding are separate. On the other hand, every RISC-V instruction is the same simple shape, easy to decode and easy to overlap. The whole x86 function fits in 22 bytes against RISC-V's 36, though RISC-V's optional 16-bit "compressed" instructions would close much of that gap.
CISC and RISC
These two styles have names. In the 1970s, memory was expensive and slow, and many programs were still written by hand in assembly. So designers built complex instruction set computers (CISC): rich, compact instructions that each did a lot of work, implemented with microcode like Tiny-8's. x86 is the most famous survivor.
Around 1980, researchers led by David Patterson at Berkeley and John Hennessy at Stanford noticed that compilers mostly used the simple instructions anyway. They proposed reduced instruction set computers (RISC): fewer, simpler, fixed-length instructions that could be decoded quickly and overlapped in a pipeline. RISC designs went on to power workstations, then game consoles, and today, as ARM, almost every phone, as well as Apple's Mac processors. Hennessy and Patterson shared the 2017 Turing Award, the "Nobel prize" of computing, for this work. RISC-V, started at Berkeley in 2010, is the latest in that line. Its specification is open, so anyone may build a RISC-V chip without paying a licence fee.
Today the line between the two is blurry. As we saw in Chapter 6, modern x86 processors translate their complex instructions into simple RISC-like micro-ops inside. And RISC designs have gained plenty of specialized instructions. The real difference now is mostly in the front door: how instructions are encoded, and how hard they are to decode.
Try it yourself
-
Use the encoder to find the 32-bit word for
addi a0, a0, 1.Show answer
0x00150513. The number 1 sits in the top 12 bits, a0 (register 10) appears in both rs1 and rd, and the opcode is0010011. -
Why can't a single
addiadd 5000? How doesli a0, 5000manage it?Show answer
The immediate field is only 12 bits, so it holds −2048 to 2047. The assembler splits 5000 intolui a0, 1(which puts 1 × 4096 into a0) followed byaddi a0, a0, 904: 4096 + 904 = 5000. (On 64-bit RISC-V, assemblers useaddiw, a close cousin ofaddithat works on 32-bit values, but the idea is the same.) -
add rax, rbxis48 01 D8. What are the bytes foradd rax, rcx?Show answer
48 01 C8. Only the reg field of the ModRM byte changes: rbx is 011 and rcx is 001, so11 011 000(D8) becomes11 001 000(C8). -
How many bytes does "return from a function" take on each ISA?
Show answer
x86-64: 1 byte (C3). RISC-V: 4 bytes.retis shorthand forjalr zero, 0(ra), a normal 32-bit instruction. With the compressed extension it shrinks to 2 bytes. -
After
mov rax, -1and thenmov eax, 1, what's in rax? What if the second instruction weremov al, 1instead?Show answer
Withmov eax, 1, rax = 1: writing the 32-bit register clears the upper half. Withmov al, 1, rax =0xFFFFFFFFFFFFFF01: only the lowest byte changes.
What we learned
- An ISA is the contract between hardware and software: registers, instructions and their encodings.
- x86-64 has 16 registers with nested historical names (
rax⊃eax⊃ax⊃ah,al); RISC-V has 32 equal ones, withx0fixed at zero. - Operands come from registers, immediates or memory. x86 has rich addressing modes; RISC-V is a load/store architecture with a single, simple mode.
- RISC-V instructions are a fixed 32 bits in six tidy formats. x86 instructions are 1–15 bytes of prefixes, REX, opcode, ModRM, SIB, displacement and immediate.
- CISC packs more work into each instruction; RISC keeps instructions simple and easy to pipeline. Inside, modern chips blend both.
That completes Part II: you now know how a processor runs a program, from the transistor to the instruction set. But every program we've seen has lived in a tiny memory that magically answers instantly. In Part III we look at real memory: how billions of bytes are organized and addressed, why memory is so much slower than the processor, and the clever tricks (caches and virtual memory) that hide it.