在x86-64中仅使用16个寄存器跟踪调用方和被调用方功能

时间:2018-10-14 17:55:25

标签: assembly x86-64 cpu-registers calling-convention

我对那些保存了“保存呼叫者”和“保存有被调用者”的寄存器感到困惑。如果一个函数既是调用者又是被调用者,该怎么办?

main函数调用函数P,而函数P调用函数Q。在这种情况下,P既是被调用方(对于main)又是调用方(对于Q),那么程序集将使用哪个寄存器?

1 个答案:

答案 0 :(得分:1)

为简单起见,我将假设使用 System V ABI ,并将此说明限制为整数和指针参数。有关Unix调用约定的详细说明,请参见this post


前六个参数在寄存器rdirsirdxrcxr8r9中传递给函数。这些寄存器是保存为调用者的,即它们可能不会在函数调用之间保留,因为它们被调用者可能掩盖了它们(对于它们而言,更合适的术语是呼叫密集的寄存器)。

我们将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破坏寄存器rdirsirdxrcxr8和{{1 }}(以及其他)。这些寄存器包含调用r9的参数(即Pabcd和{{1 }}和它们的值仍然是第二次调用e所必需的。因此,寄存器fQrbxrbpr12r13用于在调用之前复制这些寄存器到r14中以保留原始参数r15的调用方式。

Q不允许破坏用于保存被调用方保存的寄存器的这些寄存器,因为它们是 caller-saved 寄存器,即,它们的值必须保留在函数调用(它们的另一个术语是调用保留的寄存器)。因此,函数P中的代码会在破坏寄存器之前使用Q将这些寄存器保存到堆栈中,并在控制流从{{1}返回之前用堆栈中的P将它们恢复。 }(pushpop主叫方)。 P的调用者将看到的净结果是这些寄存器的原始值与调用P之前的值相同。

请注意,第二次调用Q可能会使寄存器PPQrdirsi和{{ 1}}也是如此,但是没有必要保留它们的值,因为它们不再在rdx返回之后出现。


要更深入地了解寄存器破坏,请查看What registers are preserved through a linux x86-64 function call