Every tutorial of the x86-64 system-call ABI opens with the same table: number in RAX, arguments in RDI, RSI, RDX, R10, R8, R9, answer in RAX, clobbered registers RCX and R11. Where does that table come from?
Not from Intel. Not from AMD. Not from any architecture manual. There is no such concept as a “system call number” in the x86-64 architecture, and if you search the SDM for the phrase you will find nothing. It is a Linux decision, and it is a decision made for a reason that is visible once you see which registers the hardware already took.
That distinction is the point of this concept, and it is worth being pedantic about because conflating the two produces a specific and common error: writing a syscall wrapper that sets the carry flag on error, because that is what the i386 ABI did, and then wondering why your x86-64 program reports errors wrongly.
Sort the convention into what the hardware guarantees and what a particular operating system chose, and the second list turns out to be nearly everything.
Why R10 and not RCX is the whole story in one fact. The System V AMD64 calling convention — the C ABI, which your compiler already implements — assigns the first six integer arguments to RDI, RSI, RDX, RCX, R8, R9. The hardware then takes RCX and R11 for its own bookkeeping. If the syscall convention followed the C ABI, a four-argument syscall would collide with the return address. So the kernel routed around its own calling convention, and put the fourth argument in R10, a register the C ABI does not use for arguments in this position and which is free precisely because the hardware took RCX instead.
This is worth understanding as a design constraint rather than a fact to memorise: every part of the syscall ABI is downstream of the two registers the instruction stole.
There is a better source than either manual, and it is a file the kernel maps into every process: the vDSO. It contains real kernel-compiled code that issues real system calls, and it is readable from user mode. The executable-images course read its ELF header. This course reads its instructions.
The opcode for syscall is 0f 05 — two bytes, and the ISA course's decoder already knows that. So: find those two bytes in the vDSO's executable segment, and look at the five bytes before each one.
$ ./privbench | sed -n '/Scanning the executable segment/,/decoded/p'
Scanning the executable segment for 0f 05, the SYSCALL opcode:
vaddr 0x0ab2 b8 e4 00 00 00 0f 05
.......
`mov $228, %eax ; syscall` -> clock_gettime
vaddr 0x1031 b8 60 00 00 00 0f 05
.`.....
`mov $96, %eax ; syscall` -> gettimeofday
vaddr 0x12f5 b8 e5 00 00 00 0f 05
.......
`mov $229, %eax ; syscall` -> clock_gettime64
vaddr 0x15c3 00 00 4c 89 ce 0f 05
..L....
vaddr 0x1600 00 00 00 31 d2 0f 05
...1...
5 SYSCALL instruction(s) in 6667 bytes of vDSO code, 3 decoded
Five syscall instructions in the vDSO, three of which are unambiguously a five-byte immediate load into EAX immediately before the instruction. The numbers are 228, 96 and 229. Check them against asm/unistd_64.h: they are clock_gettime, gettimeofday and clock_gettime64. Three of them, exactly right, in the kernel's own encoding, read out of a page a user process is allowed to read.
The other two sites are not decoded, and the artifact says so rather than inventing a number for them: in both, the immediate is being loaded somewhere other than EAX, and a bare b8 byte occurring nearby is a one-in-256 coincidence, not an instruction. Only two of the five facts about this convention come from the architecture, and this measurement recovers three kernel-chosen numbers without consulting a single manual.
The obvious thing to look for is the sigreturn trampoline: the code that returns from a signal handler, which on older kernels lived in the vDSO as mov $15, %eax ; syscall. It is not in this build. Linux removed the vDSO sigreturn path years ago, and a correct decoder reports that:
$ ./privbench | sed -n '/No sigreturn trampoline/,/lies/p'
No sigreturn trampoline in this build: Linux removed the vDSO
sigreturn path, and a decoder that printed 15 here anyway,
from a pattern that merely happened to be nearby, is a decoder
that lies. This file reports the absence instead.
This is a small thing with a large lesson attached. The first draft of this concept's decoder did print 15 — it looked for b8 five bytes before each 0f 05 and printed whatever it found, and on a build with a trampoline it would have been right, so the code looked fine. It found a plausible-looking number on a build that had no trampoline, and there was nothing to distinguish the two cases. Matching the exact byte pattern b8 0f 00 00 00 0f 05 0f instead of a heuristic makes the absence detectable, which is the only way to know the difference between “found it” and “looked for it.”
Before the numbers above could be read, the program had to find the vDSO. It got the wrong pointer for a while, and the failure was instructive.
$ ./privbench | sed -n '/where does the vDSO pointer come from/,/recur silently/p'
where does the vDSO pointer come from?
__ehdr_start (the MAIN EXECUTABLE) 0x00005cb8d9644000 e_phnum=14
AT_SYSINFO_EHDR (the vDSO) 0x00007465da26f000
Two different images, and the one with the obvious name is not
the one this section is about. Print both or you will debug the
wrong header, which is what this file did first.
__ehdr_start is a glibc symbol holding the base of the ELF header of the main executable. It is not the vDSO, and the name is not a hint that it might be. The first version of this section read it, printed fourteen program headers and an e_shoff that pointed past the end of the text, and reported every one of those as a fact about the vDSO. Every one of them was true, and every one of them was about the wrong image.
Why so easy to get wrong? Because the two images are almost identical in shape. Both are ET_DYN, both are EM_X86_64, and both carry a plausible fourteen-entry program header table. The vDSO is a shared object; a position-independent executable is a shared object. When a decoder reports something surprising about a file, check that you are looking at the file you meant before you investigate why the file is strange. The artifact now prints both addresses on adjacent lines so the confusion cannot recur quietly.
/usr/include/x86_64-linux-gnu/asm/unistd_64.h. Then disassemble your own libc: objdump -d /lib/x86_64-linux-gnu/libc.so.6 | grep -B3 syscall and find the same pattern. How many distinct syscall numbers can you account for, and what are the ones you cannot yet name?R10 decision in one sentence, without using the word “because.” Then explain why the sixth argument is R9 and not something else, given the C ABI runs out of registers at the same place. (It does not: the C ABI has RDI RSI RDX RCX R8 R9 and the syscall convention has RDI RSI RDX R10 R8 R9 — six in both cases, with exactly one substitution, and the substitution is the collision.)syscall() wrapper that checks the carry flag, compile it, and use it to call a syscall that fails. Now work out why it reports success. Then fix it, and write down which convention is x86-64 and which is i386.SYSCALL page says about the return value. Is the SDM silent, or does it say something?Forward, the canonical-address concept is the other half of what makes ring 3 ring 3. This one covered the entry; that one covers the space you enter from.
Backwards, the dependency is tight and worth naming. The doors concept established that SYSCALL performs no CPL check and reads three MSRs; this concept established that the register the architecture does specify — the number — is one the architecture never mentions. Together they say something a single fact could not: the architecture's half of a system call is a stack switch and two register moves, and the operating system's half is everything else.
To earlier courses, three of them. The vDSO concept established that the page exists, is 8 KB, has no file behind it, and is where the clock lives. The auxiliary-vector concept established that AT_SYSINFO_EHDR is how you are told where it is — and, characteristically, that no libc function returns it. The ISA course's decoder is what made 0f 05 a thing you could search for. Three courses, one page, and not one of them ever read a byte of its code.
Outward, for the compiler author this is the concept that matters most, because it is the one place where the ABI is the operating system's ABI and not yours. A calling convention is something a compiler and a library agree on and can version. A system-call convention is something a kernel and every binary ever compiled against it agree on, and it can never be changed. When you emit a call to syscall you are emitting something that will outlive the kernel, and the reason R10 is in the fourth slot is a decision made in 2003 about two registers.