程序如何知道要为堆栈上的局部变量分配多少space?
How do programs know how much space to allocate for local variables on the stack?
在这个简单的函数中,space分配给局部变量。然后,初始化变量并调用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)
让我困惑的是这一行sub rsp,0x10
。程序如何知道分配 0x10 字节?是猜测吗?程序是否事先解析过?
编译器知道是因为它查看了源代码(或者实际上是解析后的逻辑内部表示),并将必须分配堆栈的所有内容所需的总大小加起来 space为了。而且它还必须在 call
之前让 RSP 16 字节对齐,因为函数入口上的 RSP % 16 == 8。
因此,对齐是编译器保留的功能可能比实际使用的功能多的原因之一,而且编译器未优化的错误也会使其浪费 space:common for GCC to waste an extra 16 bytes,尽管这里没有发生这种情况。
是的,现代编译器在为它发出任何代码之前解析整个函数(实际上是整个源文件)。这就是提前优化编译器的意义所在,因此它是围绕着这样做而设计的,即使您进行调试构建也是如此。相比之下,Tiny C 编译器 TCC 是一次性的,并在其函数序言中留下一个位置,以便稍后返回并在到达源代码中的函数底部后填充任何总大小。请参阅 - 当该数字恰好为零时,那里仍然有一个 sub esp, 0
。 (TCC 仅针对 32 位模式。)
相关:Function Prologue and Epilogue in C
在叶函数中,编译器可以在针对 x86-64 System V 时使用 RSP 下面的红色区域,避免保留尽可能多(或任何)堆栈的需要 space 即使有一些局部函数选择spill/reload。 (例如,任何未优化的代码。)另请参阅 Why is there no "sub rsp" instruction in this function prologue and why are function parameters stored at negative rbp offsets? 除了内核代码,或使用 -mno-red-zone
.
编译的其他代码
或者在 Windows x64 中,调用者需要为他们的被调用者保留阴影 space space 在他们的 return 地址之上。但对于非叶函数,这意味着保留至少 32 个字节的影子 space 加上任何用于对齐或局部变量。例如参见 [=22=]
在 x86-64 以外的 ISA 的标准调用约定中,其他规则可能会发挥影响。
请注意,在 64 位代码中,leave
弹出 RBP,而不是 EBP,ret
弹出 RIP,而不是 EIP。
此外,mov ecx,DWORD PTR [rbp-0x4]
不是 variable initialization
。 这是从未初始化的内存到寄存器的加载。可能你在没有初始化器的情况下做了类似 int a,b,c;
的操作,然后将它们作为参数传递给 printf.
在这个简单的函数中,space分配给局部变量。然后,初始化变量并调用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)
让我困惑的是这一行sub rsp,0x10
。程序如何知道分配 0x10 字节?是猜测吗?程序是否事先解析过?
编译器知道是因为它查看了源代码(或者实际上是解析后的逻辑内部表示),并将必须分配堆栈的所有内容所需的总大小加起来 space为了。而且它还必须在 call
之前让 RSP 16 字节对齐,因为函数入口上的 RSP % 16 == 8。
因此,对齐是编译器保留的功能可能比实际使用的功能多的原因之一,而且编译器未优化的错误也会使其浪费 space:common for GCC to waste an extra 16 bytes,尽管这里没有发生这种情况。
是的,现代编译器在为它发出任何代码之前解析整个函数(实际上是整个源文件)。这就是提前优化编译器的意义所在,因此它是围绕着这样做而设计的,即使您进行调试构建也是如此。相比之下,Tiny C 编译器 TCC 是一次性的,并在其函数序言中留下一个位置,以便稍后返回并在到达源代码中的函数底部后填充任何总大小。请参阅 sub esp, 0
。 (TCC 仅针对 32 位模式。)
相关:Function Prologue and Epilogue in C
在叶函数中,编译器可以在针对 x86-64 System V 时使用 RSP 下面的红色区域,避免保留尽可能多(或任何)堆栈的需要 space 即使有一些局部函数选择spill/reload。 (例如,任何未优化的代码。)另请参阅 Why is there no "sub rsp" instruction in this function prologue and why are function parameters stored at negative rbp offsets? 除了内核代码,或使用 -mno-red-zone
.
或者在 Windows x64 中,调用者需要为他们的被调用者保留阴影 space space 在他们的 return 地址之上。但对于非叶函数,这意味着保留至少 32 个字节的影子 space 加上任何用于对齐或局部变量。例如参见 [=22=]
在 x86-64 以外的 ISA 的标准调用约定中,其他规则可能会发挥影响。
请注意,在 64 位代码中,leave
弹出 RBP,而不是 EBP,ret
弹出 RIP,而不是 EIP。
此外,mov ecx,DWORD PTR [rbp-0x4]
不是 variable initialization
。 这是从未初始化的内存到寄存器的加载。可能你在没有初始化器的情况下做了类似 int a,b,c;
的操作,然后将它们作为参数传递给 printf.