函数栈帧及调用约定

什么是栈帧

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。

栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。

从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。实现上有硬件方式和软件方式(有些体系不支持硬件栈)首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。

寄存器:ebp、esp这两个寄存器存放了维护这个栈的栈底(高地址)和栈顶元素(低地址)ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;esp所指的栈帧顶部和系统栈的顶部是同一个位置。

调用堆栈

在函数的调用时,查看【调用堆栈】,下面以一个简单程序为例讲解函数栈帧的创建:

#include <stdio.h>

int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int ret = 0;

	ret = Add(a, b);
	printf("ret = %d\n", ret);
	return 0;
}

在程序调试的时, 查看【调用堆栈】(按F10进入调试-窗口-调用堆栈,或按快捷键ctrl+alt+C) ,用VS2013调试 如下图:

如果用版本更老的,或其他如VC6.0等编辑器则可以看到更多信息。

我们发现其实main函数在 __tmai nCRTStartup 函数中调用的,而 __tmai nCRTStartup 函数是在 mai nCRTStartup 被调用的。我们知道每一次函数调用都是一个过程。这个过程我们通常称之为: 函数的调用过程。

函数调用过程中要为函数开辟栈空间,用于本次函数的调用过程中的临时变量的保存,现场保护。这块空间我们称之为函数栈帧。

而栈帧的维护我们必须了解ebp和esp两个寄存器。 在函数调用的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。比如:调用main函数, 我们为main函数分配栈帧空间, 那么栈帧维护如下:

根据汇编代码分析

1.main函数的调用过程:

过程分析:

  1. 首先mainCRTStartup(),__mainCRTStartup()函数的调用,调main()函数;
  2. 将ebp压栈处理(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;
  3. 将esp的值赋给ebp,产生新的ebp;
  4. 给esp减去一个16进制数0E4H(为main函数预开辟空间),产生新的esp;
  5. 4~6行:压栈处理:ebx、esi、edi;
  6. 7~10行:把栈开辟的空间全部预初始化为:0xcccccccc。其中lea指令:lea ---> load effective address  加载有效地址;
  7. 11~12行:处理局部变量a,b的创建;
  8. 创建变量ret,将其初始化为0;
  9. 获取参数b的值;
  10. 将b存入寄存器eax,再将将eax压栈;
  11. 将a存入寄存器ecx,再将将ecx压栈;
  12. call指令的调用,先要压栈call指令下一条指令的地址,然后跳转(push+jmp)到Add()函数的地方(__cdecl调用约定)。
  13. 用ret接收Add函数的返回值;

2.Add函数的调用过程

过程分析:

  1. 首先将main()函数ebp压栈处理,保存指向main()函数栈帧底部的ebp的地址;
  2. 将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp;
  3. 给esp减去一个16进制数(为Add()函数预开辟空间);
  4. 4~6行:堆栈处理:ebx、esi、edi;
  5. 7~10行:将栈帧全部初始化为:0xcccccccc;
  6. 创建变量z;
  7. 获取形参的a和b相加;
  8. 将结果存储到z中;
  9. 将结果存储到eax寄存器,通过寄存器带回函数的返回值;
  10. 出栈处理:edi、esi、ebx,esp 会向下移动;
  11. 将ebp赋给esp,使esp指向ebp指向的位置;
  12. ebp 出栈,将出栈的内容保存到ebp,回到main()函数的栈帧;
  13. ret 指令,出栈一次,并将出栈的内容当做地址,将程序执行跳转到该地址处(pop+jmp)。

注:栈帧这部分内容在不同编译器上实现存在差异,但思想都是一致的。

栈帧的全过程

注意esp和ebp的变化。

总结:

1.打开栈帧

  1. Push Ebp;
  2. Mov eb,esp;
  3. Sub esp,40(拉开栈)。

2.寻址方式

分类:

  1. 立即数寻址: mov eax,1;
  2. 寄存器寻址:mov eax,ebx;
  3. 存储器寻址:mov eax,[XXX]。

存储器寻址又分为:

  1. 寄存器直接寻址:mov eax,[1000];
  2. 寄存器间接寻址:mov eax,[ebx];
  3. 寄存器相对寻址:mov eax,[ebx+5];
  4.  寄存器基址变址寻址:mov eax,[ebx+edi];
  5. 寄存器相对基址变址寻址:mov eax,[ebx+edi+5]。

3.寄存器

寄存器对栈帧的标示作用:

函数栈帧:ESP和EBP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部,ESP示识了当前栈帧的项部。

除了与栈相关的寄存器外,还需要记住另一个至关重要的寄存器:eip

eip指令寄存器(Extended Instruction Pointer), 其内存放着一一个指针,该指针永远指向下一条等待执行的指令地址,可以说如果控制了EIP 寄存器的内容,就控制了进程一-我们让EIP指向哪里,CPU就会去执行哪里的指令。

4.函数栈帧

  1. 局部变量:为函数局部变量开辟的内存空间。
  2. 栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的项部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。
  3. 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

5.函数调用约定

分类:__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal

参数传递顺序:

1.从右到左依次入栈:__stdcall,__cdecl,__thiscall,__fastcall

2.从左到右依次入栈:__pascal

常用描述:

__cdecl

  1. 参数是从右向左传递的,放在堆栈中;
  2. 堆栈平衡是由调用函数来执行的(在call B,之后会有add esp x,x表示参数的字节数);
  3. 函数的前面会加一个前缀_(_sumExample)。

__stdcall

  1. 参数是从右往左传递的,放在堆栈中;
  2. 函数的堆栈平衡操作是由被调用函数执行的;
  3. 在函数名的前面用下划线修饰,在函数名的后面由@来修饰并加上栈需要的字节数的空间(_sumExample@8)。

__fastcall

  1. 参数是从右往左传递的,放在寄存器中,而不是栈中;
  2. 当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈;
  3. 像浮点值、远指针和__int64类型总是通过堆栈来传递的。

6.函数调用

函数调用大致包括以下几个步骤:

  1. 参数入栈:将参数从右向左依次压入系统栈中。
  2. 返回地址入栈:将当前代码区调用指令的下- - 条指令地址压入栈中,供函数返回时继续执行。
  3. 代码区跳转处理器从当前代码区跳转到被调用函数的入口处。
  4. 栈帧调整。具体包括:
  •  保存当前栈帧状态值,已备后而恢复本栈帧时使用(ebp入栈);
  • 将当前栈帧切换到新栈帧(将esp值装入ebp,更新栈帧底部);
  • 给新栈帧分配空间(把esp减去所需空间的大小,抬高栈顶)。

函数调用时系统栈的变化过程:

7.函数返回

步骤:

(1)保存返回值:通常将函数的返回值保存在寄存器eax中。

(2)弹出当前栈,恢复上一个栈帧。具体包括:

  • 在堆栈平衡的基础上,给ESP加上栈帧的大小,  降低栈项,回收当前栈帧的空间。
  • 将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一一个栈帧。
  • 将函数返回地址弹给EIP寄存器。

(3)跳转:按照函数返回地址跳回母函数中继续执行。

还是以C语言和Win32平台为例,函数返回时的相关的指令序列如下:

addesp,xxx ; 降低栈顶,回收当前的栈帧

pop ebp; 将上一个栈帧雇部位置恢复到ebp,

retn ;这条指令有两个功能。
    a)弹出当前栈项元素,即弹出栈帧中的返回地址。 至此,栈帧恢复工作完成
    b)让处理器跳转到弹出的返回地址,恢复调用前的代码区

猜你喜欢

转载自blog.csdn.net/qq_40933663/article/details/82937833