函数调用过程栈的变化
程序中栈的基础知识
栈是向下生长的
向下生长指的是从内存的高地址-->低地址的方向拓展。
栈有栈底和栈顶,从上面可以知道栈顶的地址是比栈底的要低的。
对于X86体系的CPU而言,大概需要知道以下基础知识:
- ebp寄存器:一般叫做基址指针或者帧指针;
- esp寄存器:一般叫做栈指针
- ebp在没有改变之前始终指向栈底,ebp主要用于在堆栈中寻址
- esp会随着数据入栈和出栈变化,esp始终指向栈顶
函数调用的过程描述
若函数A调用函数B,那么A函数一般叫做调用者,B函数一般为被调用者,函数调用过程可以做如下描述
- 现将函数A的堆栈基址ebp入栈,用于保存之前任务信息
- 然后将函数A的栈顶指针esp的值赋给ebp,用作新的基址(这里就是函数B的栈底)
- 紧接着在新的ebp基础上开辟相应的空间当做被调用者B的栈空间,开辟空间一般用sub指令;
- 函数B返回后,从当前栈底ebp恢复为调用者A的栈顶esp,使得栈顶恢复成函数B被调用前的位置;
- 最后调用者A从恢复的栈顶弹出之前的ebp值(因为在函数调用前一步被压入堆栈);这样ebp和esp都变成了调用函数B前的位置;
示意图如下所示
简单例子
函数调用示例代码
一个简单的函数调用例子
#include <iostream> int __cdecl Add(int a, int b) { return a + b; } int main() { auto res = Add(2, 3); std::cout << "2 + 3 = " << res << std::endl; std::cout << "Hello World!\n"; }
函数调用过程汇编解析
-
在main函数调用Add函数之前,main函数的栈帧情况如下所示
-
当main函数调用Add函数的时候,汇编如下
auto res = Add(2, 3); 00E12618 push 3 00E1261A push 2 00E1261C call Add (0E111D6h) 00E12621 add esp,8 00E12624 mov dword ptr [res],eax
- 从调用Add函数的汇编语言中大概可以得出调用函数的大概模式就是如下:
push parameter_n push parameter_... push parameter_1 call funcName; //调用函数funcName, 加你个返回地址填入栈,并且跳转到funcName
main函数调用Add函数的栈示意图如下:
当call Add (0E111D6h) 进入Add函数之后,汇编语言如下所示
int __cdecl Add(int a, int b){
00E12300 push ebp 00E12301 mov ebp,esp 00E12303 sub esp,0C0h 00E12309 push ebx 00E1230A push esi 00E1230B push edi 00E1230C lea edi,[ebp-0C0h] 00E12312 mov ecx,30h 00E12317 mov eax,0CCCCCCCCh 00E1231C rep stos dword ptr es:[edi] 00E1231E mov ecx,offset _44E0C52E_AnalyseFunc@cpp (0E1F026h) 00E12323 call @__CheckForDebuggerJustMyCode@4 (0E11280h) return a + b; 00E12328 mov eax,dword ptr [a] 00E1232B add eax,dword ptr [b] } 00E1232E pop edi 00E1232F pop esi 00E12330 pop ebx 00E12331 add esp,0C0h 00E12337 cmp ebp,esp 00E12339 call __RTC_CheckEsp (0E1128Ah) 00E1233E mov esp,ebp 00E12340 pop ebp 00E12341 ret
在Add函数的汇编语言中可以看到开始的前3句,这里做如下解释
00E12300 push ebp; //进入新的函数,新函数也需要一个栈帧了,就必须将main函数的栈帧底部全部保存起来,栈顶则是作为一个新函数的栈底 00E12301 mov ebp,esp;//上一个栈帧顶部就是这个栈帧的底部 00E12303 sub esp,0C0h;//为当前栈帧开辟相应的空间
- main函数进入Add函数的示意图如下所示
当Add函数执行完之后,将执行ret 指令返回,并且esp指向Add函数栈帧底部(就是main 函数栈帧顶部), 紧接着就是从弹出保存的ebp恢复现场,这样就回到了调用Add函数之前的状态。
总结
1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
2、调用者函数使用call 指令调用被调函数,并把call 指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call 指令中);
3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
4、在被调函数中,从 ebp 的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;