4

I was reading this page Linux x64 Calling Convention, but got confused about passing function parameters by registers and stack. It says:

Arguments 1-6 are accessed via registers RDI, RSI, RDX, RCX, R8, R9 before they are modified or via offsets from the RBP register like so: rbp - $offset.

If the first 6 parameters are passed by registers, how come they can also be read from and written to the stack? Are the first 6 parameters stored both in the registers and on the stack?

Sep Roland
  • 33,889
  • 7
  • 43
  • 76
Xiaoyong Guo
  • 361
  • 1
  • 7
  • It's assuming that all the arguments are also present in the stack of the calling function, in the reverse order as they're passed to the callee. Seems like an unsafe assumption to make... – Shawn Jul 13 '23 at 07:09
  • 1
    For a better explanation, with references, see https://stackoverflow.com/questions/2535989/what-are-the-calling-conventions-for-unix-linux-system-calls-and-user-space-f. – Nate Eldredge Jul 13 '23 at 07:11
  • @NateEldredge That's about the Linux system call abi, which is different from the normal user space one (different registers; in particular r10 instead of rcx) – Shawn Jul 13 '23 at 07:13
  • 1
    Ohh, I think I see. They're looking at unoptimized output from gcc, which will spill all the register-passed arguments onto the stack (so that, for instance, they have a well-defined address for a debugger to look at). But this is in no way part of the calling convention, and if you enable optimizations, the spills will no longer occur. – Nate Eldredge Jul 13 '23 at 07:13
  • @Shawn: The user-space ABI is described there too if you keep reading. The title of that post is annoying but the content is good. – Nate Eldredge Jul 13 '23 at 07:14
  • Related: [Why does the x86-64 System V calling convention pass args in registers instead of just the stack?](https://stackoverflow.com/q/51976465) - another person looking at unoptimized code, where the callee spills incoming register args unless you declare them `register int foo`. Also [gcc argument register spilling on x86-64](https://stackoverflow.com/q/7201763) . [Redundant register values in GCC-compiled binary?](https://stackoverflow.com/q/61893664) shows optimized asm for a function caller and callee. – Peter Cordes Jul 13 '23 at 17:55

1 Answers1

3

The article is mixing up the actual calling convention, which simply calls for passing the first 6 arguments in registers, with the way a particular compiler emits code for the called function.

Specifically, when gcc is used with optimization turned off, it will begin the function by "spilling" all the register arguments into stack memory. There is no inherent need to do that - it would work just fine, and be more efficient, to just use the arguments from the registers where they were passed. But when optimizations are off, gcc's priorities are fast compilation and easy debugging, at the expense of emitting code that's often hilariously inefficient.

With regard to debugging, one of its objectives is that every variable in the program, including function parameters, has a well-defined address in memory. (Variables declared register could be an exception.) Moreover, the value is loaded from that address before each line of code that uses it (even if the value happens to already be in a register) and stored back afterward. This makes it easy to inspect and modify values of variables as you step through the program in your debugger, without running into problems with variables that may have been optimized out.

I don't think there is any particular pattern followed by the compiler in deciding which argument goes at which stack offset. It could be simply moving down the stack as the arguments go left to right, but you wouldn't want to rely on that. It certainly isn't a documented behavior.

If you turn on optimizations (-O, -O2, etc), you will see the spills go away. The compiler will just use the argument values from the registers where they were passed. As needed, it might move them into other registers, or overwrite them once they are no longer needed.

So in short, passing the arguments in registers is a well-defined and predictable behavior, specified by the ABI standard. Spilling them to the stack is behavior that occurs only when building with a specific compiler using a specific set of options, and is not standard or predictable in any way.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82