我有两个关于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
答案 0 :(得分:1)
EBP
形成堆栈中变量的固定引用点:主要是函数的所有参数,函数的所有本地参数以及最后的返回地址。使用此固定点,函数几乎可以随机增长/改变它的堆栈,从任何地方跳转到函数结尾,并将堆栈指针恢复到原始位置。
这个概念紧挨着强制性,因为原始的8086代码不允许堆栈指针与mov ax, [sp + 10]
中的位移一起使用,但仅限于push
和pop
。引用除了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文件并不是所有的库调用都已解决,所以它看起来有点奇怪,但是不会影响局部变量!)