mov %esp,%ebp 的目的是什么?

逆向工程 拆卸
2021-06-17 02:46:34

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

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

那么将esp迁移到ebp的目的是什么?

2个回答

移动espebp辅助调试,并在某些情况下,异常处理完成。 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 调用约定,我们知道段错误处的堆栈将如下所示:

  1. main 栈帧的顶部
  2. main 的堆栈空间 - 在这种情况下,足以对齐 16 个字节
  3. 0x0804841b 返回地址为 call foo
  4. 指向 1.
  5. foo 的栈空间
  6. 0x080483e7 返回地址为 call bar
  7. 指向 4.
  8. 酒吧的堆栈空间
  9. 0x080483f9 返回地址为 call quux
  10. 指向 7.
  11. quux 的堆栈空间

指令指针将为0x08048408ebp将指向10..

此时,处理器产生一个异常,由操作系统处理。然后它发送SIGSEGV到进程,该进程乐于终止并转储核心。然后你用 调出 gdb 中的核心转储gdb -c core,你输入file a.outand 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 个不同的偏移量访问参数。