A function call on x86-64 has no visible ceremony. There is no push of the arguments, no stack frame set up by the callee, no ret address computed. The caller puts six values into six registers and executes call, and that is the entire mechanism. Which six registers is therefore not a detail — it is the calling convention, and there is nowhere else to look it up.
And the answer is one of those facts that is easy to get confidently wrong, because a plausible story circulates about it and the story is false in an instructive way. This course was drafted around that story:
The first error is checkable by reading a disassembly, and the audit below checks it sixty-four times. The second is checkable by reading a register, and the measurement in the second half of this page does it. Both halves are needed, because the first half alone leaves you with the fact that the fourth argument is %rcx and no reason to have believed anything else.
The System V AMD64 ABI assigns integer arguments to registers in a fixed order. The numbers are the part nobody memorises and the part a backend cannot guess:
$ ./abidump | sed -n '/1B\. THE SPECIFICATION/,/^ the two sequences/p'
1B. THE SPECIFICATION, PRINTED AS THE ORACLE AND NOT MEASURED.
This is the only table in this file that is quoted. Every
other number below came out of this machine.
integer argument registers rdi rsi rdx rcx r8 r9
their register numbers 7 6 2 1 8 9
SSE argument registers xmm0 .. xmm7, and NO others
Read the numbers twice. %rcx is register 1 and %rdx is register 2 — they are not adjacent in the encoding order, and the fourth argument register is the one whose encoding number is the smallest of the six. %r8 and %r9 are numbers 8 and 9, continuing the physical order, which is why the list looks like it was assembled by taking registers 7, 6, 2, 1 and then starting again at 8.
That is the specification. Here is the point of the course, which is that it can be checked rather than quoted. A corpus of sixteen call sites is compiled four ways, and a small program reads the disassembly back and recovers which argument went into which register, identified by name, because each argument's source is its own volatile global:
$ grep '^CALLER | O2' abi_dis.txt CALLER | O2 | c_f6 | v11=rdi v12=rsi v13=rdx v14=rcx v15=r8 v16=r9 | v11=rdi v12=rsi v13=rdx v14=rcx v15=r8 v16=r9 | MATCHES THE SPECIFICATION CALLER | O2 | c_f7 | v11=rdi v12=rsi v13=rdx v14=rcx v15=r8 v16=r9 v17=stack | v11=rdi v12=rsi v13=rdx v14=rcx v15=r8 v16=r9 v17=stack | MATCHES THE SPECIFICATION CALLER | O2 | c_mx | w1=xmm0 w2=xmm1 w3=xmm2 w4=xmm3 v21=rdi v22=rsi v23=rdx v24=rcx | w1=xmm0 w2=xmm1 w3=xmm2 w4=xmm3 v21=rdi v22=rsi v23=rdx v24=rcx | MATCHES THE SPECIFICATION
That third row is worth pausing on, because it is a belief rather than a fact that it used to be. mx takes four doubles and four longs, interleaved, and the doubles go in %xmm0 through %xmm3 while the longs go in %rdi, %rsi, %rdx, %rcx — the integers do not start at %rdiafter the doubles; the integers start at %rdi regardless of the doubles. The two sequences are independent counters. The specification says so, and this row is the measurement.
And the seventh argument is not in a register at all. The caller's row says v17=stack, and the callee's row says where on the stack:
$ grep -E '^CALLEE \| O2 \| f[5-8]' abi_dis.txt CALLEE | O2 | f5 | rdi rsi rdx rcx r8 | stack=none CALLEE | O2 | f6 | rdi rsi rdx rcx r8 r9 | stack=none CALLEE | O2 | f7 | rdi rsi rdx rcx r8 r9 | stack=0x10 CALLEE | O2 | f8 | rdi rsi rdx rcx r8 r9 | stack=0x18
One slot is 8 bytes past the return address, and the second is 8 past that. Those are the only two numbers in the calling convention that are not a register number, and the next concept is about what they are aligned to.
The draft's second claim was that the hardware needs %rcx for the return RIP. Here is the word at 0(%rsp) at a callee's first instruction, read by the callee itself:
$ ./abidump | sed -n '/%rsp at that point/,/^ THE RETURN/p' \
| sed 's/0x0000[0-9a-f]\{12\}/<per-run address>/'
%rsp at that point <per-run address> (a STACK address)
0(%rsp) at that point <per-run address> (a TEXT address)
and the two are not within a few bytes of each other, which is the
point: the word AT %rsp is not part of the stack. It is the
address `ret` will pop and jump to, and the control is that this
function returned to its own caller rather than anywhere else.
BOTH READINGS COME FROM ONE CALL, which is the only way a
comparison between two readings of a register means anything.
%rsp names a stack address. The word sitting at that address is a code address, in a different part of the address space entirely. call pushes the return RIP; ret pops it; no register is involved in either. The control is the second thing to read: the function returned to its own caller rather than somewhere else, which is what makes the word a return address rather than a coincidence.
Both readings come from one call on purpose. The first version of this measurement asked for %rsp in one call and for 0(%rsp) in another, and the two came from two different stack depths, so the comparison produced a number near 264 and printed as an address-looking value that is obviously wrong to a reader and was not caught for a whole build. A difference between two readings of a register is only meaningful if they are the same reading of the same instant.
Now the part of the draft that is true, attached to the instruction it is actually true of. A bare syscall writes the address of the following instruction into %rcx:
$ ./abidump | sed -n '/getpid() through libc/,/survived/p' \
| grep -v RFLAGS \
| sed 's/0x0000[0-9a-f]\{12\}/<per-run address>/'
getpid() through libc = 982204
%rax after the bare `syscall` = 982204 match ? YES
the instruction right after it = <per-run address>
%rcx BEFORE the syscall = <per-run address>
%rcx AFTER the syscall = <per-run address>
%rcx == the address of the NEXT INSTRUCTION ? YES
%r10 after it, my own marker = 0xfeedfacecafebeef survived ? YES
Read the things that are checked rather than assumed — and notice that the addresses are elided by the sed in the command above rather than by an author, because an address is not a measurement. This is a position-addressed binary, so the text base moves on every run; quoting one would invite the reader to compare it with the recorded output and conclude that something had changed when nothing had.
%rcx after the instruction equals the address of the instruction that follows it, bit for bit. The "after" label is right above the code that captured the address, and the program compares the two as numbers and prints the verdict. A memory address in a register and the address of the next instruction are not the same kind of thing to coincide by accident.%rax after the instruction equals what libc's getpid() returned. So the instruction really was a system call and not a nop that happens to leave %rcx alone. The control is a value nobody put there.%r11 holds the RFLAGS value the instruction saved before entering the kernel — 0x202 or 0x206 in the recorded runs, the difference being bit 2, the parity flag, which depends on the parity of the process id that came back in %rax. That is the one line the grep in the command above drops, and dropping it is the honest move: a value that depends on the parity of a pid is not a result. Bit 1 is set, which the ISA requires of bit 1 in every RFLAGS, and bit 9 is the interrupt-enable flag, preserved across the call as a caller's value should be.%r10 came back holding the marker this program put there before the instruction. The kernel did not touch it, which is the entire reason the system call convention is free to use it.This is not a curiosity. It means that every time you read about “the x86-64 calling convention” you should ask which one, and the answer is visible in the disassembly: a call site that ends in call is the first, and one that ends in syscall is the second. They share five of their six argument registers. The sixth is the entire difference, and the fourth argument is where it shows.
Here is the whole of the audit result, from the recorded run:
$ ./abidump | sed -n '/THE AUDIT, PER OPTIMISATION LEVEL/,/^$/p' level callers audited match the specification differ -O0 16 16 0 -O1 16 16 0 -O2 16 16 0 -Os 16 16 0
Sixty-four call sites audited, sixty-four matches, zero differences, at four optimisation levels. That is the course's distinctive idea stated as a number, and it is worth being precise about what it does and does not establish.
What it establishes is that a specification can be transcribed into a checker and the checker can be run against a real compiler's real output, and the two agree. That is a genuinely useful thing to have, because the specification is a document nobody re-reads and the compiler is a program that changes every quarter. A rule that has never been checked is a rule that is a memory.
What it does not establish is that the specification is right. The audit compares the emitted code against a transcription. If the transcription is wrong, the audit reports a perfect match with a wrong oracle, and the number 64 of 64 would be exactly as confident and exactly as wrong. The specification is section 1B of the artifact, printed separately from every measurement precisely so that a reader can check the oracle against the document it came from — the psABI itself — in one reading, without a compiler in the way.
And the corpus had to be built to make the question possible, which is where two of this collection's harder-won lessons went. Marking the functions noinline was not enough: at -O2 gcc constant-folded every one of them, because the bodies are pure, so the calls vanished and the register assignment was no longer anywhere in the program. The attribute that works is noipa, which forbids interprocedural analysis of any kind. And even then, gcc knows the value of every argument in the caller, so the sources are volatile globals. A corpus that does neither measures the optimiser's mood rather than the ABI.
cd courses/x86abi/assets/samples && ./build_samples.sh, then read section 2. (Expect 16 of 16 at all four levels. If a row differs, do not assume the compiler is wrong: grep DIFFERS abi_dis.txt shows the mapping it recovered and the mapping it expected, and the interesting case is a parameter the optimiser deleted rather than a register it chose wrongly. Both readings are in the row, which is the point of printing both.)SPEC["c_f4"] in mkabi.py so that argument four is expected in %rdx, and re-run. (Expect every level to report one difference, and the difference to be c_f4 and only c_f4. A checker that fails on everything is not a checker; a checker that fails on exactly the thing you broke is an oracle.)getpid block out of abidump.c, put it in its own file, and add a second movq $marker, %r10 before the syscall and read it back after. (Expect %r10 to survive and %rcx to be gone. Then move the syscall number into %r10 as well and confirm it fails — the kernel reads the number from %rax, and a system call with the number in the wrong register is -ENOSYS rather than a crash, which is its own small lesson about how much checking happens where.)f(double, long, double, long) puts the doubles in %xmm0, %xmm1 and the longs in %rdi, %rsi — not the longs in the two registers after %xmm1, which is not even a thing the hardware has.)%rsp, and expect the alignment of that area to be the subject of the next concept, where it turns out to be the difference between working and a SIGSEGV.)Backwards, the assembly course's syntax concept is a prerequisite rather than a link: the audit in this concept reads a disassembly and recovers a mapping from it, and it cannot do that without knowing that mov rdi,QWORD PTR [rip+0x2b26] is a load whose destination is the operand on the left. The integer set is where movabs, the instruction every measurement here uses to plant a 64-bit marker, is taught. isa-rex is where register numbers 8 through 15 come from, which is why %r8 is fifth in the argument list rather than being absent.
Forwards, three things in this concept are the raw material of the next three. The stack=0x10 in the table above is the seventh argument, and what it is aligned to is the subject of the stack frame, where the alignment rule turns out to be a fault rather than a slowdown and to be two rules rather than one. %r8 and %r9 are the only two of the six that are caller-saved and not used for anything else, which is the observation behind the callee-saved concept. And the independent double counter is what makes a variadic function possible at all, which is the varargs concept.
Outward, the neighbour that owns the other half of this page. The syscall measurement here is a register-level observation and deliberately stops there: why Linux renumbered its argument list rather than saving and restoring %rcx is a decision recorded in the kernel, and this file cannot observe a decision. The third course in this section is about the machine's privileged side, where syscall is not a curiosity but the only way into the kernel, and where the argument registers are the same six minus one.