了解x86 IA32程序集中函数调用的前/后汇编代码

时间:2014-08-07 03:53:46

标签: c function assembly x86 callstack

所以我们有下面的代码,设置一个带有参数的函数调用,它的主体被省略(等等等),然后在函数的末尾弹出。

pushl %ebp
movl %esp, %ebp
pushl %ebx
movl 8(%ebp), %ebx
movl 12(%ebp), %ecx
etc
etc
etc
//end of function
popl %ebx
popl %ebp

这是我(我想)理解的内容。

假设我们有%esp指向内存地址100。

pushl %ebp

所以这实际上使%ebp指向%esp指向的位置(内存地址100)+ 4.所以现在%ebp指向内存地址104.这使我们当前的内存状态看起来像这样:

----------
|100|%esp
|104|%ebp
----------

然后我们有下一行代码:

movl %esp, %ebp

所以根据我的理解,ebp现在指向内存地址100.我对于为什么我们这一步有一点直觉,但我的困惑是下一行:

pushl %ebx

推送ebx的目的是什么,我假设它会指向内存地址104?我有一个模糊的想法,ebp(104)正下方的空间应该是一个“旧堆栈指针”的引用,所以我可以看到为什么接下来的两行将8和1​​2添加到ebp作为“参数”我们的功能,而不是4和8。

但我很困惑为什么我们首先将ebx推入堆栈。

我也不明白弹出,为什么我们弹出ebx和ebp?

在他不得不睡觉之前和别人谈论这件事时,他提到我们没有提到我们的堆栈指针是100的事实 - 直到我们回到ebp。现在,我认为ebp的值是100,所以我不明白他想要做的这一点。

所以澄清一下:

  1. 到目前为止我的理解是否正确?

  2. 为什么我们将ebx推入堆栈?

  3. 什么是生活在ebp下方的“旧堆栈指针”?那是我们推动的ebx吗?

  4. 是否有一些我不理解的东西,比如我们推动的ebx和我们论证后的行中的ebx之间的某种差异?被推动的ebp与之后的线路中的ebp有区别吗?

  5. 为什么我们最后会出现?

  6. 如果难以理解,我道歉。我理解有关此问题的类似问题,但我试图直观地理解并描绘函数调用中究竟发生了什么,这对我来说是有意义的。

    注意:我编辑了一些关于我对正在发生的事情的理解的重要事项,特别是关于ebp。

2 个答案:

答案 0 :(得分:3)

正如Joachim在你的问题上所说in a comment一样,推送寄存器会将当时寄存器的内容推送到堆栈中;它没有推动对寄存器或其他任何东西的引用。我不确定你是说那是发生了什么,但是否则这个图表不清楚:

----------
|100|%esp
|104|%ebp
----------

尽管如此,我会尝试解释它的作用和原因。


当调用者调用我们的函数并且%esp之后的指令位于0x100时,说call0x200。当我们执行call时,我们按0x200(返回地址)并跳转到该过程。我们的筹码是:

          Address  Value
%esp -->  0x100    0x200

%ebp是有价值的;它可能指向堆栈,也可能不会。它甚至不需要代表地址。因此%ebp对我们来说毫无意义。

但是虽然它对我们毫无意义,但是呼叫者确实希望它在呼叫之前和之后保持不变,所以我们必须保留它。假设它包含值0xDEADBEEF。我们推动它,所以堆栈现在看起来像这样:

          Address  Value
          0x100    0x     200
%esp -->  0x0fc    0xDEADBEEF

在大多数情况下,我们可以将所有内容都作为%esp的偏移来解决,这也适用于此。但是如果编译器正在编译一些处理可变长度数组或其他特性的C代码,我们通常会想要从我们推送的第一件事索引而不是我们推送的最后一件事。为此,我们将%ebp设置为我们现在所处的位置。事情看起来像这样:

                Address  Value
                0x100    0x     200
%esp, %ebp -->  0x0fc    0xDEADBEEF

请注意%ebp指向的地址处的值是%ebp的旧值,因此您可以按照之前提到的那样处理堆栈。

接下来,我们推送%ebx,我们会说它的值为0xBEEFCAFE。这是与函数序言没有直接关系的第一件事。然后我们的堆栈看起来像这样:

          Address  Value
          0x100    0x     200
%ebp -->  0x0fc    0xDEADBEEF
%esp -->  0x0f8    0xBEEFCAFE

但为什么我们推%ebx?好吧,事实证明,x86 C调用约定规定,与%ebp一样,%ebx必须保持与调用之前相同。因为你省略的代码可能会改变%ebx,它必须保留初始值,以便它可以为调用者恢复它。

我们恢复%ebx后,我们会弹出%ebp,同时还原值,因为在调用之后也必须保留该值。最后我们回来了。


TL; DR: %ebp%ebx被推送和弹出,因为它们在执行函数体期间被操纵,但x86 C调用约定指示在调用之前和之后值必须保持不变,因此必须保留初始值,以便我们可以恢复它们。

答案 1 :(得分:0)

pushl %ebp

将ebp的值保存在堆栈中。任何push命令都会影响%esp。

的值

movl %esp, %ebp

将esp的当前值移动到ebp。这将设置堆栈帧,您现在可以在ebp上方找到函数参数(随着堆栈的增长)。

pushl %ebx

保存ebp的值(不是100%肯定,但很可能是ABI规则)。

movl 8(%ebp), %ebx

将内存ebp + 8移动到ebx。如前所述,由于堆栈增长,这是函数参数之一。

movl 12(%ebp), %ecx

与前一条指令类似,这会将另一个函数参数移入ecx。

popl %ebx

恢复我们之前保存在堆栈中的ebx的值。

popl %ebp

恢复ebp的值。此时,每次推送都有一个匹配弹出,因此esp返回到函数入口上的状态,以便我们返回。