假设我有一个用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编译器(我知道它们可以由供应商以不同方式操作)
然后我假设要访问它,它使用sp(堆栈指针)+1。b是sp + 2,c是sp + 3。
由于d和e在堆栈上,我猜我们的堆栈现在看起来像这样吗?
当我们到达函数末尾时。
我猜这就是为什么old c要求在函数顶部声明所有变量,以便编译器可以计算在函数末尾需要执行的弹出次数的原因?
我知道它可以将0x5000存储在一个寄存器中,但是C程序能够将多个层次深入到许多功能中,并且只有这么多寄存器...
谢谢!
答案 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
,所有这些都将更改。