我在下面编写了一个小型C程序:

#include <stdlib.h>
int sub(int x, int y){
  return 2*x+y;
}

int main(int argc, char ** argv){
  int a;
  a = atoi(argv[1]);
  return sub(argc,a);
}


用gcc 5.4.0编译,目标32位x86。我在拆卸时得到以下信息:

0804841b <main>:
 804841b: 8d 4c 24 04           lea    0x4(%esp),%ecx
 804841f: 83 e4 f0              and    q4312078qxfffffff0,%esp
 8048422: ff 71 fc              pushl  -0x4(%ecx)
 8048425: 55                    push   %ebp
 8048426: 89 e5                 mov    %esp,%ebp
 8048428: 53                    push   %ebx
 8048429: 51                    push   %ecx
 804842a: 83 ec 10              sub    q4312078qx10,%esp
 804842d: 89 cb                 mov    %ecx,%ebx
....


push %ebp之前的前三个说明是什么?我没有在较旧的gcc编译二进制文件中看到这些文件。

#1 楼

这些指令在做什么


push %ebp之前执行的前三个指令是什么?




 804841b: 8d 4c 24 04           lea    0x4(%esp),%ecx      <-  1
 804841f: 83 e4 f0              and    
>>> x/x $esp+4
0xffffd140: 0x01
xfffffff0,%esp <- 2 8048422: ff 71 fc pushl -0x4(%ecx) <- 3


这很容易看出是否使用gdb(或其他调试器)来单步执行代码。



804841b: 8d 4c 24 04 lea 0x4(%esp),%ecx


此时,在寄存器$esp中的内存地址为0xffffd13c,因此4(%esp) = $esp+4 = 0xffffd140

0xffffd13c:            11111111111111111101000100111100
0xfffffff0:       AND  11111111111111111111111111110000
                  -------------------------------------
                       11111111111111111101000100110000


这意味着lea指令将0x4(%esp)的有效地址0xffffd140装入$ecx中。




804841f: 83 e4 f0 and $espxfffffff0,%esp


Next ,则0xffffd13c中的值0xfffffff00xffffd130进行“与”运算:

 lea    0x4(%esp),%ecx         // load 0xffffd140 into $ecx
 and    
8048425: 55                    push   %ebp
8048426: 89 e5                 mov    %esp,%ebp
xfffffff0,%esp // subtract 0x0c (decimal 12) from $esp pushl -0x4(%ecx) // decrement $esp by 4, save 0xffffd13c on stack


这将得出值$esp,该值存储在0xffffd13c中。这等效于

0x0c-0xffffd130 = 0xfffffff0

这会在进程运行时堆栈上创建12个字节的空间。附带说明一下,值-16将表示为and and $-16,%espxfffffff0,%esp,因此我们可以将

8048422: ff 71 fc pushl -0x4(%ecx)

视为

lea 0x4(%esp),%ecx

这样做是为了使堆栈与16个字节的边界对齐,因为下一条指令(请参见3)将堆栈指针减4,然后将值保存到堆栈中。




$ecx


由于前面的$esp+40xffffd140中的值等于-0x4(%ecx)(即0xffffd140) )。结果,

0xffffd13c = $esp-4 = main()

这是pushl开头$ebp+4的值。现在,该值通过$ebp+4指令保存在进程运行时堆栈中。



摘要:

>>> x/x $ecx-4
0xffffd13c: 0xf7e12637
>>> x/x 0xf7e12637
0xf7e12637 <__libc_start_main+247>: 0x8310c483



这些说明的目的


这些说明的主要目的是什么?


有关这些指令目的的线索是它们在常规功能序言之前执行:

根据System V应用程序二进制接口Intel386体系结构处理器,功能序言8048422: ff 71 fc pushl -0x4(%ecx)执行后,第四版的补充内容是返回地址在运行时堆栈上的位置。



通过指令

0xffffd13c
保存在栈中的地址0xf7e12637__libc_start_main()。这是指向main()的指针,它是__libc_start_main()中的偏移量247的地址:

对于$ecx而言,该寄存器仅保存argc的值:

>>> x/x $ecx
0xffffd140: 0x00000001


