main()有时会在x86上使用-fomit-frame-pointer保持帧指针

Joh*_*ing 6 c x86 assembly gcc x86-64

当我做作业时,我意外地在x86上发现了关于-fomit-frame-pointer和GCC的一些奇怪的事情.
看下面的代码(这看起来很废话但不知何故与我如何发现问题有关)

#include <stdio.h>

void foo(void);

int main()
{
    foo();
    return 0;
}

void foo()
{
    printf("0x%x\n", *(unsigned char *)main);
}
Run Code Online (Sandbox Code Playgroud)

当使用-m64 -O1flag(-fomit-frame-pointer enabled)编译时,反汇编如下所示

0000000000400500 <foo>:
  400500:   48 83 ec 08             sub    $0x8,%rsp
  400504:   0f b6 35 14 00 00 00    movzbl 0x14(%rip),%esi        # 40051f <main>
  40050b:   bf c4 05 40 00          mov    $0x4005c4,%edi
  400510:   b8 00 00 00 00          mov    $0x0,%eax
  400515:   e8 c6 fe ff ff          callq  4003e0 <printf@plt>
  40051a:   48 83 c4 08             add    $0x8,%rsp
  40051e:   c3                      retq   

000000000040051f <main>:
  40051f:   48 83 ec 08             sub    $0x8,%rsp
  400523:   e8 d8 ff ff ff          callq  400500 <foo>
  400528:   b8 00 00 00 00          mov    $0x0,%eax
  40052d:   48 83 c4 08             add    $0x8,%rsp
  400531:   c3                      retq 
Run Code Online (Sandbox Code Playgroud)

一切看起来都很好,因为%rbp根本没有出现.但是,当使用-m32 -O1flag 编译代码时(从gcc 4.6 -fomit-frame-pointer开始变为默认值并且我的是GCC 4.8.2)或甚至-fomit-frame-pointer明确使用,反汇编如下所示.

08048400 <foo>:
 8048400:   83 ec 1c                sub    $0x1c,%esp
 8048403:   0f b6 05 1e 84 04 08    movzbl 0x804841e,%eax
 804840a:   89 44 24 04             mov    %eax,0x4(%esp)
 804840e:   c7 04 24 c0 84 04 08    movl   $0x80484c0,(%esp)
 8048415:   e8 b6 fe ff ff          call   80482d0 <printf@plt>
 804841a:   83 c4 1c                add    $0x1c,%esp
 804841d:   c3                      ret    

0804841e <main>:
 804841e:   55                      push   %ebp
 804841f:   89 e5                   mov    %esp,%ebp
 8048421:   83 e4 f0                and    $0xfffffff0,%esp
 8048424:   e8 d7 ff ff ff          call   8048400 <foo>
 8048429:   b8 00 00 00 00          mov    $0x0,%eax
 804842e:   c9                      leave  
 804842f:   c3                      ret    
Run Code Online (Sandbox Code Playgroud)

该功能foo在32位和64位中看起来完全相同.但是,与64位的不同,main的前两个指令是(注意它是用-fomit-frame-pointer编译的):

push %ebp
mov %esp, %ebp
Run Code Online (Sandbox Code Playgroud)

它类似于普通的x86代码.
经过几次实验,我发现如果main调用另一个函数,代码将如上所述,如果没有函数调用main,代码将类似于64位代码.

我知道这个问题可能看起来很奇怪,但我只是好奇为什么x86和x86_64代码之间存在这种差异,并且只存在于main()函数中.

gsg*_*gsg 6

-fomit-frame-pointer据我所知,这是无关的- 相反,它是堆栈对齐的结果.

main需要对齐堆栈(with and $0xfffffff0, %esp),以便它调用的函数获得它们期望的对齐.这破坏了旧的价值esp,因此必须保存和恢复,以便ret做正确的事.(ret执行时,esp必须指向进入时的相同位置main:即,在堆栈上保存的返回地址).

所以esp必须保存和恢复:为什么不到一个被调用者保存寄存器,如ebp?事实上ebp是一个很好的选择,因为有一个专门的指令,在最后leave执行所需movl %ebp, %esp/popl %ebpmain.

在x64上,可以预期堆栈在进入时对齐main,因此esp不需要对齐以及由此产生的保存和恢复.