目录
🍖前言
在C语言的学习过程中,我们会遇到许多问题,它是比较隐晦难懂的,但是这些好巧不巧有对以后的学习有着深远的作用。我们在学习完函数这一章后,有一些问题它是在许多书本上是不会讲解的,太过于接近底成。函数中:1局部变量是怎么创建的?2为什么局部变量的值是随机值?函数是怎么传参的?传参的顺序是怎么样的?3形参和实参是什么关系?4函数调用是怎么做的?5函数结束后是怎么返回的?这些问题大家肯定经常困惑,今天这篇函数的栈帧与销毁就是对这些问题进行讲解。
🍕理解需知
🫕常用的寄存器
- eax: 通常用来执行加法,函数调用的返回值一般也放在这里面
- ebx: 数据存取
- ecx: 通常用来作为计数器,比如for循环
- edx: 读写I/O端口时,edx用来存放端口号
- esp: 栈顶指针,指向栈的顶部
- ebp: 栈底指针,指向栈的底部,通常用
ebp+偏移量
的形式来定位函数存放在栈中的局部变量 - esi: 字符串操作时,用于存放数据源的地址
- edi: 字符串操作时,用于存放目的地址的,和esi两个经常搭配一起使用,执行字符串的复制
🍘常用的汇编指令
MOV 传送字或字节.
MOVSX 先符号扩展,再传送.
MOVZX 先零扩展,再传送.
PUSH 把字压入堆栈.
POP 把字弹出堆栈.
PUSHA 把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈.
POPA 把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈.
PUSHAD 把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI依次压入堆栈.
POPAD 把EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX依次弹出堆栈.
BSWAP 交换32位寄存器里字节的顺序
CMPXCHG 比较并交换操作数.(第二个操作数必须为累加器AL/AX/EAX)
XADD 先交换再累加.(结果在第一个操作数里)
🍿函数的栈帧与销毁
为了方便演示,这里编写了一个加法函数,我们通过调试和观察它的反汇编来理解函数的栈帧与销毁。
🥙函数的创建
在开始前,我们了解两个小知识:函数和局部变量是在栈区中创建的,栈区的存放习惯是从高地址到低地址存放的。还是两个非常重要的寄存器ebp和esp,他们两叫栈底指针和栈顶指针,函数的栈帧就是有他们来控制的。(注意尽量使用低级的编译器,这样子观察比较容易,高级的封装的比较完善,不宜观察)
🫔main函数栈帧的创建
这里有一个问题,main函数,也是函数,那它也会被别人调用吗?答案是对的,我们可以通过调试中的调用堆栈来观察,果然我们发现的确有一个函数调用它,而这个函数又被另一个函数调用,用画图解释:
这里我们回到正文,通过反汇编我们一步一步来解读函数(上面那个main函数的栈帧是一个简单的图,下面我们会详细来画)。
第一条指令是push一下ebp就是将ebp压入栈顶,因为esp是栈顶指针,这时esp就得向上移动一次,我们可以通过监视观察到esp移动了。
mov ebp,esp 和sub esp,0E4h是说将esp赋给ebp,就是ebp的位置到了esp上。esp减去0E4h,这之间的空间就是main函数的栈帧开辟
00FB18B9 push ebx 00FB18BA push esi 00FB18BB push edi 这三条指令就是将ebx,esi,edi压栈,随后esp向上移动三次。
00FB18BC lea edi,[ebp-24h] 将ebp减去24h加载到edi中
00FB18BF mov ecx,9 将9赋给ecx
00FB18C4 mov eax,0CCCCCCCCh 将0cccccccch赋给eax
00FB18C9 rep stos dword ptr es:[edi] edi开始将0cccccccch9次向下赋给下面的单元空间
🍥main函数里面语句的执行
一直到这里算是main函数的栈帧完全开辟好了。接下来就是main函数里面语句的执行了。
int a = 10;
00FB18D5 mov dword ptr [ebp-8],0Ah 将10赋给ebp-8这个空间
int b = 20;
00FB18DC mov dword ptr [ebp-14h],14h 将20赋给ebp-14这个空间
🥟 函数传参
int c = add(a, b);
00FB18E3 mov eax,dword ptr [ebp-14h] 将b的值赋给eax这个寄存器
00FB18E6 push eax 将eax压栈,esp向上移动一次
00FB18E7 mov ecx,dword ptr [ebp-8] 将a的值赋给ecx这个寄存器
00FB18EA push ecx 将ecx压栈,esp向上移动一次
00FB18EB call _add (0FB1023h) 用call指令调用add函数,且记住下一条指令
通过这些指令,我们可以发现函数传参是从右往左的
🍤add函数栈帧的创建
这里执行下一条语句的时候我们就要按F11进入add函数了
0FB1850 push ebp
00FB1851 mov ebp,esp
00FB1853 sub esp,0CCh
00FB1859 push ebx
00FB185A push esi
00FB185B push edi
00FB185C lea edi,[ebp-0Ch]
00FB185F mov ecx,3
00FB1864 mov eax,0CCCCCCCCh
00FB1869 rep stos dword ptr es:[edi]
这些画图板里有解释,我就不重复了
🥣add函数语句的执行
int z = 0;
00FB1875 mov dword ptr [ebp-8],0
z = x + y;
00FB187C mov eax,dword ptr [ebp+8]
00FB187F add eax,dword ptr [ebp+0Ch]
00FB1882 mov dword ptr [ebp-8],eax
return z;
00FB1885 mov eax,dword ptr [ebp-8]
这里我们可以发现,形参x和y只是通过mov指令将a和b的值拷贝了一份放到eax寄存器中加起来放到了z中,返回的时候将z的值放回寄存器eax中,这样就不会因为函数的销毁数据丢失.
🍠函数的销毁
🍡add函数的销毁
00FB1888 pop edi
00FB1889 pop esi
00FB188A pop ebx
00FB188B add esp,0CCh
00FB1891 cmp ebp,esp
00FB1893 call __RTC_CheckEsp (0FB1244h)
00FB1898 mov esp,ebp
00FB189A pop ebp
00FB189B ret
这里是通过pop和add指令将add函数的空间释放掉,让esp和ebp回到原来main函数的位置。
🥞main函数的销毁
00FB1909 pop edi
00FB190A pop esi
00FB190B pop ebx
00FB190C add esp,0E4h
00FB1912 cmp ebp,esp
00FB1914 call 00FB1244
00FB1919 mov esp,ebp
00FB191B pop ebp
00FB191C ret
上面的指令和add函数销毁是一样的,这里我就不画图讲解了,可以根据上面的解释自行理解。
🍉总结
现在,对于一开始的那些问题,相比大家就已经清楚了叭:
1局部变量是怎么创建的? 是函数栈帧创建后编译器分配,且是从高地址到低地址创建的
2为什么局部变量的值是随机值? 因为函数栈帧创建后编译器会将全部的内容都自动初始化一个值
3函数怎么传参?传参顺序是怎么样 传参将实参拷贝到寄存器中进行压栈到栈顶 顺序是从右到左
4形参和实参是什么关系? 形参是实参的临时拷贝,它们只是值相同,但是地址不同
5函数调用是怎么做的? 通过call指令来调用
6函数结束后是怎么返回的 通过ret指令来返回