The Function Signature Is a Lie | Josh Lospinoso Skip to content ABI series | Part 1 of 9 | May 29, 2026<br>The Function Signature Is a Lie<br>Modern ABIs, hidden arguments, and the binary protocols beneath ordinary-looking function calls.<br>“<br>The interface between two programs consists of the set of assumptions that each programmer needs to make about the other program…
Butler Lampson, Hints for Computer System Design<br>ABI series | Part 1 of 9<br>Inside the ABI series<br>The function call you write is the public API. The ABI call is the private wire format.
View series
ABI labTrace hidden result storage. Open the focused ABI lab for this part.
Show all nine posts01The Function Signature Is a Lie<br>02Same Function, Three Realities<br>03The Bugs That Make ABI Rules Memorable<br>04Hidden Arguments Are Everywhere<br>05Varargs: The Function Call With Missing Type Information<br>06ARM64 Is Not Just x64 With More Registers<br>07It Works Until the Stack Walker Arrives<br>08The ABI Is the Boundary<br>09Calls Are Now a Security Surface<br>labABI evidence lab<br>A function signature is one of those assumptions. It is not all of them.
You write this:
struct Big make_big(long long seed);<br>You call it like this:
struct Big b = make_big(100);<br>One source argument. One return value. A perfectly ordinary function signature.
Now cross the ABI boundary.
On common 64-bit targets, if the return type is not ABI-register-returnable, the caller allocates storage for the result and passes a pointer to that storage into the callee. Size is the easiest way to trigger that path, which is why this example uses a deliberately large struct. But the real rule is ABI classification: layout and C++ object semantics can force an indirect return even when a casual size-only rule would mislead you. That pointer is not in your source signature. It is not the seed. It is not a local variable you named. But at the ABI level, it may be the first thing the callee receives.
For this deliberately large example:
struct Big {<br>long long data[8];<br>};
struct Big make_big(long long seed);<br>The call can lower conceptually like this:
Target ABIHidden result channelWhere the visible seed goesWindows x64result pointer in RCX, returned again in RAXshifted to RDXSystem V AMD64result pointer in RDI, returned again in RAXshifted to RSIAAPCS64indirect result location in x8still in x0<br>The first source argument may not be the first ABI argument.1 2 3
Open the ABI lab at the System V AMD64 trace. It highlights mov rax, rdi in missing_sret.sysv.s.txt, then explains what that line shows and where the evidence stops. The ABI standard remains the authority.
The small excerpts below keep the one-argument make_big shape from this section. The lab receipts use the two-argument make_sret_record(seed, tag) variant introduced later so that argument shifting is visible for more than one source parameter.
; Windows x64, abbreviated one-argument shape<br>make_big:<br>movq %rcx, %rax ; return the hidden result pointer<br>movq %rdx, (%rcx) ; seed was shifted to RDX
; System V AMD64, abbreviated one-argument shape<br>make_big:<br>mov rax, rdi ; return the hidden result pointer<br>mov qword ptr [rdi], rsi
; AArch64 Linux, abbreviated one-argument shape<br>make_big:<br>str x0, [x8] ; seed in x0, result storage in x8<br>If a JIT stub, hand-written trampoline, or FFI bridge forgets the hidden result channel, the failure depends on the target ABI. On Windows x64 and System V AMD64, omitting the hidden result channel can put the first visible value where the callee expects a writable result address. On AAPCS64, the visible arguments can still be in x0 and x1; the danger is that x8 is stale, unset, or otherwise not a valid result-storage address. The source call looked like ordinary visible arguments. The machine-level call also needed an address.
That is the lie.
Not that the function signature is wrong. The source signature is faithful as a source-level API. It tells the programmer what type is returned, what type is accepted, and what expression is legal to write.
The lie is that the signature is complete.
A source-level function signature is a programmer-facing projection. The actual cross-boundary call is a target-specific protocol involving data layout, argument classification, hidden parameters, register assignment, stack obligations, saved state, and unwind metadata.4
The function call you write is the public API. The ABI call is the private wire format.
“Private” does not mean secret. Many ABIs are public documents, and if you write compilers, JITs, foreign-function interfaces, profilers, hooks, loaders, crash dump tools, or hand-written assembly, those documents are your operating manual. Private means beneath the source API. It is the protocol the caller and callee speak after the pretty syntax has been compiled away.
The wire is usually registers, stack, and metadata, not a byte stream.
The source call is not the whole call
A source signature is written for humans and type checkers. It says:
struct Big make_big(long...