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字节?是猜测吗?程序是否预先解析?
编译器之所以知道,是因为它查看了源代码(或者实际上是解析后逻辑的内部表示),并将必须为其分配堆栈空间的所有内容所需的总大小相加。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 。
| 归档时间: |
|
| 查看次数: |
843 次 |
| 最近记录: |