我对那些保存了“保存呼叫者”和“保存有被调用者”的寄存器感到困惑。如果一个函数既是调用者又是被调用者,该怎么办?
说main
函数调用函数P
,而函数P
调用函数Q
。在这种情况下,P
既是被调用方(对于main
)又是调用方(对于Q
),那么程序集将使用哪个寄存器?
答案 0 :(得分:1)
为简单起见,我将假设使用 System V ABI ,并将此说明限制为整数和指针参数。有关Unix调用约定的详细说明,请参见this post。
前六个参数在寄存器rdi
,rsi
,rdx
,rcx
,r8
和r9
中传递给函数。这些寄存器是保存为调用者的,即它们可能不会在函数调用之间保留,因为它们被调用者可能掩盖了它们(对于它们而言,更合适的术语是呼叫密集的寄存器)。
我们将Q
声明为:
int Q(int, int, int, int, int, int);
也就是说,Q
包含六个整数,因此编译器生成的代码被强制使用所有这六个调用者保存的寄存器。
现在,让我们以这种方式定义P
:
int P(int a, int b, int c, int d, int e, int f) {
return Q(a, b, c, d, e, f) + Q(f, e, d, c, b, a);
}
通过这种方式,编译器将耗尽调用者保存的寄存器,并且将被强制使用 callee-saved 寄存器作为上述函数的代码。
上面的代码生成以下程序集:
P:
// save rbx, rbp, r12, r13, r14 and r15 onto the stack
// copy rdi, rsi, rdx, rcx, r8 and r9 into those registers
pushq %r15
movl %esi, %r15d
pushq %r14
movl %edx, %r14d
pushq %r13
movl %ecx, %r13d
pushq %r12
movl %r8d, %r12d
pushq %rbp
movl %r9d, %ebp
pushq %rbx
movl %edi, %ebx
subq $24, %rsp
call Q // <-- 1st call to Q (may clobber rdi, rsi, rdx, rcx, r8 and r9)
// prepare rdi, rsi, rdx, rcx, r8 and r9 for the 2nd call to Q
movl %ebx, %r9d
movl %r15d, %r8d
movl %r14d, %ecx
movl %r13d, %edx
movl %r12d, %esi
movl %ebp, %edi
movl %eax, 12(%rsp)
call Q // <-- 2nd call to Q
addl 12(%rsp), %eax
addq $24, %rsp
popq %rbx
popq %rbp
popq %r12
popq %r13
popq %r14
popq %r15
ret
允许调用Q
破坏寄存器rdi
,rsi
,rdx
,rcx
,r8
和{{1 }}(以及其他)。这些寄存器包含调用r9
的参数(即P
,a
,b
,c
,d
和{{1 }}和它们的值仍然是第二次调用e
所必需的。因此,寄存器f
,Q
,rbx
,rbp
,r12
和r13
用于在调用之前复制这些寄存器到r14
中以保留原始参数r15
的调用方式。
Q
不允许破坏用于保存被调用方保存的寄存器的这些寄存器,因为它们是 caller-saved 寄存器,即,它们的值必须保留在函数调用(它们的另一个术语是调用保留的寄存器)。因此,函数P
中的代码会在破坏寄存器之前使用Q
将这些寄存器保存到堆栈中,并在控制流从{{1}返回之前用堆栈中的P
将它们恢复。 }(push
是pop
的主叫方)。 P
的调用者将看到的净结果是这些寄存器的原始值与调用P
之前的值相同。
请注意,第二次调用Q
可能会使寄存器P
,P
,Q
,rdi
,rsi
和{{ 1}}也是如此,但是没有必要保留它们的值,因为它们不再在rdx
返回之后出现。
要更深入地了解寄存器破坏,请查看What registers are preserved through a linux x86-64 function call。