updated:

从C语言的函数调用谈起


零.先看一个简单的程序

void try(int); void try(int a) { int j = 456; } void main () { int i = 123; try(1011); int k = 789; }

编译生成32位a.out文件,再用IDA Pro逆编译如下:

一.函数调用协定__cdecl

这是C语言的缺省调用约定。一方面,该约定规定了函数参数的压栈顺序,即从右向左压栈,这样的压栈顺序赋予了C语言可变长度参数的特性。比如printf函数的原型如下:

1
int printf(const char *format, ...); 

在将参数压栈时,format总是位于栈顶,可以通过栈顶指针esp (为什么不使用栈基底指针ebp?) 便捷的确定其位置,进而根据format确定参数的个数和类型。

另一方面,该约定规定堆栈由调用者来清理(即实现栈平衡,函数调用前后保证栈指针不变),而不是被调用的函数

x86-64下的不同之处:x86-64架构摒弃了__stdcall、__cdecl、__fastcall、_thiscall 等混乱的调用协定,仅使用一个类似于__fastcall的调用协定。该协定规定前几个参数保存在指定的寄存器中(该过程较复杂),剩下的参数从右往左依次入栈,被调用者实现栈平衡。

二.堆栈平衡的实现

分析如下:

疑问:分析过程中的第三步为什么要分配16字节的内存?从程序运行的过程来看,这一部分栈内存只使用了一半?难道栈帧有最小大小?

可能的答案:编译器在编译时为防止缓冲区溢出攻击多分配了一些内存给栈帧使用。

三.总结

这一张图可以很好的总结上面实现堆栈平衡的过程,这一设计被称为过程活动记录,一个过程活动记录也叫做一个栈帧,它就像内存中的一帧,记录函数执行过程中用到的局部变量,参数和调用者的地址,函数的生命周期结束后,栈帧也随之弃置或分配给其他函数使用。


← Prev 关于Cache存储器 | Matlab学习笔记(1) Next →