英文:
Linux x64 Calling Convention - Why are the first 6 args at negative offsets from RBP?
问题
The first 6 parameters (Arguments 1-6) can be accessed using registers RDI, RSI, RDX, RCX, R8, and R9. They can also be read from and written to the stack using offsets from the RBP register like this: rbp - $offset. So, yes, the first 6 parameters can be stored both in registers and on the stack.
英文:
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?
答案1
得分: 3
这篇文章混淆了实际的调用约定,实际上只要求将前6个参数传递到寄存器中,并与特定编译器为被调用的函数生成代码的方式混淆。
具体来说,当使用gcc关闭优化时,它将通过将所有寄存器参数“溢出”到堆栈内存来开始函数。没有固有的需要这样做 - 仅使用从寄存器传递的参数将工作得很好,而且效率更高。但是当关闭优化时,gcc的优先级是快速编译和易于调试,代价是生成的代码通常效率低下。
关于调试,其中一个目标是程序中的每个变量,包括函数参数,在内存中都有一个明确定义的地址(声明为register
的变量可能是一个例外)。此外,在使用每行代码之前从该地址加载该值(即使该值可能已经在寄存器中),并在之后存储回去。这使得在调试器中逐步执行程序时,轻松查看和修改变量的值成为可能,而不会遇到可能已被优化掉的变量的问题。
我认为编译器在决定哪个参数放在哪个堆栈偏移量上没有特定的模式。它可能只是按照参数从左到右的顺序在堆栈上移动,但您不会希望依赖于这一点。它肯定不是一种记录在文档中的行为。
如果打开优化(-O
,-O2
等),你将看到溢出消失。编译器将只是使用从寄存器传递的参数值。根据需要,它可能将它们移动到其他寄存器,或者一旦不再需要就覆盖它们。
总之,将参数传递到寄存器中是一种明确定义和可预测的行为,由ABI标准指定。将它们溢出到堆栈是仅在使用特定编译器和特定选项集构建时才会发生的行为,而且在任何方面都不是标准或可预测的。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论