本节我们将以两数相加的函数讲解函数栈帧的创建与销毁
#define _CRT_SECURE_NO_WARNINGS 1
#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 c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
在介绍之前,我们先介绍关于寄存器的知识:
像eax,ebx,ecx,edx,ebp,esp这些都是CPU上通用寄存器的名称
ebp,esp这俩个寄存器中存放的是地址,这俩个地址是用来维护函数栈帧的
每一个函数调用,都要在栈区创建空间,main函数也是被其他函数所调用的,所以调用main函数也要在栈区创建空间
在main函数中,调用了Add函数,所以Add函数也在栈区创建一块空间
下图为这段代码在栈区开辟空间图解:
当我们按(Fn)+(F10)开始调试代码后,在调试-->窗口-->反汇编 即可打开代码所对应的汇编代码,具体操作如下图:
接下来我们首先研究main函数的汇编代码
第一列:001517C0......是该条指令的地址
简单介绍一下几条汇编指令
- mov:传送指令,把一个字节,字或双字的操作数从源位置传送到目标位置,源操作数的内容不变。第一个操作数为目标位置,第二个操作数为源位置。(mov ebp,esp:把esp指针指向的值赋给ebp指针指向的内容)
- push:压栈操作,将一个寄存器中的数据压入栈底
- pop:弹栈操作,栈顶元素出栈且有一个寄存器接收数据,栈顶指针减一
- add:加法,两个操作对象相加(寄存器的内容与数据相加)
- sub:减法,两个操作对象相减(寄存器的内容与数据相减)
- lea(load effective address装入有效地址),它的操作数就是地址,常见的用法:
- lea eax,[addr]:将表达式addr的值放入eax寄存器(右操作数为一个指针,该条指令与 mov eax,addr等价)
- lea eax,dword ptr[ebx]:将ebx的值赋给eax
- lea eax,c:其中c为一个int型的变量,该指令的意思是把c的地址写入eax中
- rep(repeat):重复前缀指令,每执行一次,ecx减一,直到ecx减为0,重复执行结束
- stos(store string):串存储指令,将eax中的数据传送到目的地址(默认为es:[edi])
- call 标号:将下一条指令所在地址压栈,转到标号处执行指令
- cmp:比较指令,相当于减法指令sub,它不保存结果,会影响相应的标志位
- ret:子程序的返回指令
- xor:异或操作,与 AND 指令及 OR 指令相同。两个操作数对应位遵循操作原则:如果两个位的值相同(同为 0 或同为 1),则结果位等于 0;否则结果位等于 1。如果两个操作数为寄存器,则相当于清0
我们发现在创建变量之前,还有一段代码
push ebp:把ebp压入栈
注意:ebp中存放的地址是维护栈底的指针,esp中存放的地址是维护栈顶的指针,每次压栈,esp会自动指向栈顶
执行完这一条语句后栈区状态如下图:
mov ebp,esp;把esp的值赋给ebp
sub esp,0E4h;esp的值减0E4h(十进制228)
执行完这俩条语句后栈区状态如下图:
push ebx
push esi
push edi把ebx,esi,edi依次压入栈
执行完这三条语句后栈区状态如下图:
lea edi,[ebp-24h];把ebp-24h中的内容写入寄存器edi
mov ecx,9;ecx=9
mov eax,0CCCCCCCCh;把0CCCCCCCCh这个十六进制数写入eax
rep stos dword ptr es:[edi];将eax中的数据传送到es:[edi],且重复执行
mov ecx,15C003h;修改ecx的值为15C003h
call 0015131B ;将下一条指令所在地址001517E5压栈,程序转移到0015131B处执行完这六条语句后,栈区的内容如下图
从ebp向低地址9个单元的内容被修改成cc cc cc cc
这几条指令的汇编代码在不同编译器下有区别,这里展示的是VS2019编译器下的运行结果
mov dword ptr [ebp-8],0Ah
mov dword ptr [ebp-14h],14h
mov dword ptr [ebp-20h],0执行完这三条语句后,a,b,c三个变量被创建并赋值
接下来执行c = Add(a, b);我们可以看到函数传参过程
由以上汇编指令,我们可以看到函数调用时,传参方式是由后向前传参
在这段代码中,先传递变量b,再传递变量a,并对应有压栈操作
call 001513C0 ;将下一条指令所在地址00151807压栈,程序转移到001513C0处
add esp,8 ;add指令:esp = esp + 8(十进制)//栈顶指针加一个数,弹栈操作
mov dword ptr [ebp-20h],eax ;把eax中的值赋给ebp-20h单元执行完这一段代码后,我们再次看一下内存分布情况
接下来我们使用快捷键(Fn)+F11进入Add函数
进入函数后,我们发现这一段汇编代码和main函数中开始的一段代码类似
这是在为Add函数创建栈帧 ,每一个函数调用都需要在栈区开辟一块空间
执行这段代码之后,从ebp向低地址3个单元的内容被修改成cc cc cc cc
栈区的分布如下图
函数栈帧创建完成后,进入正式的运算部分
mov dword ptr [ebp-8],0 ; ebp-8单元的内容赋值为0
mov eax,dword ptr [ebp+8] ;eax中的内容赋值为ebp+8单元的内容
add eax,dword ptr [ebp+0Ch] ;eax中的内容与ebp+0Ch单元的内容相加
mov dword ptr [ebp-8],eax ;ebp-8单元的内容赋值为eax单元的内容
mov eax,dword ptr [ebp-8] ;eax单元的内容赋值为ebp-8单元的内容//程序最终的结果为寄存器eax中存放30
这段代码是在Add函数中的
pop edi
pop esi
pop ebx ;这三条指令将edi,esi,ebx弹栈
add esp,0CCh ;栈顶指针esp+0CCh//释放栈区Add函数开辟的空间,与sub esp,0CCh对应
cmp ebp,esp ;ebp-esp
call 00151244 ;将下一条指令所在地址001517B8压栈,程序转移到00151244处
mov esp,ebp ; esp=ebp
pop ebp ;ebp弹栈
ret ;子程序调用结束
接下来回到主程序
add esp,8 ;esp+8,弹栈操作
mov dword ptr [ebp-20h],eax ;[ebp-20h] = [eax]printf("%d\n", c);
mov eax,dword ptr [ebp-20h] ;[eax] = [ebp-20h]
push eax ;此时eax的内容为30
push 0B47B30h
call 00B410CD
add esp,8 ;esp+8,弹栈操作
return 0;
xor eax,eax ;寄存器eax清零
这部分和Add函数同理,当函数调用结束后,CPU会进行栈区空间的释放
pop edi
pop esi
pop ebx ;这三条指令将edi,esi,ebx弹栈
add esp,0E4h ;栈顶指针esp+0E4h//释放栈区Add函数开辟的空间,与sub esp,0E4h对应
cmp ebp,esp ;ebp-esp
call 00151244 ;将下一条指令所在地址001517B8压栈,程序转移到00151244处
mov esp,ebp ; esp-ebp
pop ebp ;ebp弹栈
ret ;子程序调用结束,因为main函数也是被其他函数调用的
当我们看到main has returned;exit somehow...时,证明整个程序已经运行结束