首页 > 解决方案 > 为什么传参数时栈上有洞?

问题描述

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

我有一个简单的 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   

主要的():

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   

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

在此处输入图像描述

我的问题是:

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

标签: assemblyx86-64disassemblycalling-convention

解决方案


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

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


您的最后两个问题实际上是在问同样的事情:

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

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


如果您在启用优化的情况下进行编译,则不会有任何废话。请参阅如何从 GCC/clang 程序集输出中删除“噪音”?有关如何让编译器生成好看的 asm 的建议。例如,从Godbolt 编译器资源管理器,用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    $7, %eax        #, tmp105       # c+d = constant 7
    ret     

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

IDK 正是为什么 gcc 保留了比实际需要更多的堆栈空间;即使启用了优化,有时也会发生这种情况。例如 gcc 的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在断点处停止变量,甚至jump可以在同一函数内的另一个源代码行,并且程序仍将按照您在 C 抽象机中的预期运行和工作。所以编译器不能跨语句保留寄存器中的任何内容,或基于语句中的文字常量以外的任何内容进行优化。)

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


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

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

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

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


推荐阅读