如何解开堆栈并识别堆栈框架

时间:2019-01-02 14:48:56

标签: c

假设我有一个用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处被调用。

据我所知,一个c编译器(我知道它们可以由供应商以不同方式操作)

  • 将c推入堆栈
  • 将b推入堆栈
  • 将a推入堆栈
  • 将0x5000推送到堆栈(返回地址)
  • 致电0x4000

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

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

  • c
  • b
  • a
  • 0x5000
  • d
  • e

当我们到达函数末尾时。

  • 然后将e和d弹出堆栈吗?
  • 然后...按e + d?还是将其保存到寄存器中以便返回后使用?
  • 是否返回0x5000,因为它是堆栈的顶部?
  • 然后弹出返回地址(0x5000)以及a,b和c?

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

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

谢谢!

1 个答案:

答案 0 :(得分:1)

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中有一个main用于返回esp,就像在推入参数之前一样。在my_func中有这样的一对:

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

此对集用于将某些内存保留为堆栈。 leave反转push ebp/move ebp,esp的效果。函数使用ebp来访问其参数和堆栈分配的变量。返回值始终位于eax中。

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

顺便说一下,所有这些都是针对32位代码编译的,相比之下,通常您永远不会通过堆栈推入64位来传递参数,而是通过寄存器传递它们。如注释中所述,在64位参数中,仅从第5个参数(on microsoft x64 calling convention开始通过堆栈传递。

更新

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