什么是栈帧
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;esp所指的栈帧顶部和系统栈的顶部是同一个位置。
函数调用过程
示例代码
#include <stdio.h>
#include <stdlib.h>
int Add(int left, int right){
int sum = 0;
sum = left + right;
return sum;
}
int main(){
int num1 = 1;
int num2 = 2;
int result = 0;
result = Add(num1, num2);
printf("result: %d\n", result);
system("pause");
return 0;
}
_tmainCRTStartup函数的调用
我们对该代码进行调试,查看调用堆栈如下:
我们可以发现,main函数是在_tmainCRTStartup函数中调用的。而_tmainCRTStartup是在mainCRTStartup中调用的。
下面看一下_tmainCRTStartup函数的栈帧。
main函数的调用
既然main函数被调用的,那么这个程序第一步应该会为main函数分配栈空间。调试中查看反汇编。
int main(){
/* 将ebp压栈处理(方便函数返回之后的现场恢复) */
00C51420 push ebp
/* 这里使esp的值赋给ebp,产生新的ebp */
00C51421 mov ebp,esp
/* 给esp减去一个16进制数字0E4H,产生新的esp */
00C51423 sub esp,0E4h
00C51429 push ebx
00C5142A push esi
00C5142B push edi
/* 将栈帧与开辟的空间全部初始化为0xCCCCCCCC */
00C5142C lea edi,[ebp-0E4h]
00C51432 mov ecx,39h
00C51437 mov eax,0CCCCCCCCh
00C5143C rep stos dword ptr es:[edi]
/* 创建局部变量num1、num2、result */
int num1 = 1;
00C5143E mov dword ptr [num1],1
int num2 = 2;
00C51445 mov dword ptr [num2],2
int result = 0;
00C5144C mov dword ptr [result],0
图解如下
过程分析
- 首先_tmainCRTStartup函数调用main函数。
- 将ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复)。
- 将esp的值赋给ebp,产生新的ebp。
- 给esp减去一个16进制数0E4H(为main函数预开辟空间)。
- push ebx、esi、edi。
- lea指令,加载有效地址。
- 初始化预开辟的空间为0xCCCCCCCC。
- 创建变量num1、num2和result。
Add函数的调用
先查看main函数中调用Add之前的反汇编:
result = Add(num1, num2);
/* 参数压栈,先压num2 */
00C51453 mov eax,dword ptr [num2]
00C51456 push eax
/* 参数压栈,压num1 */
00C51457 mov ecx,dword ptr [num1]
00C5145A push ecx
/* call指令调用Add函数 */
00C5145B call _Add (0C510E6h)
00C51460 add esp,8
00C51463 mov dword ptr [result],eax
逐语句执行到call指令,如下,跳转到Add函数。
再来看Add函数的反汇编:
int Add(int left, int right){
/* 将ebp压栈处理(方便函数返回之后的现场恢复) */
00C513D0 push ebp
/* 这里使esp的值赋给ebp,产生新的ebp */
00C513D1 mov ebp,esp
/* 给esp减去一个16进制数字0CCH,产生新的esp */
00C513D3 sub esp,0CCh
00C513D9 push ebx
00C513DA push esi
00C513DB push edi
/* 将栈帧与开辟的空间全部初始化为0xCCCCCCCC */
00C513DC lea edi,[ebp-0CCh]
00C513E2 mov ecx,33h
00C513E7 mov eax,0CCCCCCCCh
00C513EC rep stos dword ptr es:[edi]
/* 创建局部变量sum */
int sum = 0;
00C513EE mov dword ptr [sum],0
/* 获取形参left和right的值,相加之后存到sum中 */
sum = left + right;
00C513F5 mov eax,dword ptr [left]
00C513F8 add eax,dword ptr [right]
00C513FB mov dword ptr [sum],eax
/* 将结果存到寄存器,通过寄存器带回函数的返回值 */
return sum;
00C513FE mov eax,dword ptr [sum]
}
/* 寄存器edi、esi、ebx出栈 */
00C51401 pop edi
00C51402 pop esi
00C51403 pop ebx
/* 将ebp赋给esp,使esp指向ebp指向的地方,释放Add函数的栈帧 */
00C51404 mov esp,ebp
/* ebp出栈,将出栈的内容给ebp(即main函数的ebp),回到main函数的栈帧 */
00C51406 pop ebp
/* 出栈一次,将出栈的内容当做地址,并跳转到该地址处(call指令的下一条) */
00C51407 ret
图解如下
注意:C语言函数调用参数的压栈顺序为从右向左,上述例子中就是num2先压栈,num1后压栈。
过程分析
- 将num2存入eax寄存器,将eax压栈。
- 将num1存入ecx寄存器,将ecx压栈。
- call指令的调用,先将call指令的下一条指令的地址压栈。然后跳转到Add函数。
- 将main函数的ebp压栈,方便函数返回之后的现场恢复。
- 将esp的值赋给ebp,产生新的ebp,即Add函数的ebp。
- 给esp减去一个16进制的数0CCH,为Add函数预开辟空间。
- push ebx、esi、edi。
- lea指令,加载有效地址。
- 初始化预开辟的空间为0xCCCCCCCC。
- 创建变量sum。
- 获取形参left和right的值再相加,将结果存到sum中。
- 将结果存储到eax寄存器,通过寄存器带回函数的返回值。
- pop edi、esi、ebx。
- 将ebp的值赋给esp,是esp指向ebp指向的地方。释放Add函数的栈帧。
- ebp出栈,将出栈的内容给ebp(即main函数的ebp),回到main函数的栈帧。
- ret指令,出栈一次,将出栈的内容当做地址,并跳转到该地址处(call指令的下一条指令的地址)。
总结
- ebp寄存器:扩展基址指针寄存器(extended base pointer)其内存放一个指针,该指针指向系统栈最上面一个栈帧的底部。
- esp寄存器:扩展栈指针寄存器(extended stack pointer),是指针寄存器的一种,用于存放函数栈顶指针。esp为栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部)。