为什么传递参数时栈上有空洞?

Why there are holes on the stack when passing parameters?

我对汇编代码不是很熟悉。对不起,如果这个问题很幼稚。

我有一个简单的 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   [=11=]x3,-0x8(%rbp) ; c = 3
  1f:   c7 45 fc 04 00 00 00    movl   [=11=]x4,-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   

主要():

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

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

我的问题是:

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

传入 x86-64 System V ABI 的 Arg 在堆栈上使用 8 字节 "slots",用于不适合寄存器的 args。任何不是 8 字节倍数的东西都会在下一个堆栈 arg 之前有空洞(填充)。

这是跨操作系统/架构调用约定的非常标准。在 32 位调用约定中传递 short 将使用一个 4 字节堆栈槽(或占用整个 4 字节寄存器,无论它是否被符号扩展到整个寄存器宽度)。


你最后两个问题问的是同一件事:

您正在未经优化的情况下进行编译,因此为了进行一致的调试,包括函数 args 在内的每个变量都需要一个内存地址,调试器可以在断点处停止时修改该地址的值。这包括 mainargcargv,以及 f1.

的寄存器参数

如果您将 main 定义为 int main(void)(这是托管 C 实现中 main 的两个有效签名之一,另一个是 int main(int argc, char**argv)),则d 没有传入的参数让 main 溢出。


如果您在启用优化的情况下编译,就会有 none 的废话 。见 for suggestions on how to get compilers to make asm that's nice to look at. e.g. from the Godbolt compiler explorer,用gcc -O3 -fPIC1编译,得到:

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    , %eax        #, tmp105       # c+d = constant 7
    ret     

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

IDK 究竟为什么 gcc 保留比实际需要更多的堆栈 space;即使启用了优化,有时也会发生这种情况。例如gcc 的 main 看起来像这样:

# gcc -O3
main:
    subq    , %rsp    # useless; the space isn't used and it doesn't change stack alignment.
    movl    , %r9d
    movl    , %r8d
    movl    , %ecx
    pushq   
    movl    , %edx
    movl    , %esi
    movl    , %edi
    pushq   
    pushq   
    call    f1@PLT
    xorl    %eax, %eax    # implicit return 0
    addq    , %rsp
    ret

在您的函数版本中发生的所有额外的废话都是一致调试所需的反优化的结果,您可以使用默认值 -O0.(一致的调试意味着你可以 set 在断点处停止的变量,甚至 jump 到同一函数内的另一个源代码行,程序仍然 运行 并且可以工作正如您在 C 抽象机中所期望的那样。因此,编译器无法跨语句将任何内容保存在寄存器中,也无法根据语句中文字常量以外的任何内容进行优化。)

-O0 也意味着编译速度快,不要试图有效地分配堆栈 space。


脚注 1:-fPIC 阻止 gcc 优化掉 main.

中的调用

没有它,即使使用 __attribute__((noinline)),它也可以看到该函数没有副作用,因此它可以省略调用而不是内联它并对其进行优化。

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

clang 显然假设即使使用 -fPIC 它仍然可以优化这种方式,所以我猜 clang 假设不允许对同一函数的冲突定义或其他什么?这似乎会破坏 LD_PRELOAD 覆盖库函数以从库内调用。