我正在参加基于语言的安全课程,我必须逐步了解正常执行函数时堆栈中发生的事情,以便稍后我可以学习如何防止漏洞攻击。到目前为止,我非常了解从堆栈推送和弹出的内容以及ESP,EBP如何跟踪帧的跟踪。另外,我知道EIP已保存在堆栈中。
我不知道的是函数中的代码实际执行到哪里得到结果(我假设在内存中的其他地方,堆?)如果我给出一个简单函数的演练,可以有人解释缺失位(我会用问题标记这些部分)。设定一个简单的功能:
int add(int x, int y)
{
int sum = x + y;
return sum;
}
在main()中使用add(3,4);
调用在初始化新函数时,堆栈(从最低地址到最高地址)的ESP指向顶部,EBP指向新帧的基础。下面是main()。
现在,参数从右到左被压入堆栈。函数调用将EIP的内容保存在堆栈中。 [这是函数返回后要执行的下一条指令的地址?]
现在Prolog部分:将旧的EBP地址压入堆栈,并使EBP指向ESP。最后,局部变量被压入堆栈[这些只是它们的值存储位置的地址吗?]
Epilog是当前帧的展开堆栈的时候。 ESP被转移到EBP,因此局部变量无法访问(通常)。旧的EBP从堆栈中弹出,并指向其原始地址。 ESP移动指向保存的EIP,这是在调用add(3,4)之前的位置。
在给我研究的解释中,最后一部分是返回指令将保存的EIP值弹回到EIP寄存器中。 [当然这不是函数中的return语句,而是机器级别的ret语句,对吗?]
最后一个问题,有人可以解释当函数中的代码执行时会发生什么,以及在调用,prolog和epilog发生的所有时间点?或者提供一个明确解释的良好链接?
预先感谢大家(可以这么说:)
答案 0 :(得分:2)
首先,我编译然后反汇编你的函数,这样你就可以看到ASM级别实际发生了什么。我禁用了优化并编译为32位代码以保持简单:
Dump of assembler code for function add:
0x080483cb <+0>: push %ebp
0x080483cc <+1>: mov %esp,%ebp
0x080483ce <+3>: sub $0x10,%esp
0x080483d1 <+6>: mov 0x8(%ebp),%edx
0x080483d4 <+9>: mov 0xc(%ebp),%eax
0x080483d7 <+12>: add %edx,%eax
0x080483d9 <+14>: mov %eax,-0x4(%ebp)
0x080483dc <+17>: mov -0x4(%ebp),%eax
0x080483df <+20>: leave
0x080483e0 <+21>: ret
End of assembler dump.
尝试查看上面的反汇编并识别它正在做什么以及它如何与您的C代码匹配。现在回答你的问题。
现在Prolog部分:将旧的EBP地址压入堆栈,并使EBP指向ESP。最后,局部变量被压入堆栈[这些只是它们的值存储位置的地址吗?]
此处的序言从0x080483cb <+0>
转到0x080483ce <+3>
。
首先我们按照你的说法用push %ebp; mov %esp,%ebp
创建一个框架,然后我们用sub $0x10,%esp
为堆栈上的局部变量分配0x10字节的空间。该指令所做的就是将堆栈指针向下移动0x10字节。它不存储任何值,它只留下一些空间,我们可以使用它们用于局部变量(如果我们想要的话(我们会看到编译器甚至不使用所有值!)。
接下来我们有函数的实际逻辑。 首先,我们将堆栈中的两个参数x和y加载到寄存器中:
0x080483d1 <+6>: mov 0x8(%ebp),%edx
0x080483d4 <+9>: mov 0xc(%ebp),%eax
我们将它们加在一起:
0x080483d7 <+12>: add %edx,%eax
现在我们将结果存储在局部变量中。那个局部变量实际上只是我们在prolog中分配的堆栈空间。我们为局部变量分配了0x10字节,这里我们只用前4个字节来存储加法的结果:
0x080483d9 <+14>: mov %eax,-0x4(%ebp)
因为没有任何优化,我们立即将该结果从局部变量加载回寄存器,以便我们可以返回它:
0x080483dc <+17>: mov -0x4(%ebp),%eax
正如您所看到的,代码效率极低,但至少它很容易阅读。 现在只剩下epilog了,这很简单:
0x080483df <+20>: leave
0x080483e0 <+21>: ret
leave
破坏了我们在prolog中创建的帧,ret
返回到调用函数的下一条指令。
Epilog是当前帧的展开堆栈的时候。 ESP被转移到EBP,因此局部变量无法访问(通常)。旧的EBP从堆栈中弹出,并指向其原始地址。 ESP移动指向保存的EIP,这是在调用add(3,4)之前的位置。
在给我研究的解释中,最后一部分是返回指令将保存的EIP值弹回到EIP寄存器中。 [当然这不是函数中的return语句,而是机器级别的ret语句,对吗?]
函数中的return语句对应于机器级别的ret指令。这是直接翻译。
请记住,您的计算机不直接运行C代码,所有C都首先编译为机器代码,而ret
指令确实是EIP弹出的。
最后一个问题,有人可以解释当函数中的代码执行时会发生什么,以及在调用,prolog和epilog发生的所有时间点?或者提供一个明确解释的良好链接?
您在上面看到的反汇编是计算机运行的粗略文本表示。 EIP包含计算机将运行的下一条指令的地址。程序运行时,它存储在内存中,EIP直接指向内存中的指令。
因此计算机将按照编写的顺序运行该函数,而prolog和epilog是函数的一部分。
prolog和epilog是一种惯例,但它们只是代码。如果你愿意的话,你可以完全删除序言并写一个疯狂的结语,它也会起作用。
我建议您使用反汇编程序和调试程序,以熟悉它的实际工作方式。这不是那么难和非常合乎逻辑。