目录
注:不同编译器环境的函数栈帧存在一定差异,本文使用VS2019
一、源代码理论分析
源代码:
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
return 0;
}
- 源代码的主函数和自定义函数运行时都会在栈上开辟空间,变量、参数也是存储在栈里
- 函数的创建与销毁、变量的创建与销毁都依靠寄存器实现,寄存器的作用是存储数据或地址
- 寄存器有eax、ebx、ecx ...... 和 ebp、esp,后两个寄存器指向当前函数的始末,前面的数个寄存器一般保存数据
- 参数传递、临时变量存储是依靠寄存器完成
- 在栈中,栈底是高地址,栈顶是低地址
- main函数也是需要其他函数进行调用的,由编译器实现
二、主函数的创建
1. 逐步骤调试,打开反汇编窗口和监视窗口,查询ebp、esp
这里可见ebp、esp是存在的,ebp指向0x0055f9a0,esp指向0x0055f984
ebp的值大于esp,即ebp指向高地址(栈底),esp在低地址(栈顶)
2. 逐步分析main函数创建反汇编代码
push ebp
push表示压栈,将ebp寄存器压入栈顶存储
执行之后可见esp的指向的地址发生改变,从0x0055f984变为0x0055f980,代表esp的地址减少了4字节,esp指向更低的地址
mov ebp,esp
move表示移动,将esp的值移动到ebp中,也就是ebp指向esp指向的地址,即从栈底移到栈顶
sub esp,0E4h
sub表示减法,指esp - 0E4h
0E4h是一个十六进制数,esp减去这个数之后指向更低地址的栈空间
esp指向的地址由0x0055f980变为0x0055f89c
push ebx
push esi
push edi
连续三次压栈,ebx、esi、edi压入栈顶
esp的指向的地址改变,从0x0055f89c变到0x0055f890
打开内存监视窗口,查询esp的内存地址,可以看到ebx、esi、edi的被压入栈(小端存储),这几个寄存器的值具体是什么我们暂不关注
lea edi,[ebp-24h]
lea 全名是 load effective address,加载有效地址
这行代码表示edi指向 [ebp-24h] 表示的地址,edi指向的地址为0x0055f95c
mov ecx,9
mov eax,0CCCCCCCCh
将9这个值移入到ecx中,将0xCCCCCCCC这个值移动到eax中
rep stos dword ptr es:[edi]
dword表示双字节,占4个字节
这行代码表示从 edi 指向的地址处(0x0055f95c),往高地址处(栈底)的9个双字节都写入0xCCCCCCCC,这里的9是ecx,0xCCCCCCCC是eax
经计算易得 0x0055f95c 到 ebp的地址 0x0055f980正好相差36,即36字节
打开内存监视进行查看地址0x0055f95c 到 0x0055f980 之间的值
edi最后指向edp指向的地址(main函数栈帧的起始地址)
接下来的 move 和 call 指令我们先暂不关注,call 指令是函数调用,并存储下一条指令的地址,部分编译器没有这两条语句或执行方式不同,这两行代码主要与主函数创建有关,代表主函数正是创建成功,接下来可以开始执行C语言代码
三、c语言代码的汇编
1. int a = 10;
mov dword ptr [ebp-8],0Ah
将0Ah移入ebp - 8 的地址处,ebp - 8的地址就是main函数空间中距离ebp8个字节的地方
0Ah是十六进制数代表十进制数10
通过mov完成赋值
2. int b = 20; int c = 0;
mov dword ptr [ebp-14h],14h
mov dword ptr [ebp-20h],0
同理,进行赋值
3. c = Add(a, b);
mov eax,dword ptr [ebp-14h]
push eax
将 ebp - 14h处的值存入寄存器eax中并将eax压栈
这里也就是复制b的临时变量进行传参
mov ecx,dword ptr [ebp-8]
push ecx
同理,将ebp - 8处的值存入寄存器ecx中并将ecx压栈
这是复制a的临时变量进行传参
由此可见函数在传参的时候,是从右到左进行传参的
4. 调用Add()函数
call 009711B3
将call指令的下一条指令的地址压栈保存,然后进入Add()函数体内
进入函数体内之后可观察到函数栈帧的创建和main函数栈帧的创建是一样的
函数栈帧创建完毕之后再开始执行C语言代码
int z = 0;
mov dword ptr [ebp-8],0
移动赋值
5. 参数的运算
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
mov dword ptr [ebp-8],eax
由上图可得,ebp + 8的地址处是ecx,也就是a的临时变量10
将10移动赋值给eax,此处的eax是另一个寄存器,不再是main函数保存参数时压栈的eax寄存器
ebp + 0Ch的地址处是b的临时变量20,将10 与20 相加得 30,存入eax寄存器中
ebp + 8 和 ebp + 0Ch分别是参数x 和 y 的值
最后将eax的值移动赋值给ebp - 8的地址处,也就是z的值
6. 返回值
return z;
mov eax,dword ptr [ebp-8]
将ebp - 8地址处的值赋值到eax寄存器中,也就是z的值
红色框内的汇编代码代表了函数栈帧的销毁
pop edi
pop esi
pop ebx
add esp,0CCh
pop表示出栈,将元素弹出栈空间(销毁)
esp + 0CCh,代表esp指向更高地址的空间(指向ebp指向的地址,退出Add函数)
cmp ebp,esp
call 00971235
mov esp,ebp
这几条指令不关注,最后是将ebp的值给esp,也就是回收Add函数的栈帧空间
pop ebp
ebp出栈,之前Add函数栈帧创建的时候,最开始的那个push ebp 指令是将上一个ebp的地址压栈存储了,此时pop ebp,ebp将回到上一个ebp指向的地址
ret
ret是回到call指令下一条指令的地址,之前call的时候将call指令的下一条指令的地址也是进行了存储了的,也就是重新回到了主函数,并从call指令的下一条指令的位置继续执行
7. 变量得到返回值
add esp,8
mov dword ptr [ebp-20h],eax
esp + 8 ,代表回收参数的栈空间
将eax的值赋给 ebp - 20h 处,eax存储的是返回值z
加法函数调用结束,加法运算完成
之后的若主函数还有代码则继续执行,若没有代码了则回收主函数栈帧