指令在做什么
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
(或其他一些调试器)来单步调试代码。
804841b: 8d 4c 24 04 lea 0x4(%esp),%ecx
在过程中的这一点上,在寄存器中的存储器地址$esp
是0xffffd13c
,这样4(%esp)
= $esp+4
= 0xffffd140
:
>>> x/x $esp+4
0xffffd140: 0x01
这意味着lea
指令装载的有效地址0x4(%esp)
,0xffffd140
入$ecx
。
804841f: 83 e4 f0 and $0xfffffff0,%esp
接下来,在价值$esp
,0xffffd13c
被相与0xfffffff0
:
0xffffd13c: 11111111111111111101000100111100
0xfffffff0: AND 11111111111111111111111111110000
-------------------------------------
11111111111111111101000100110000
这将产生0xffffd130
存储在中的值$esp
。这相当于
0xffffd13c
- 0x0c
= 0xffffd130
。
这会在进程运行时堆栈上创建 12 个字节的空间。附带说明一下,值 -16 将表示为0xfffffff0
,因此我们可以想到
and $0xfffffff0,%esp
作为
and $-16,%esp
这样做是为了使堆栈与 16 字节边界对齐,因为下一条指令(参见 3)将堆栈指针递减 4,然后将一个值保存到堆栈中。
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
是返回地址在运行时堆栈上的位置。
$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()?帕特里克霍根:
最后值得注意的是,如果返回地址main()
的0xf7e12637
更仔细地检查,我们看到,这个地址所在的外text
段以及运行栈。这个位于 中的地址__libc_start_main()
实际上位于虚拟内存中的内存映射段中,如Gustavo Duarte 的文章Anatomy of a Program in Memory中的这张图所示: