C语言-函数栈帧

本文最后更新于:1 年前

栈帧概念

当发生函数调用时,会将函数运行需要的信息全部压入栈中,这常常被称为栈帧(Stack Frame)或 活动记录(Activate Record),一般包括一下几个方面内容:

  1. 函数的返回地址,也就是函数执行完成后从哪里开始继续执行后面的代码。

  2. 参数和局部变量。(有些编译器,或者编译器在开启优化选项的情况下,会通过寄存器来传递参数,而不是将参数压入栈中)

  3. 编译器自动生成的临时数据。

    a. 当函数返回值的长度较大(比如占用40个字节)时,会先将返回值压入栈中,然后再交给函数调用者。

  4. b. 当返回值的长度较小(char、int、long 等)时,不会被压入栈中,而是先将返回值放入寄存器,再传递给函数调用者。

  5. 一些需要保存的寄存器,为了在函数退出时能够恢复到函数调用之前的场景,继续执行上层函数。

调用惯例

  1. 函数参数的传递方式,是通过栈传递还是通过寄存器传递

  2. 函数参数的传递顺序,是从左到右入栈还是从右到左入栈。

  3. 参数弹出方式。函数调用结束后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由调用方来完成,也可以由被调用方来完成。

  4. 函数名修饰方式。函数名在编译时会被修改,调用惯例可以决定如何修改函数名

在函数声明处是为调用方指定调用惯例,

而在函数定义处是为被调用方(也就是函数本身)指定调用惯例。

调用惯例 参数传递方式 参数出栈方式 名字修饰
cdecl 按照从右到左的顺序入栈 调用方 下划线+函数名, 如函数 max() 的修饰名为 _max
stdcall 按照从右到左的顺序入栈 函数本身 (被调用方) 下划线+函数名+@+参数的字节数, 如函数 int max(int m, int n) 的修饰名为 max@8
fastcall 将部分参数放入寄存器, 剩下的参数按照从右到左的顺序入栈 函数本身 (被调用方) @+函数名+@+参数的字节数
pascal 按照从左到右的顺序入栈 函数本身 (被调用方) 较复杂

示例一

1
2
3
4
5
6
7
void func(int a, int b){
int p =12, q = 345;
}
int main(){
func(90, 26);
return 0;
}

函数使用默认的调用惯例 cdecl,即参数从右到左入栈,由调用方负责将参数出栈

函数进栈出栈过程:

  • 第一个变量和 old ebp 之间有4个字节的空白,变量之间也有若干字节的空白。

    因为Debug模式下方便加入调试信息,Release模式下会优化

  • 为局部变量分配内存时,仅仅是将 esp 的值减去一个整数,预留出足够的空白内存。因此未初始化的局部变量值是垃圾值

示例二

从133~139行我们可以看到main函数栈帧的形成过程(入栈操作):

1) push %rbp 将上一级函数栈帧的栈底指针压栈

2) mov %rsp, %rbp 将BP指针指向SP,因为上一级函数的栈顶指针是下一级函数的栈底指针,证明栈帧是依次向下增长的

3) sub $0x10, %rsp SP栈顶指针向下位移16个字节,即创建main函数栈帧。这个地方为什么是16个字节呢?是因为上一级函数栈底指针和当前函数返回时下一条指令地址各占4个字节,m和n两个整形变量各占4个字节,加起来就是16个字节。

4) movl $0x8, -0x4(%rbp) 将变量m压栈

5) movl $0x6, -0x8(%rbp) 将变量n压栈

6) mov -0x8(%rbp), %edx 将m变量值加载到edx寄存器

mov -0x4(%rbp), %eax 将n变量值加载到eax寄存器

mov %edx, %esi

mov %eax, %edi

7)callq 4004c4 调用callq指令跳转到func函数段,同时压栈EIP+4,即返回func函数时下一条可执行指令的地址

从func函数的反汇编代码可以看到,0x4004c4地址就是func函数开始处,和前面的callq对应。在进入func函数段之后,就是func函数压栈的动作,基本顺序和前面的main函数压栈过程一致。这个地方需要注意的是,首先是mov %edi, -0x4(rbp),从前面的汇编代码可以看到%edi保存的是n变量的值,其次才执行mov %esi, -0x8(rbp)压栈m变量值,证明函数参数的传递顺序是从右往左。