传递参数时,为什么堆栈上有孔?

时间:2018-09-28 03:08:41

标签: assembly x86-64 disassembly calling-convention

我对汇编代码不太熟悉。如果这个问题很天真,请原谅。

我有一个简单的C程序:

int f1(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)
{
  int c = 3;
  int d = 4;
  return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + c + d;
}

int main(int argc, char** argv)
{
  f1(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

我将其编译为 elf64-x86-64 ,并获得以下反汇编代码:

f1():

0000000000000000 <f1>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)      ; 1
   7:   89 75 e8                mov    %esi,-0x18(%rbp)       ; 2
   a:   89 55 e4                mov    %edx,-0x1c(%rbp)      ; 3
   d:   89 4d e0                mov    %ecx,-0x20(%rbp)      ; 4
  10:   44 89 45 dc             mov    %r8d,-0x24(%rbp)  ; 5
  14:   44 89 4d d8             mov    %r9d,-0x28(%rbp)  ; 6
  18:   c7 45 f8 03 00 00 00    movl   $0x3,-0x8(%rbp) ; c = 3
  1f:   c7 45 fc 04 00 00 00    movl   $0x4,-0x4(%rbp) ; d = 4
  26:   8b 45 e8                mov    -0x18(%rbp),%eax     ;2
  29:   8b 55 ec                mov    -0x14(%rbp),%edx    ; 1
  2c:   01 c2                   add    %eax,%edx                
  2e:   8b 45 e4                mov    -0x1c(%rbp),%eax     ;3
  31:   01 c2                   add    %eax,%edx
  33:   8b 45 e0                mov    -0x20(%rbp),%eax     ;4
  36:   01 c2                   add    %eax,%edx
  38:   8b 45 dc                mov    -0x24(%rbp),%eax     ;5
  3b:   01 c2                   add    %eax,%edx
  3d:   8b 45 d8                mov    -0x28(%rbp),%eax    ; 6
  40:   01 c2                   add    %eax,%edx
  42:   8b 45 10                mov    0x10(%rbp),%eax     ;7
  45:   01 c2                   add    %eax,%edx
  47:   8b 45 18                mov    0x18(%rbp),%eax    ; 8
  4a:   01 c2                   add    %eax,%edx
  4c:   8b 45 20                mov    0x20(%rbp),%eax    ; 9
  4f:   01 c2                   add    %eax,%edx
  51:   8b 45 f8                mov    -0x8(%rbp),%eax    ; c =3
  54:   01 c2                   add    %eax,%edx
  56:   8b 45 fc                mov    -0x4(%rbp),%eax    ; d =4
  59:   01 d0                   add    %edx,%eax
  5b:   5d                      pop    %rbp
  5c:   c3                      retq   

main():

000000000000005d <main>:
  5d:   55                      push   %rbp
  5e:   48 89 e5                mov    %rsp,%rbp
  61:   48 83 ec 30             sub    $0x30,%rsp
  65:   89 7d fc                mov    %edi,-0x4(%rbp)
  68:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  6c:   c7 44 24 10 09 00 00    movl   $0x9,0x10(%rsp)
  73:   00 
  74:   c7 44 24 08 08 00 00    movl   $0x8,0x8(%rsp)
  7b:   00 
  7c:   c7 04 24 07 00 00 00    movl   $0x7,(%rsp)
  83:   41 b9 06 00 00 00       mov    $0x6,%r9d
  89:   41 b8 05 00 00 00       mov    $0x5,%r8d
  8f:   b9 04 00 00 00          mov    $0x4,%ecx
  94:   ba 03 00 00 00          mov    $0x3,%edx
  99:   be 02 00 00 00          mov    $0x2,%esi
  9e:   bf 01 00 00 00          mov    $0x1,%edi
  a3:   b8 00 00 00 00          mov    $0x0,%eax
  a8:   e8 00 00 00 00          callq  ad <main+0x50>
  ad:   c9                      leaveq 
  ae:   c3                      retq   

main()f1()传递参数时,堆栈上似乎有一些

enter image description here

我的问题是:

  • 为什么需要这些孔?

  • 为什么我们需要下面两行组装?如果它们是用于上下文还原的,那么我看不到任何说明。 %rsi寄存器甚至没有在其他地方使用。为什么仍将%rsi保存在堆栈中?

65: 89 7d fc mov %edi,-0x4(%rbp) 68: 48 89 75 f0 mov %rsi,-0x10(%rbp)

  • 又提出了一个问题,由于参数1 ~ 6已通过寄存器传递,为什么将它们移回内存f1()的开头?

1 个答案:

答案 0 :(得分:2)

在x86-64系统V中传递的arg ABI在堆栈中使用8字节的“插槽”,用于不适合寄存器的arg。不是8字节倍数的任何内容在下一个堆栈arg之前都会有孔(填充)。

这是跨OS /体系结构调用约定的相当标准。在32位调用约定中传递short将使用4字节的堆栈插槽(或占用整个4字节的寄存器,无论是否将其符号扩展为整个寄存器的宽度)。


您的最后2个问题确实在问同一件事:

您在编译时没有优化,因此为了进行一致的调试,包括函数args在内的每个变量都需要一个内存地址,调试器可以在该内存地址在断点处停止修改该值。这包括main的{​​{1}}和argc,以及argv的寄存器args。

如果您将f1定义为main(这是托管C实现中int main(void)的两个有效签名之一,另一个是main),则会有没有传入的args可以使main溢出。


如果启用了优化功能进行编译,则不会有任何废话。请参阅How to remove "noise" from GCC/clang assembly output?,以获取有关如何使编译器制作asm的建议,这很好看。例如从the Godbolt compiler explorer,用int main(int argc, char**argv) 1 编译,您将得到:

gcc -O3 -fPIC

(我使用AT&T语法而不是Intel,因为您在问题中使用了它)

IDK正是为什么gcc保留了比实际需要更多的堆栈空间;即使启用优化,有时也会发生这种情况。例如gcc的f1: addl %esi, %edi # a2, tmp106 # tmp106 = a1 + a2 movl 8(%rsp), %eax # a7, tmp110 addl %edx, %edi # a3, tmp107 addl %ecx, %edi # a4, tmp108 addl %r8d, %edi # a5, tmp109 addl %r9d, %edi # a6, tmp110 addl %edi, %eax # tmp110, tmp110 addl 16(%rsp), %eax # a8, tmp112 addl 24(%rsp), %eax # a9, tmp113 addl $7, %eax #, tmp105 # c+d = constant 7 ret 看起来像这样:

main

在您使用的功能版本中,所有多余的废话都是由于一致调试所需的反优化结果而产生的,而默认情况下,# gcc -O3 main: subq $16, %rsp # useless; the space isn't used and it doesn't change stack alignment. movl $6, %r9d movl $5, %r8d movl $4, %ecx pushq $9 movl $3, %edx movl $2, %esi movl $1, %edi pushq $8 pushq $7 call f1@PLT xorl %eax, %eax # implicit return 0 addq $40, %rsp ret 可以使调试得到优化。(一致调试意味着您可以-O0在断点处停止时的变量,甚至可以set到同一函数内的另一个源代码行,该程序仍将按照您在C抽象中的预期运行和运行这样一来,编译器就无法在语句之间的寄存器中保留任何内容,也不能基于语句内文字常量以外的任何内容进行优化。)

jump也意味着快速编译,不要试图有效地分配堆栈空间。


脚注1:-O0阻止gcc优化-fPIC中的呼叫。

否则,即使使用main,它也可以看到该函数没有副作用,因此它可以忽略该调用,而不是对其进行内联和优化。

但是__attribute__((noinline))意味着为共享库生成代码,(当针对Linux时)意味着可以插入符号,因此编译器无法假设-fPIC会实际调用 this call f1@plt的定义,因此无法基于它进行优化而没有副作用。

clang显然假设即使使用f1,它仍然可以以这种方式进行优化,因此我猜clang假设相同功能的冲突定义是不允许的或者是什么?对于从库中进行的调用,这似乎破坏了库函数的LD_PRELOAD覆盖。