02 · Reading a Listing¶
Chapter header
| Target | xv6 and host — the same function compiled for each, and nothing executed |
| Prerequisites | ch00 |
| What it measures | What the compiler made of one small function on each architecture: bench/results/shapes-riscv64.json, bench/results/shapes-aarch64.json |
The question¶
What is the compiler telling me, and what is it not?
Almost every chapter from here on puts a listing in front of you — the instructions a compiler produced from source you can read beside it. Three of them are in the next chapter. You are never asked to write assembly, and you are never asked to know every mnemonic; you are asked to say what the compiler did, which is a much smaller skill and takes one chapter to acquire.
Acquire it now rather than in passing, because the notation that carries most of the weight — parentheses, which mean memory — is also the most likely to be read as something else.
The material¶
One function, two instruction sets¶
Here is a function with no cleverness in it at all:
/* Two conditionals and three exits. The interesting question is whether the compiler emits
* branches or conditional moves, and the two architectures answer it differently — which makes
* this the smallest honest demonstration that instruction selection is not a detail. */
int sysfs_clamp(int value, int low, int high) {
if (value < low) {
return low;
}
if (value > high) {
return high;
}
return value;
}
Compiled for each of the book’s two architectures, at the same optimisation level — how hard the compiler was asked to try — by the same version of the same compiler. The conditions line under each listing says exactly which:
sysfs_clamp compiled for riscv64 at -O2 — the xv6 target’s instruction set, 10 instructions.
0000000000000000 <sysfs_clamp>:
0: blt a0,a1,18 <.L4>
4: mv a1,a0
6: blt a2,a0,10 <.L6>
a: sext.w a0,a1
e: ret
0000000000000010 <.L6>:
10: mv a1,a2
12: sext.w a0,a1
16: ret
0000000000000018 <.L4>:
18: mv a0,a1
1a: retConditions: target xv6 · riscv64 cross compiler, any machine — nothing here was executed · riscv64-linux-gnu-gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 · -O2 -Wall -Wextra -march=rv64gc -mabi=lp64d -c · 2026-09-17 · Source: bench/results/shapes-riscv64.json, code hash 96ffaccb103ddd44.
sysfs_clamp compiled for aarch64 at -O2 — the host target’s instruction set, 7 instructions.
0000000000000000 <sysfs_clamp>:
0: cmp w0, w1
4: b.lt 14 <sysfs_clamp+0x14> // b.tstop
8: cmp w0, w2
c: csel w0, w0, w2, le
10: ret
14: mov w0, w1
18: retConditions: target host · aarch64 cross compiler, any machine — nothing here was executed · aarch64-linux-gnu-gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 · -O2 -Wall -Wextra -c · 2026-09-17 · Source: bench/results/shapes-aarch64.json, code hash eb70bdd44fe4bbb2.
The rest of this chapter is how to read those two.
How a listing is laid out¶
The left-hand column is an address, in hexadecimal, counted from the start of the function.
0: is the first instruction, and the number on the next line tells you how many bytes it took —
so a second instruction at 4: means the first was four bytes long. In the RISC-V listing you will
also find instructions two bytes apart, because RISC-V has short forms of its commonest
instructions RISC-V International (n.d.) and the compiler uses them unasked. Instructions here are
not all the same length, and ch06 turns on that fact.
Then the mnemonic: blt is branch-if-less-than, mv is move, ret is return. You are not
expected to know these, and this book never asks you to write assembly — only to read enough of it
to say what the compiler did.
Then the operands, destination first. mv a1,a0 copies a0 into a1, not the other way
round. That order is the assembler’s convention RISC-V International (n.d.) and it is the one thing
most likely to mislead you if you skim.
a0, a1, a2 are registers — the processor’s own named slots, the fastest storage a program
has. The a ones carry arguments and return values RISC-V International (n.d.), which is why a function’s first
argument arrives in a0 and its answer leaves in a0. Appendix A lists them all.
Parentheses mean memory, and the number in front is a byte offset. 4(a0) is the memory at
the address in a0, plus four bytes — not a0 times anything. Nearly every listing in
Part I turns on that one piece of notation, and the offset is usually the width of
whatever is stored at that address, which is how the same C source becomes 4(a0) in one function
and 8(a0) in another.
Everything under a listing is provenance: which compiler, which flags, which result file it was captured from. It is there so that a listing you find surprising can be regenerated rather than argued about.
What the two disagree about¶
Read the second comparison in each. AArch64 settles it with csel — compute both candidates,
select one, never branch. RV64GC — the particular set of RISC-V instructions xv6 is built for,
and the one every RISC-V listing in this book was compiled for — has no conditional select
RISC-V International (n.d.), so the same decision has to be a branch, and the function comes out with
three separate exits.
That is a real difference, and it is not a difference in speed. Nothing above says which is faster, and What this cannot tell you below says why not.
Two smaller things are visible in the same listings. The RISC-V one has labels — .L4, .L6 —
sitting inside the function, and the AArch64 one has none; that is a division of labour between
two tools you meet in ch12 and
ch15, and for now it is enough to have noticed it. And sext.w, which
RISC-V emits on each path and AArch64 does not emit anywhere: RISC-V keeps a 32-bit int in a
64-bit register and has to say so, while AArch64 has a 32-bit view of the register and uses it.
Neither is in the C. Both are the kind of thing ch14 is for.
What we measured¶
One function, compiled for each of the book’s instruction sets by the same compiler at the same optimisation level. Nothing was executed and nothing was timed: a listing is a fact about a compiler, so it is the one kind of result in this book that CI can regenerate and check on every push. ch24 is about the kind that cannot be.
What this cannot tell you¶
Which of the two is faster. This is the mistake the chapter exists to prevent, and it is
tempting precisely because the listings are short enough to count. AArch64 settles the second
comparison without branching and RISC-V cannot, so one comes out shorter — and that tells you
nothing about time. A branch the processor guessed right is nearly free and one it guessed wrong
is not; csel pays a fixed price either way, and has to wait for both candidates before it can
choose. Which wins depends on the data the function is given, and the only way to find out is to
run both on hardware that can be asked. ch26 and ch27 do that.
What any of these instructions costs. A mnemonic is a name, not a price. Nothing in a listing says how many cycles an instruction takes, whether its operands were in cache, or whether the core ran it at the same time as its neighbour. Those are properties of a machine and this page has not been near one.
Whether the compiler was right. It made these choices at one optimisation level, for one target, from one version of one compiler — all of which the conditions line records, because all of them change the answer. A different flag produces a different listing from identical source, which is ch26’s subject.
Anything about a program. One function with no calls in it is the easiest possible listing. Real code spends its instructions on stack frames, calling conventions and address arithmetic, and none of that is visible here. ch14 is where a listing stops being a curiosity and starts being a thing you read to answer a question.
Problems¶
Three, in tests/reading_a_listing/. Each is a Python stub: this chapter comes before any C.
2.1 — How long is each instruction? Given the address column of a listing, say how many bytes each instruction occupied. RISC-V has short forms of its commonest instructions and the compiler uses them unasked, so the answer is not the same number every time.
python3 -m pytest tests/reading_a_listing/test_problem_1_lengths.py2.2 — Does this operand touch memory? Eleven operands drawn from both instruction sets. Say which of them name a register or a literal, and which reach memory. One pair differs only by punctuation and disagrees.
python3 -m pytest tests/reading_a_listing/test_problem_2_memory.py2.3 — How wide was the element? Given a load and its offset, say how many bytes wide the thing being stepped over was. This is the same fact the next chapter opens on, approached from the instruction rather than from the C.
python3 -m pytest tests/reading_a_listing/test_problem_3_width.pyWhere to go next¶
Appendix A is the RISC-V register and CSR reference, and Appendix F is this page’s counterpart for AArch64 — three differences that change how a listing reads. The assembler syntax and the register roles are defined in the RISC-V unprivileged specification RISC-V International (n.d.) and the psABI RISC-V International (n.d.).
ch03 is next: one complete program, and its listing read the way this chapter has just shown you.
- RISC-V International. (n.d.). The RISC-V Instruction Set Manual, Volume I: Unprivileged Architecture. https://riscv.org/technical/specifications/
- RISC-V International. (n.d.). RISC-V ELF psABI Specification. https://github.com/riscv-non-isa/riscv-elf-psabi-doc