当执行通过执行调用进入一个新函数时,我经常看到这个代码模板(在调试模式下由 Gnu Debugger 生成的 asm 列表):
0x00401170 push %ebp
0x00401171 mov %esp,%ebp
0x00401173 pop %ebp
那么将esp迁移到ebp的目的是什么?
当执行通过执行调用进入一个新函数时,我经常看到这个代码模板(在调试模式下由 Gnu Debugger 生成的 asm 列表):
0x00401170 push %ebp
0x00401171 mov %esp,%ebp
0x00401173 pop %ebp
那么将esp迁移到ebp的目的是什么?
移动esp
到ebp
辅助调试,并在某些情况下,异常处理完成。 ebp
通常称为帧指针。考虑到这一点,想想如果你调用多个函数会发生什么。 ebp
指向您推送旧的内存块ebp
,它本身指向另一个保存的ebp
,等等。因此,您有一个堆栈帧的链接列表。从这些中,您可以查看返回地址(它们始终位于堆栈帧中帧指针上方的 4 个字节)以找出哪行代码称为有问题的堆栈帧。指令指针可以告诉你当前执行的位置。这允许您生成堆栈跟踪,通过显示整个程序的执行流程,这对调试很有用。
作为一个实际示例,请考虑以下代码:
void foo();
void bar();
void baz();
void quux();
void foo() {
bar();
}
void bar() {
baz();
quux();
}
void baz() {
//do nothing
}
void quux() {
*(int*)(0) = 1; //SEGFAULT!
}
int main() {
foo();
return 0;
}
这将生成以下程序集(使用 Debian gcc 4.7.2-4 gcc -m32 -g test.c
,已剪断):
080483dc <foo>:
80483dc: 55 push %ebp
80483dd: 89 e5 mov %esp,%ebp
80483df: 83 ec 08 sub $0x8,%esp
80483e2: e8 02 00 00 00 call 80483e9 <bar>
80483e7: c9 leave
80483e8: c3 ret
080483e9 <bar>:
80483e9: 55 push %ebp
80483ea: 89 e5 mov %esp,%ebp
80483ec: 83 ec 08 sub $0x8,%esp
80483ef: e8 07 00 00 00 call 80483fb <baz>
80483f4: e8 07 00 00 00 call 8048400 <quux>
80483f9: c9 leave
80483fa: c3 ret
080483fb <baz>:
80483fb: 55 push %ebp
80483fc: 89 e5 mov %esp,%ebp
80483fe: 5d pop %ebp
80483ff: c3 ret
08048400 <quux>:
8048400: 55 push %ebp
8048401: 89 e5 mov %esp,%ebp
8048403: b8 00 00 00 00 mov $0x0,%eax
8048408: c7 00 01 00 00 00 movl $0x1,(%eax)
804840e: 5d pop %ebp
804840f: c3 ret
08048410 <main>:
8048410: 55 push %ebp
8048411: 89 e5 mov %esp,%ebp
8048413: 83 e4 f0 and $0xfffffff0,%esp
8048416: e8 c1 ff ff ff call 80483dc <foo>
804841b: b8 00 00 00 00 mov $0x0,%eax
8048420: c9 leave
8048421: c3 ret
请注意,leave
这与以下内容相同:
mov %ebp, %esp
pop %ebp
考虑到这一点,以及 x86 上的标准 C 调用约定,我们知道段错误处的堆栈将如下所示:
0x0804841b
返回地址为 call foo
1.
0x080483e7
返回地址为 call bar
4.
0x080483f9
返回地址为 call quux
7.
指令指针将为0x08048408
。ebp
将指向10.
.
此时,处理器产生一个异常,由操作系统处理。然后它发送SIGSEGV
到进程,该进程乐于终止并转储核心。然后你用 调出 gdb 中的核心转储gdb -c core
,你输入file a.out
and bt
,它给你回应:
#0 0x08048408 in quux () at test.c:20
#1 0x080483f9 in bar () at test.c:12
#2 0x080483e7 in foo () at test.c:7
#3 0x0804841b in main () at test.c:24
#0
由指令指针生成。然后,它转到ebp
(10),查看堆栈 (9) 上的前一项,并生成#1
。它遵循ebp
(即mov %ebp, (%ebp)
)到(7),并且看起来比(6)高4个字节以生成#2
. 它最终遵循 (7) 到 (4) 并查看 (3) 以生成#3
。
注意: 这只是进行这种堆栈跟踪的一种方式。GDB 非常非常聪明,即使您使用-fomit-frame-pointer
. 但是,在非常基本的实现中,这可能是生成堆栈跟踪的最简单方法。
我喜欢罗伯特的解释,它有一个很好的例子,但是..我认为它没有抓住这个指令的真正目的。
是作为调试帮助完成的,在某些情况下用于异常处理
嗯.. 不是真的,不仅如此。它是 x86(32 位)标准函数序言的一部分,它是设置函数堆栈帧的(常见)技术,以便参数和局部变量可以作为 的固定偏移量访问ebp
,毕竟, *B*base 框架 *P*ointer。
使ebp
等于esp
在函数入口处,您将在堆栈内有一个固定的相对指针,该指针在您的函数的生命周期内不会改变,并且您将能够以(固定)正和(固定)负偏移量访问参数和局部变量,分别为ebp
。
您可以或不能在发布的优化代码中看到这个标准的序言:优化器可以(并且经常做)FPO(帧指针优化)来摆脱ebp
并仅esp
在您的函数内部使用来访问参数和局部变量。这要棘手得多(我不会手动完成),因为esp
它会在函数生命周期内发生变化,因此,例如,可以在代码中的两个不同点使用 2 个不同的偏移量访问参数。