请注意,由于从未使用变量a,因此编译器会进行优化拨出电话至atoi

因此,为了直接回答问题,在序言之前main()中的指令将参数传递给main()argc的值),并将main()的返回地址保存在运行时堆栈中。

C运行时环境和Linux进程剖析

自然,下一个问题是“ __libc_start_main是什么?”根据Linux标准基础PDA规范3.0RC1:


__libc_start_main()函数应初始化进程,使用适当的参数调用主函数,并处理main()的返回值。 br />
那么__libc_start_main()来自哪里?简短的答案是,它是共享对象/lib/i386-linux-gnu/libc-2.23.so中的一个函数,该对象动态链接到可执行的ELF二进制文件中:同样是过程初始化的一部分,它也动态链接到可执行的ELF二进制文件:

 $ ldd [binary_name]
    linux-gate.so.1 =>  (0xf7764000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7586000)
    /lib/ld-linux.so.2 (0x56640000)


这是完整的图片,来自Linux x86程序启动,或者-如何获得main()?通过Patrick Horgan:



最后一点,如果仔细检查__libc_start_main()__gmon_start__的返回地址,我们会发现该地址位于main()段之外以及运行时堆栈。该地址位于0xf7e12637中,实际上位于虚拟内存的内存映射段中,如Gustavo Duarte的文章《内存中的程序剖析》中的示意图所示:



#2 楼



这三个语句用于将main的堆栈帧(从其返回地址开始)移动到下一个16字节对齐的地址。

lea    0x4(%esp),%ecx    # save address of arguments
and    
%esp+8:  argv (a pointer to an array of pointers)
%esp+4:  argc (a 32-bit integer)
%esp+0:  return address (from call)
xfffffff0,%esp # align stack pushl -0x4(%ecx) # move return address ... # continue normal preamble


同时,不会移动mainargcargv)的参数,因此将指向它们的指针保存在%ecx中。输入main

%ecx+4:  argv pointer
%ecx+0:  argc
%ecx-4:  original return address
         ...
%esp+4:  copy of return address
%esp+0:  saved base pointer


参数位于返回地址的正上方,因此在调整堆栈指针之前将%esp+4保存到%ecx
接下来,也请%ecx用作我们定位原始返回地址-4(%ecx)的指针,我们将其推入新的堆栈框架。

在其余序言之后,堆栈将如下所示:

...
mov    -0x8(%ebp),%ecx   # load pointer to argc
leave                    # unwind stack frame, pop %ebp
lea    -0x4(%ecx),%esp   # restore original stack pointer
ret                      # jump out, using the original return address!


在您的代码中,您还可以看到%ecx在前导之后被压入堆栈(即保存为局部变量);它将在该函数的结尾处从那里恢复,如下所示:

sub    q4312078qxc,%esp    # pad stack by 12 bytes
push   %eax         # push 4-byte argument
call   puts


为什么这一切都做完了?

由于各种原因,诸如数据对齐到16字节边界的现代处理器;某些操作可能会严重影响性能,否则其他操作可能根本无法工作。

一次调整main堆栈框架可以使其余代码无需进一步调整即可运行,只要注意始终分配调用前以16字节的倍数堆叠。这就是为什么您经常会看到以下内容的原因:

q4312078q

main上找到帧调整-堆栈已对齐。

评论


欢迎!根据您到目前为止所写的内容,我希望阅读您以后的文章。

– julian♦
18年8月3日在23:30

谢谢!我来到这里进行查询,然后感到,虽然您的回答很详尽,但它缺少一些细节。由于作为新用户我无法发表评论,所以我自己拍摄了一张照片。希望你不要介意! :)

– Pesco
18年8月4日在13:14

我喜欢这个比@SYS_V的答案要好(对SYS_V没有冒犯)。我不相信SYS_V的回答地址“目的是什么”。它在解释指令的作用方面做得很好。答案似乎非常非常简单。除了明显的对齐优化之外,“同时,main的参数(argc和argv)没有移动,因此指向它们的指针保存在%ecx中。”美丽。万分感谢。

–埃文·卡洛尔(Evan Carroll)
18年11月6日在22:02