栈空间
程序的虚拟内存空间可以分为内核空间和用户空间,栈空间就是从用户空间的最高内存地址开始向下增长的空间。栈空间在程序运行期间,主要作用就是维护函数调用的上下文,栈的数据结构(后进先出)也与函数调用流程相符。
函数调用栈
函数调用栈,是将每个函数所用的信息,称之为活动记录或者栈帧,按照调用的顺序依次压入栈中(保存在栈空间中),等最上层的函数执行完了,就弹出相应的栈帧,栈帧主要包括以下几个内容:
- 函数的参数和返回地址
- 旧的EBP
- 保护寄存器的值
- 局部变量
- 其它数据
下面详细地解释函数调用的整体流程,以及为什么要保存这些信息
函数调用流程
我们以一个简单的C++程序来实验下函数调用的整体流程
#include <iostream>
using namespace std;
int Add(int a, int b)
{
int c = 0;
c = a + b;
return 0;
}
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);
cout << ret << endl;
}
反汇编(visual studio2022 windows debug x-86)如下图所示
-
push ebp
将EBP里的值入栈
-
move ebp,esp
将栈顶指针的地址传给EBP
-
sub esp,0E4h
ESP减去0E4h,相当于把栈顶指针向下移,预留栈空间
-
push ebx
-
push esi
-
push edi
到这就做好了寄存器的保护
-
lea edi,[ebp-24h]
mov ecx,9
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
,这几个指令表示从 EBP-24h 到 EBP 的栈空间的值设置为 0xCCCCCCCCh
-
mov dword ptr [a],0Ah
mov dword ptr [b],14h
mov dword ptr [ret],0
将局部变量赋值,可以查看到局部变量所在的地址
EBP 的值为 0x0137fb84,之前将 0x0137fb84 ~ 0x0137fb60 之间的值设为 0xCCCCCCCCh,现在将中间的a
,b
,ret
局部变量设为对应值,图示为连续的地址,其实不是连续的,从上面的地址能看出来
下面就要在main函数中调用一个函数Add
-
mov eax,dword ptr [b]
push eax
mov ecx,dword ptr [a]
push ecx
将参数压栈(这里是从右向左压栈)
-
call Add
调用Add函数,分为两步,一个是将EIP压栈,一个是跳转到对应函数地址开始执行(将EIP的值改为跳转命令的地址,然后执行跳转命令,EIP的值就改为了Add函数的开始地址),Add函数反汇编如下图所示
-
下面这部分和之前main函数入栈一样,将保护寄存的值入栈,并将栈区部分初始化为 0CCCCCCCCh,这里只有局部变量 c
-
mov dword ptr [c],0
mov eax,dword ptr [a]
add eax,dword ptr [b]
mov dword ptr [c],eax
将 c 赋值为0,再赋值为 a+b,可以看下a,b,c的地址,可以看到这里的a,b就是我们在main中压栈的a,b,当时esp为0x0137fa94,压栈b->0x0137fa90,压栈a->0x0137fa8c
-
后面就是把eax清零(因为这里是返回0,如果返回的是c,会把eax的值改为c的值,这个在main函数的反汇编中可以看到 将eax的值赋给ret),在把保护寄存器的值出栈
-
add esp,0CCh
mov esp,ebp
将ESP改为EBP的值,相当于将中间的局部变量等出栈
-
pop ebp
把旧的EBP的值弹出给EBP寄存器,而旧的EBP的值就是在main函数中EBP寄存器的值,此时EBP也回到main函数的状态
-
ret
就是将返回地址弹出到EIP中,这样下面会继续执行main函数的后续指令,接续到main
-
回到main函数继续执行,
add esp 8
就是把之前压栈的参数 a 和 b 出栈了,此时栈区回到调用 Add 函数前的状态,然后mov dword ptr [ret],eax
如上面所说,将返回的值保存在eax,然后赋给ret -
可以看到 main 函数返回的时候也执行了类似的操作,这样也就回到了main函数调用前栈区的状态