以下代码中EBP的用途是什么?

时间:2014-03-19 19:35:50

标签: assembly

我有两个关于EBP注册的问题。

我理解ESP和EIP。但是,我真的不明白为什么会使用EBP。

在下面的代码中,我将EBP寄存器(实际上是0000000)推送到堆栈。然后,我将堆栈的内存地址移动到EBP,以便ESP和EBP具有相同的数据。这是序言。有一些代码用syscall完成。然后我做反向(epilog),因为'leave'表示我将EBP移动到ESP(这些值与prolog相同)然后将堆栈的最后一个值(即EBP,即00000000)弹出到EBP。这使得EBP与prolog之前的值相同。

为什么有人会这样做?有什么意义?请以简单的方式回答!请记住,我没有掌握EBP(帧指针)实际上做了什么。

编辑:或者这是一种在函数中有效备份堆栈(ESP)的方法吗?换句话说:程序可以执行它对堆栈的操作,并且'原始堆栈'将始终存在于EBP中。然后,当程序结束时,EBP将重新回到之前的状态。它是否正确?如果是这样的话,epilog只是一个整理过程吗?

另外,AIUI,我可以用'enter'代替'push ebp / mov ebp,esp'。然而,当我尝试在nasm中编译时,我得到'错误:操作码和操作数的无效组合''离开'工作正常; 'enter'没有。什么是正确的语法?

谢谢!

Example:

    push ebp
    mov, ebp, esp 

    [some code here]
    int 0x80

leave
ret   

3 个答案:

答案 0 :(得分:1)

EBP形成堆栈中变量的固定引用点:主要是函数的所有参数,函数的所有本地参数以及最后的返回地址。使用此固定点,函数几乎可以随机增长/改变它的堆栈,从任何地方跳转到函数结尾,并将堆栈指针恢复到原始位置。

这个概念紧挨着强制性,因为原始的8086代码不允许堆栈指针与mov ax, [sp + 10]中的位移一起使用,但仅限于pushpop。引用除了mov xx, [bp + 10]所需的顶级元素之外的任何其他内容。

答案 1 :(得分:1)

EBP的想法确实是形成一个固定的参考点。通常你可以使用堆栈指针(例如,当将参数推入堆栈准备进行调用时),并发现确定某些数据相对于堆栈指针的位置真的很痛苦。但相对于基指针,它总是相同的。现代编译器可以毫无困难地解决这个问题,但是如果你想编写一个使用堆栈进行推送和弹出的大块汇编代码(手工编写),你会发现相对于寄存器更容易引用局部变量(EBP) )不会改变。

答案 2 :(得分:0)

enter还需要一个数字,即要分配的空间量,这是您的问题的关键:这些指令用于为函数的局部变量设置空间。

通过EBP寄存器引用局部变量。让我举个例子:

import core.stdc.stdio;
void main() {
    int a = 8;
    a += 8;
    printf("%d\n", 8);
}

(这是D代码,但这不是真正相关的)

Disassembly of section .text._Dmain:

00000000 <_Dmain>:
   0:       55                      push   ebp
   1:       8b ec                   mov    ebp,esp
   3:       83 ec 04                sub    esp,0x4
   6:       b8 08 00 00 00          mov    eax,0x8
   b:       89 45 fc                mov    DWORD PTR [ebp-0x4],eax
   e:       01 45 fc                add    DWORD PTR [ebp-0x4],eax
  11:       50                      push   eax
  12:       b9 00 00 00 00          mov    ecx,"%d\n"
  17:       51                      push   ecx
  18:       e8 fc ff ff ff          call   printf
  1d:       31 c0                   xor    eax,eax
  1f:       83 c4 08                add    esp,0x8
  22:       c9                      leave
  23:       c3                      ret

让我们将其分解为每个部分:

   0:       55                      push   ebp
   1:       8b ec                   mov    ebp,esp
   3:       83 ec 04                sub    esp,0x4

这是功能prolog,设置ebp。 sub esp, 0x4推送了4个字节的堆栈 - 这为我们的本地int a变量腾出了空间,这个变量长4个字节。

enter指令很少使用,但我相信enter 4,0做同样的事情 - 输入一个4字节局部变量空间的函数。编辑:另一个0是嵌套级别,我从未见过它使用过...输入通常比编译器在这里做的步骤更慢。 /编辑

   6:       b8 08 00 00 00          mov    eax,0x8
   b:       89 45 fc                mov    DWORD PTR [ebp-0x4],eax

这是a=8行 - 第二行将值存储在局部变量的内存中。

   e:       01 45 fc                add    DWORD PTR [ebp-0x4],eax

然后,我们在a+=8中添加它(编译器在这里重用了eax,因为它认识到了 数字是一样的......)

之后,它通过将其参数推送到堆栈来调用printf,然后将eax寄存器(xor eax, eax)清零,这就是D从函数返回0的方式。

  11:       50                      push   eax
  12:       b9 00 00 00 00          mov    ecx,"%d\n"
  17:       51                      push   ecx
  18:       e8 fc ff ff ff          call   printf
  1d:       31 c0                   xor    eax,eax
  1f:       83 c4 08                add    esp,0x8

请注意,此处的add esp, 0x8是对printf的调用的一部分:调用者负责在调用函数后清理参数。这是必需的,因为只有调用者知道它实际发送了多少个args - 这就是启用printf变量参数的原因。

无论如何,最后,我们清理局部变量并从函数返回:

  22:       c9                      leave
  23:       c3                      ret

编辑:leave btw扩展为mov esp, ebp; pop ebp; - 它与设置说明正好相反,就像Aki Suihkonen在另一个答案中所说的那样,这里的一个好处就是堆栈恢复到如何它是在函数入口处,无论函数内部发生了什么(好吧,除非函数完全破坏了堆栈的内容,在这种情况下你的程序很可能很快崩溃)。 /编辑

所以,底线,ebp的东西都是关于你的局部变量。它使用esp开始,所以它有一个很好的内存空间,不会踩到其他函数(它在调用堆栈上),但是将它移动到ebp,这样你的locals在整个函数中保持一致的偏移量 - 变量{ {1}}在此函数中总是[EBP-4],即使在操作堆栈时也是如此。

最简单的方法是通过反汇编用C语言编写的函数来实现,就像我们在这里所做的那样。 linux命令a是我使用的(然后我手动修复了一些小的东西,使它更具可读性。如果你反汇编.o文件并不是所有的库调用都已解决,所以它看起来有点奇怪,但是不会影响局部变量!)