0

我正在学习基于语言的安全性课程,我必须逐步了解当函数正确执行时堆栈中发生了什么,以便稍后我可以学习如何防止漏洞利用。到目前为止,我已经很好地理解了从堆栈中推送和弹出的内容以及 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 指令,对吧?]

最后一个问题,有人能解释一下函数中的代码在执行时发生了什么,以及调用、序言和结语发生在什么时候?或者提供一个清晰解释的好链接?

提前致谢(可以这么说:)

4

1 回答 1

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按照你说的创建一个框架,然后我们为堆栈上的局部变量分配 0x10 字节的空间sub $0x10,%esp。该指令所做的只是将堆栈指针向下移动 0x10 个字节。它不存储任何值,它只是留下一些空间,如果我们愿意,我们可以将其用于局部变量(我们会看到编译器甚至不会使用所有这些空间!)。

接下来我们有函数的实际逻辑。首先,我们将堆栈中的两个参数 x 和 y 加载到寄存器中:

0x080483d1 <+6>:     mov    0x8(%ebp),%edx
0x080483d4 <+9>:     mov    0xc(%ebp),%eax

我们将它们加在一起:

0x080483d7 <+12>:    add    %edx,%eax

现在我们将结果存储在一个局部变量中。该局部变量实际上只是我们在序言中分配的堆栈空间。我们为局部变量分配了 0x10 个字节,这里我们只使用前 4 个字节来存储加法的结果:

0x080483d9 <+14>:    mov    %eax,-0x4(%ebp)

而且因为没有任何优化,我们立即将结果从局部变量加载回寄存器,以便我们可以返回它:

0x080483dc <+17>:    mov    -0x4(%ebp),%eax

如您所见,代码效率非常低,但至少它相当容易阅读。现在只剩下结语了,这很简单:

0x080483df <+20>:    leave  
0x080483e0 <+21>:    ret   

leave销毁我们在序言中创建的帧,并返回ret到调用函数的下一条指令。

Epilog 是为当前帧展开堆栈的时间。ESP 移至 EBP,因此(通常)无法访问局部变量。旧的 EBP 从堆栈中弹出,并指向其原始地址。ESP 指向保存的 EIP,这是调用 add(3,4) 之前的位置。

在我学习的解释中,最后一部分是返回指令将保存的 EIP 值弹出回 EIP 寄存器。[当然这不是函数中的 return 语句,而是机器级别的 ret 指令,对吧?]

函数中的 return 语句对应于机器级别的 ret 指令。是直接翻译。请记住,您的计算机不会直接运行 C 代码,所有 C 都首先编译为机器代码,而ret这里的指令确实是弹出 EIP 的内容。

最后一个问题,有人能解释一下函数中的代码在执行时发生了什么,以及调用、序言和结语发生在什么时候?或者提供一个清晰解释的好链接?

您在上面看到的反汇编是计算机运行内容的粗略文本表示。EIP 包含计算机将运行的下一条指令的地址。当您的程序运行时,它存储在内存中的某个位置,而 EIP 直接指向内存中的指令。

所以计算机只会按照编写的顺序运行函数,prolog 和 epilog 是函数的一部分。

prolog 和 epilog 是一种约定,但它们只是代码。如果你愿意,你可以完全删除序言并编写一个疯狂的结语,它也可以。

我建议您使用反汇编器和调试器,以熟悉它的实际工作原理。这并不难,而且非常合乎逻辑。

于 2015-04-10T18:29:37.070 回答