了解在机器级别执行功能

Understanding executing a function at the machine level

我正在学习基于语言的安全性 class,我必须逐步了解正确执行函数时堆栈中发生的情况,以便以后我可以学习如何防范功绩。到目前为止,我对从堆栈中压入和弹出的内容以及 ESP、EBP 如何移动以跟踪帧有了很好的理解。另外,我知道EIP是保存在栈上的。

我不知道函数中的代码实际上在哪里执行以获得结果(我假设在内存中的其他地方,堆?)如果我给出一个简单函数的演练,有人可以解释一下缺失的部分(我会用问题标记这些部分)。假设一个简单的函数:

int add(int x, int y)
{
   int sum = x + y;
   return sum;
}

在 main() 中用 add(3,4) 调用;

在新函数初始化时,堆栈(从最低地址到最高地址)的 ESP 指向顶部,EBP 指向新帧的底部。下面是 main().

现在,参数从右到左压入堆栈。函数调用将 EIP 的内容保存在堆栈上。 [这是函数returns之后要执行的下一条指令的地址?]

现在是 Prolog 部分:旧的 EBP 地址被压入堆栈,EBP 指向 ESP。最后,局部变量被压入堆栈[这些只是它们存储值的地址吗?]

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

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

最后一个问题,谁能解释一下函数中的代码在执行时发生了什么,以及调用、序言和结尾发生在什么时候?或者提供一个好的link给个清楚的解释?

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

首先,我编译然后反汇编了你的函数,这样你就可以看到在 ASM 级别上实际发生了什么。我禁用优化并编译为 32 位代码以保持简单:

Dump of assembler code for function add:
   0x080483cb <+0>:     push   %ebp
   0x080483cc <+1>:     mov    %esp,%ebp
   0x080483ce <+3>:     sub    [=10=]x10,%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 代码匹配。现在回答你的问题。

Now the Prolog part: The old EBP address is pushed onto stack and the EBP is made to point to the ESP. Finally, the local variables are pushed onto the stack [Are these just the addresses of where their values are stored?]

这里序言从 0x080483cb <+0>0x080483ce <+3> 包括在内。 首先我们用push %ebp; mov %esp,%ebp创建一个帧,然后我们用sub [=19=]x10,%esp为堆栈上的局部变量分配0x10字节的space。该指令所做的只是将堆栈指针向下移动 0x10 个字节。它不存储任何值,它只是留下一些 space 在那里,如果我们愿意的话,我们可以将其用于局部变量(我们会看到编译器甚至没有使用它的全部!)。

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

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

我们将它们加在一起:

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

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

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

并且因为没有任何优化,我们立即将结果直接从局部变量加载回寄存器,这样我们就可以 return 它:

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

如您所见,代码效率低得令人难以置信,但至少它相当容易阅读。 现在只剩下结语了,很简单:

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

leave 破坏了我们在序言中创建的帧, ret return 调用函数的下一条指令。

The Epilog is when the stack is to be unwound for the current frame. ESP is moved to EBP, so that local variables are inaccessible (normally). Old EBP is popped off stack, and made to point to its original address. ESP moves to point to saved EIP which was where it was before add(3,4) was called.

In the explanation I was given to study, the final part is that the return instruction pops the saved EIP value back into the EIP register. [Surely this isn't the return statement in the function but a ret instruction at machine level, right?]

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

Last question, can someone explain what's going on when the code in the function is executing and at what point during all that does the call, prolog and epilog occur? Or provide a good link to a clear explanation?

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

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

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

我建议您尝试使用反汇编程序和调试器,以熟悉它的实际工作原理。这并不难,也很合乎逻辑。