首页 > 解决方案 > 堆栈展开和堆栈帧是如何识别的

问题描述

假设我在 C 中有这个简单的程序。

int my_func(int a, int b, int c) //0x4000
{
    int d = 0;
    int e = 0;
    return e+d;
}

int main()
{
    my_func(1,2,3); // 0x5000
    return 0;
}

忽略本质上所有可以完全优化掉的死代码这一事实。我们会说 my_func() 位于地址 0x4000,它在地址 0x5000 处被调用。

据我了解,ac 编译器(我知道它们可以由供应商以不同方式操作)可能:

然后我假设访问a它使用sp(堆栈指针)+ 1。b是sp+2,c是sp+3。

由于 d 和 e 在堆栈上,我猜我们的堆栈现在看起来像这样?

当我们到达函数的末尾时。

我猜这就是为什么旧 c 要求在函数顶部声明所有变量,以便编译器可以计算在函数末尾需要执行的弹出次数?

我知道它可以将 0x5000 存储在一个寄存器中,但是一个 C 程序能够深入到许多功能的多个层次,并且只有这么多寄存器......

谢谢!

标签: c

解决方案


在默认调用约定C,调用者在从函数返回后释放函数参数。但是函数本身在堆栈上管理自己的变量。例如,这是您的汇编代码,没有任何优化:

my_func:
  push ebp                      // +
  mov ebp, esp                  // These 2 lines prepare function stack
  sub esp, 16                   // reserve memory for local variables
  mov DWORD PTR [ebp-4], 0
  mov DWORD PTR [ebp-8], 0
  mov edx, DWORD PTR [ebp-8]
  mov eax, DWORD PTR [ebp-4]
  add eax, edx                  // <--return value in eax
  leave                         // return esp to what it was at start of function
  ret                           // return to caller
main:
  push ebp
  mov ebp, esp
  push 3
  push 2
  push 1
  call my_func
  add esp, 12                   // <- return esp to what it was before pushing arguments
  mov eax, 0
  leave
  ret

如您所见,在推送参数之前有一个add esp, 12返回的方法mainesp里面有一my_func对这样的:

  push ebp
  mov ebp, esp
  sub esp, 16 // <--- size of stack
  ...
  leave
  ret

该对集用于将一些内存保留为堆栈。leave反转 的效果push ebp/move ebp,esp。以及用于ebp访问其参数和堆栈分配变量的函数。返回值始终在eax.

快速分配堆栈大小注意事项: 如您所见,在函数中,add esp, 16即使您只int在堆栈上保留 2 个类型的变量,总大小为 8 个字节,也有一条指令。这是因为堆栈大小与特定边界对齐(至少使用默认编译选项)。如果再向 中添加 2 个int变量my_func,则该指令仍然是add esp, 16,因为总堆栈仍处于 16 字节对齐状态。但是,如果添加 的第三个变量int,则该指令变为add esp, 32。这种对齐方式可以通过 中的-mpreferred-stack-boundary选项进行配置GCC

顺便说一句,所有这些都是针对 32 位代码编译的。相比之下,您通常不会通过 64 位堆栈推送来传递参数,而是通过寄存器传递它们。如评论中所述,64 位参数仅通过堆栈从第 5 个参数开始传递(在 microsoft x64 调用约定上)。

更新:

默认调用约定,在cdecl为 x86 编译代码时通常使用的意思,没有任何编译器选项或特定函数属性。如果您将函数调用更改stdcall为示例,所有这些都会改变。


推荐阅读