I believe I understand how the linux x86-64 ABI uses registers and stack to pass parameters to a function (cf. previous ABI discussion). What I'm confused about is if/what registers are expected to be preserved across a function call. That is, what registers are guarenteed not to get clobbered?
-
See also [Where is the x86-64 System V ABI documented?](https://stackoverflow.com/q/18133812) (currently https://gitlab.com/x86-psABIs/x86-64-ABI, but that doesn't have easy-to-download PDFs, just the LaTeX source.) – Peter Cordes Jan 09 '21 at 02:54
-
See also [What are callee and caller saved registers?](https://stackoverflow.com/a/56178078) re: call-preserved vs. call-clobbered registers. (And the horrible "callee-saved" terminology that's sometimes unfortunately used to describe them.) – Peter Cordes May 10 '21 at 21:54
3 Answers
Here's the complete table of registers and their use from the documentation [PDF Link]:
r12
, r13
, r14
, r15
, rbx
, rsp
, rbp
are the callee-saved registers - they have a "Yes" in the "Preserved across function calls" column.

- 219,201
- 40
- 422
- 469
-
-
4@socketpair: DF must be unset before any call or return, so the count-upward behaviour can be assumed without `cld`. Condition flags (like ZF) are call-clobbered. I forget exactly what the ABI docs say about FP rounding modes and denormals-are-zero. Maybe something like if a function modifies them, it must save/restore the previous state before returning, but **don't take my word for the FP part**. – Peter Cordes Feb 01 '16 at 01:00
-
3I would recommend taking the PDF from one of those sources instead of uclibc website: https://stackoverflow.com/questions/18133812/where-is-the-x86-64-system-v-abi-documented :-) – Ciro Santilli OurBigBook.com Mar 17 '19 at 13:02
Experimental approach: disassemble GCC code
Mostly for fun, but also as a quick verification that you understood the ABI right.
Let's try to clobber all registers with inline assembly to force GCC to save and restore them:
main.c
#include <inttypes.h>
uint64_t inc(uint64_t i) {
__asm__ __volatile__(
""
: "+m" (i)
:
: "rax",
"rbx",
"rcx",
"rdx",
"rsi",
"rdi",
"rbp",
"rsp",
"r8",
"r9",
"r10",
"r11",
"r12",
"r13",
"r14",
"r15",
"ymm0",
"ymm1",
"ymm2",
"ymm3",
"ymm4",
"ymm5",
"ymm6",
"ymm7",
"ymm8",
"ymm9",
"ymm10",
"ymm11",
"ymm12",
"ymm13",
"ymm14",
"ymm15"
);
return i + 1;
}
int main(int argc, char **argv) {
(void)argv;
return inc(argc);
}
Compile and disassemble:
gcc -std=gnu99 -O3 -ggdb3 -Wall -Wextra -pedantic -o main.out main.c
objdump -d main.out
Disassembly contains:
00000000000011a0 <inc>:
11a0: 55 push %rbp
11a1: 48 89 e5 mov %rsp,%rbp
11a4: 41 57 push %r15
11a6: 41 56 push %r14
11a8: 41 55 push %r13
11aa: 41 54 push %r12
11ac: 53 push %rbx
11ad: 48 83 ec 08 sub $0x8,%rsp
11b1: 48 89 7d d0 mov %rdi,-0x30(%rbp)
11b5: 48 8b 45 d0 mov -0x30(%rbp),%rax
11b9: 48 8d 65 d8 lea -0x28(%rbp),%rsp
11bd: 5b pop %rbx
11be: 41 5c pop %r12
11c0: 48 83 c0 01 add $0x1,%rax
11c4: 41 5d pop %r13
11c6: 41 5e pop %r14
11c8: 41 5f pop %r15
11ca: 5d pop %rbp
11cb: c3 retq
11cc: 0f 1f 40 00 nopl 0x0(%rax)
and so we clearly see that the following are pushed and popped:
rbx
r12
r13
r14
r15
rbp
The only missing one from the spec is rsp
, but we expect the stack to be restored of course. Careful reading of the assembly confirms that it is maintained in this case:
sub $0x8, %rsp
: allocates 8 bytes on stack to save%rdi
at%rdi, -0x30(%rbp)
, which is done for the inline assembly+m
constraintlea -0x28(%rbp), %rsp
restores%rsp
back to before thesub
, i.e. 5 pops aftermov %rsp, %rbp
- there are 6 pushes and 6 corresponding pops
- no other instructions touch
%rsp
Tested in Ubuntu 18.10, GCC 8.2.0.

- 347,512
- 102
- 1,199
- 985
-
1Why are you using `+a` and `+r` in the same constraint as different options? `"+rax"` is very deceptive, because it *looks* like you're asking for the input in the RAX register (which you can't because it's clobbered). But you're not, you're actually asking for it in any GP register (r), RAX (a), or any XMM register (x). i.e. equivalent to `"+xr"`. Since you leave one XMM register unclobbered, the compiler picks XMM15. You can see this by making the asm template string `"nop # %0"` so it expands %0 in a comment. https://godbolt.org/z/_cLq2T. – Peter Cordes Mar 17 '19 at 17:17
-
1Clang chokes on `"+rx"`, but not "+xr". I think clang doesn't actually use constraint alternatives properly, and only picks one. This might be why `"+rm"` constraints often get clang to spill a register as if it picked the `"+m"` option for no reason. – Peter Cordes Mar 17 '19 at 17:19
-
@PeterCordes oops I was in a bit of a hurry, meant to be just `+r`, love how the thing does not blow up on `rax`. `+m` is just better in this case though. – Ciro Santilli OurBigBook.com Mar 17 '19 at 18:17
-
3You may have found a compiler bug. You declare a clobber on RSP *and* RBP, but gcc and clang both use RBP after the asm statement (to restore RSP), i.e. they assume RBP is still valid. They also use an RBP-relative addressing mode for `%0`, but I guess clobber declarations are not early-clobber. Still, that's surprising. If we declare only an RSP clobber (https://godbolt.org/z/LhpXWX comments the RBP clobber), they make a stack frame and use an RBP-relative addressing mode, identical to with both clobbers. TL:DR: RSP+RBP clobber = bug, even when other regs are not clobbered. – Peter Cordes Mar 17 '19 at 23:43
The ABI specifies what a piece of standard-conforming software is allowed to expect. It is written primarily for authors of compilers, linkers and other language processing software. These authors want their compiler to produce code that will work properly with code that is compiled by the same (or a different) compiler. They all have to agree to a set of rules: how are formal arguments to functions passed from caller to callee, how are function return values passed back from callee to caller, which registers are preserved/scratch/undefined across the call boundary, and so on.
For example, one rule states that the generated assembly code for a function must save the value of a preserved register before changing the value, and that the code must restore the saved value before returning to its caller. For a scratch register, the generated code is not required to save and restore the register value; it can do so if it wants, but standard-conforming software is not allowed to depend upon this behavior (if it does it is not standard-conforming software).
If you are writing assembly code, you are responsible for playing by these same rules (you are playing the role of the compiler). That is, if your code changes a callee-preserved register, you are responsible for inserting instructions that save and restore the original register value. If your assembly code calls an external function, your code must pass arguments in the standard-conforming way, and it can depend upon the fact that, when the callee returns, preserved register values are in fact preserved.
The rules define how standards-conforming software can get along. However, it is perfectly legal to write (or generate) code that does not play by these rules! Compilers do this all the time, because they know that the rules don't need to be followed under certain circumstances.
For example, consider a C function named foo that is declared as follows, and never has its address taken:
static foo(int x);
At compile-time, the compiler is 100% certain that this function can only be called by other code in the file(s) it is currently compiling. Function foo
cannot be called by anything else, ever, given the definition of what it means to be static. Because the compiler knows all of the callers of foo
at compile time, the compiler is free to use whatever calling sequence it wants (up to and including not making a call at all, that is, inlining the code for foo
into the callers of foo
.
As an author of assembly code, you can do this too. That is, you can implement a "private agreement" between two or more routines, as long as that agreement doesn't interfere with or violate the expectations of standards-conforming software.

- 45,431
- 5
- 48
- 98

- 3,249
- 1
- 12
- 4