在主要序言之前这些说明的目的是什么?

逆向工程 拆卸 x86 海湾合作委员会
2021-06-24 04:37:34

我在下面写了一个小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    $0xfffffff0,%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    $0x10,%esp
 804842d: 89 cb                 mov    %ecx,%ebx
....

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

2个回答

指令在做什么

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

即,

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

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

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

在过程中的这一点上,在寄存器中的存储器地址$esp0xffffd13c,这样4(%esp)= $esp+4= 0xffffd140

>>> x/x $esp+4
0xffffd140: 0x01

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


  1. 804841f: 83 e4 f0 and $0xfffffff0,%esp

接下来,在价值$esp0xffffd13c被相与0xfffffff0

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

这将产生0xffffd130存储在中的值$esp这相当于

0xffffd13c- 0x0c= 0xffffd130

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

and $0xfffffff0,%esp

作为

and $-16,%esp

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


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

由于lea 0x4(%esp),%ecx从较早的结果,中的值$ecx等于过去的值$esp+4(即0xffffd140)。因此,

-0x4(%ecx)= 0xffffd140- 4 = 0xffffd13c

这是价值$esp之初main()该值现在通过pushl指令保存在进程运行时堆栈中


概括:

 lea    0x4(%esp),%ecx         // load 0xffffd140 into $ecx
 and    $0xfffffff0,%esp       // subtract 0x0c (decimal 12) from $esp
 pushl  -0x4(%ecx)             // decrement $esp by 4, save 0xffffd13c on stack

这些说明的目的

在主要序言之前这些说明的目的是什么?

关于这些指令目的的一个线索是它们在传统函数序言之前执行的事实:

8048425: 55                    push   %ebp
8048426: 89 e5                 mov    %esp,%ebp

根据System V Application Binary Interface Intel386 Architecture Processor Supplement,第四版,函数序言执行后$ebp+4是返回地址在运行时堆栈上的位置。

SYS V ABI i386 补充 C 堆栈帧

$ebp+4指令保存在堆栈中的地址

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

0xffffd13c这是一个指向0xf7e12637,偏移量 247 的地址的指针__libc_start_main()

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

这表明 的返回地址main()在函数中__libc_start_main()

至于$ecx,该寄存器仅保存以下值argc

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

请注意,由于a从未使用过变量,因此编译器优化了对atoi.

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

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

自然地,下一个问题是“什么是__libc_start_main?” 根据Linux Standard Base PDA 规范 3.0RC1

__libc_start_main()函数应初始化进程,使用适当的参数调用主函数,并处理从main().

那么__libc_start_main()从哪里来呢?简短的回答是它是共享对象中的一个函数,/lib/i386-linux-gnu/libc-2.23.so它动态链接到可执行 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)

此外__libc_start_main(),作为__gmon_start__进程初始化一部分的函数也动态链接到可执行的 ELF 二进制文件:

$ readelf --dyn-syms [binary_name]

Symbol table '.dynsym' contains 5 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (2)
     2: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (3)
     4: 0804851c     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used

这是完整的图片,来自Linux x86 程序启动或 - 我们如何到达 main()?帕特里克霍根:

C进程初始化调用图

最后值得注意的是,如果返回地址main()0xf7e12637更仔细地检查,我们看到,这个地址所在的外text段以及运行栈。这个位于 中的地址__libc_start_main()实际上位于虚拟内存中的内存映射段中,如Gustavo Duarte 的文章Anatomy of a Program in Memory中的这张图所示

VM 中的 Linux 进程布局

这有什么作用?

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

lea    0x4(%esp),%ecx    # save address of arguments
and    $0xfffffff0,%esp  # align stack
pushl  -0x4(%ecx)        # move return address
...                      # continue normal preamble

同时,main(argcargv)的参数不会移动,因此指向它们的指针保存在%ecx.

回想一下进入时的堆栈布局main

%esp+8:  argv (a pointer to an array of pointers)
%esp+4:  argc (a 32-bit integer)
%esp+0:  return address (from call)

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

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

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

在您的代码中,您还可以看到%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!

为什么要完成这一切?

出于各种原因,现代处理器喜欢将数据对齐到 16 字节边界;某些操作可能会对性能造成重大影响,否则其他操作可能根本无法工作。

main只要注意在调用之前始终以 16 字节的倍数分配堆栈,调整堆栈帧一次就可以运行其余代码而无需进一步调整。这就是为什么你会经常看到这样的事情:

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

注意: x86-64 ABI 强制要求 16 字节堆栈对齐。顺便说一句,这意味着您不会main在 64 位代码中找到帧调整- 堆栈已经对齐。