程序如何知道在堆栈上为局部变量分配多少空间?

Hap*_*rry 0 x86 assembly x86-64 stack-frame

在这个简单的函数中,为局部变量分配了空间。然后,变量被初始化并被printf调用以输出它们。

000000000040056a <func>:
  40056a:       55                      push   rbp                     ; function prologue
  40056b:       48 89 e5                mov    rbp,rsp                 ; function prologue
  40056e:       48 83 ec 10             sub    rsp,0x10                ; deallocating space for local variables
  400572:       8b 4d fc                mov    ecx,DWORD PTR [rbp-0x4] ; variable initialization
  400575:       8b 55 f8                mov    edx,DWORD PTR [rbp-0x8] ; variable initialization
  400578:       8b 45 f4                mov    eax,DWORD PTR [rbp-0xc] ; variable initialization
  40057b:       89 c6                   mov    esi,eax                 ; string stuff
  40057d:       bf 34 06 40 00          mov    edi,0x400634            ; string stuff
  400582:       b8 00 00 00 00          mov    eax,0x0                 ; return value 
  400587:       e8 84 fe ff ff          call   400410 <printf@plt>     ; printf()
  40058c:       c9                      leave                          ; clean up local variables, pops ebp
  40058d:       c3                      ret                            ; return to the address that was pushed onto the stack (by popping it into eip)
Run Code Online (Sandbox Code Playgroud)

让我困惑的是这一行sub rsp,0x10。程序如何知道分配0x10字节?是猜测吗?程序是否预先解析?

Pet*_*des 5

编译器之所以知道,是因为它查看了源代码(或者实际上是解析后逻辑的内部表示),并将必须为其分配堆栈空间的所有内容所需的总大小相加。call考虑到函数入口处的 RSP % 16 == 8,它还必须在 之前获得 RSP 16 字节对齐。

因此,对齐是编译器保留的空间可能多于函数实际使用的原因之一,而且编译器错过优化的错误也可能导致空间浪费:GCC 浪费额外的 16 个字节是很常见的,尽管这里没有发生这种情况。

是的,现代编译器在为其发出任何代码之前解析整个函数(实际上是整个源文件)。这就是提前优化编译器的要点,因此即使您进行调试构建,它也是围绕这样做而设计的。相比之下,TCC(Tiny C 编译器)是一次性的,并在其函数序言中留下一个位置,以便稍后在到达源代码中的函数底部后返回并填写任何总大小。请参阅Tiny C 编译器生成的代码发出额外的(不必要的?)NOP 和 JMP - 当该数字恰好为零时,仍然存在sub esp, 0。(TCC 仅针对 32 位模式。)

相关:C 中的函数序言和结尾


在叶函数中,编译器在针对 x86-64 System V 时可以使用 RSP 下面的红色区域,从而避免需要保留尽可能多(或任何)堆栈空间,即使它们选择溢出/重新加载一些本地变量。(例如,未优化代码中的任何内容。)另请参阅为什么此函数序言中没有“sub rsp”指令以及为什么函数参数存储在负 rbp 偏移处?除了内核代码,或使用-mno-red-zone.

或者在 Windows x64 中,调用者需要保留影子空间供其被调用者使用,这也使小函数有机会不花费任何指令来移动 RSP,而只使用其返回地址上方的影子空间。但对于非叶函数,这意味着保留至少 32 个字节的影子空间以及用于对齐或局部变量的任何空间。例如参见阴影空间示例

在 x86-64 以外的 ISA 的标准调用约定中,其他规则可能会发挥作用并影响事物。


请注意,在 64 位代码中,leave弹出的是 RBP,而不是 EBP,并且ret弹出的是 RIP,而不是 EIP。

而且,mov ecx,DWORD PTR [rbp-0x4]也不是variable initialization这是从未初始化的内存到寄存器的加载。可能您做了类似int a,b,c;没有初始化程序的操作,然后将它们作为参数传递给 printf 。