当执行通过执行调用进入一个新函数时,我经常看到此代码模板(在调试模式下,由Gnu Debugger生成的asm列表):

0x00401170  push   %ebp
0x00401171  mov    %esp,%ebp
0x00401173  pop    %ebp


所以目的是什么esp移到ebp的方法?

评论

邓诺(Dunno),这里是否有这些话题?无论如何请看这里:stackoverflow.com/questions/2515598/push-ebp-movlesp-ebp

我认为是的,尽管非常基础,因为拆卸问题是主题。此外,还有理由使用与逆向工程(例如调试,运行时代码分析)更相关的框架指针(例如易于实现的alloca(),易于记住偏移量)

#1 楼

esp移入ebp是为了提供调试帮助,在某些情况下还用于异常处理。 ebp通常被称为帧指针。考虑到这一点,请考虑一下调用多个函数会发生什么。 ebp指向您将旧的ebp推入的内存块,旧的ebp本身又指向另一个已保存的gcc -m32 -g test.c,依此类推。这样,您就有了堆栈帧的链表。从这些中,您可以查看返回地址(在堆栈帧中,它们总是比帧指针高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 leave,已片段化):

080483dc <foo>:
 80483dc:   55                      push   %ebp
 80483dd:   89 e5                   mov    %esp,%ebp
 80483df:   83 ec 08                sub    
mov %ebp, %esp
pop %ebp
x8,%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
#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
x8,%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 q4312078qx0,%eax 8048408: c7 00 01 00 00 00 movl q4312078qx1,(%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 q4312078qxfffffff0,%esp 8048416: e8 c1 ff ff ff call 80483dc <foo> 804841b: b8 00 00 00 00 mov q4312078qx0,%eax 8048420: c9 leave 8048421: c3 ret


考虑到这一点,以及x86上的标准C调用约定,我们知道segfault上的堆栈看起来像:


主计算机堆栈框架的顶部
主计算机的堆栈空间-在这种情况下,足以对齐16个字节

0x0804841b call foo的返回地址

指向1.的指针
foo的堆栈空间

0x080483e7指向call bar的返回地址

指向4.的指针
bar的堆栈空间

0x080483f9返回call quux的地址

指向7.的指针
quux的堆栈空间
指令指针将为0x08048408ebp将指向10.

此时,处理器会生成一个异常,操作系统会对其进行处理。然后,它将SIGSEGV发送到该进程,该进程必须终止并转储核心。然后,您使用gdb -c core在gdb中启动核心转储,然后键入file a.outbt,它会给您响应:
指针。然后,转到#0(10),查看堆栈(9)上的前一项,并生成ebp。它跟随#1(即ebp)至(7),并在(6)之上查找4个字节以生成mov %ebp, (%ebp)。最后,它遵循(7)至(4)并查看(3)来生成#2

注意:这只是进行此类堆栈跟踪的一种方法。 GDB非常非常聪明,即使使用#3,也可以执行堆栈跟踪。但是,在非常基本的实现中,这可能是生成堆栈跟踪的最简单方法。

评论


为什么不首先使用gcc -S并获得带注释的代码?

– 0xC0000022L♦
13年5月22日在16:37

我想拥有真实的内存地址,使其与实时程序中的堆栈跟踪相匹配。我也认为objdump / gdb示例更适合rev-eng主题。

–罗伯特·梅森
13年5月22日在17:36

#2 楼

我喜欢罗伯特的解释,它有一个很好的例子,但是..我认为它没有抓住要点,这才是本指令的真正目的。


是作为调试辅助工具,在某些情况下还可以用于异常处理。



..不是真的,不仅如此。它是x86(32位)标准函数序言的一部分,并且是(通用)设置函数栈帧的技术,因此,参数和局部变量可以作为ebp的固定偏移量进行访问,毕竟, * B * ase帧* P * ointer。

在函数入口处使ebp等于esp,您将在堆栈内部拥有一个固定的相对指针,在该函数的生命周期内不会改变,您将能够分别以ebp(作为(固定)正和(固定)负偏移量)访问参数和局部变量。

您可以或不能在发行版优化代码中看到此标准序言:优化程序可以(并且经常这样做)使用FPO(帧指针优化)来摆脱ebp,而只需在函数内部使用esp即可访问参数和局部变量。这非常棘手(我不会手动完成),因为esp在函数生存期内可能会发生变化,因此例如可以在代码的两个不同点使用2个不同的偏移量来访问参数。

评论


的确如此,但是在大量已编译的代码中,堆栈指针在函数的持续时间内不会发生变化,因为编译器只执行一次子操作,并使用固定偏移量进行推/弹出操作。出于反向工程的目的,我更感兴趣的是ebp可以告诉我有关程序结构的信息,而不是它如何使程序员的工作变得更轻松(尤其是在编译代码中)。

–罗伯特·梅森
13年5月26日在20:59

话虽这么说,感谢您填补我在解释中留下的(而不是巨大的)漏洞。

–罗伯特·梅森
13年5月26日21:00