目录
1.函数栈帧的含义
概念
要用到的汇编语言的知识
示例
2.理解栈帧
2.1 main函数栈帧的创建
2.2 局部变量的创建
2.3 函数传参
2.4 调用函数
2.5 函数返回
一个.c文件在调用函数的时候(包括main 函数),其内存中的栈区有什么变化?要压栈、出栈哪些寄存器呢?函数的参数是如何进行传递的呢?函数调用结束之后栈区又是如何变化的呢?本文通过使用汇编语言,对这些内容进行了较为详细的剖析。
1.函数栈帧的含义
概念
首先,栈的概念想必不需要过多解释,那么什么是栈帧?引用百度百科:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从这句话中,可以提炼以下几点信息:
· 栈帧是一块因函数运行而临时开辟的空间。
· 每调用一次函数便会创建一个独立栈帧。
· 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
· 当函数运行完毕栈帧将会销毁。
我们知道,C语言的内存区分成了 静态区、栈区、堆区,函数栈帧无疑是在栈区创建和销毁的,所以其要符合栈“后进先出”的特点。
要用到的汇编语言的知识
在这里使用汇编语言方面知识的原因是:通过它,我们可以深入底层了解一个程序是如何运行的,在何时——什么东西压栈,什么东西出栈,寄存器(汇编语言中一些用来暂时存储数据的东西)如何变化等等。这些都是C语言无法直观体现的,我们可以通过Visual Stdio 的在调试时的反汇编功能,将C语言代码转换成汇编语言代码,以便更好地观察。(另,C语言也是汇编语言编写的。)所以,简单地说,本文主要是在分析汇编语言的执行过程。
我们首先要了解几个汇编语言方面的东西,其中ESP和EBP时专门维护函数栈帧的,分别指向栈顶和栈底:
寄存器 |
用途 |
EAX |
累加寄存器:用于乘除法、函数返回值 |
EBX |
用于存放内存数据指针 |
ECX |
计数器 |
EDX |
用于乘除法、IO指针 |
ESP |
存放栈顶指针(其值是地址) |
EBP |
存放栈底指针(其值是地址) |
汇编指令 |
用途 |
mov |
mov A,B 将数据B移动到A |
push |
压栈 |
pop |
出栈 |
call |
函数调用 |
add |
加法 |
sub |
减法 |
rep |
重复 |
lea |
加载有效地址 |
示例
比如,我们写下一个如下的C语言程序,非常容易,只有main() 函数和一个 Add() 函数,主函数里面调用了 Add() 。
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
return 0;
}
那么,一开始调用主函数的时候,主函数的函数栈帧就压栈;然后在主函数里面,调用了Add() 函数,此时Add() 的函数栈帧也要压栈。那么现在面临一个问题,是维护Add() 函数,还是维护main() 函数,亦或两者都维护?
其实,从平时使用Visual Stdio 调试的时候就可以看出来,当主函数内部调用一个函数A,按F11分步调试,会进入函数A的内部,函数A调用结束,会返回主函数。同理,实际上从进入函数A,一直到A 函数调用结束,这个过程都在维护函数A。所以,在main() 函数内部调用 Add() 函数之后,会出现如下图所示的情况,ebp和esp来维护Add() 函数的栈帧。当Add() 函数调用结束,它的函数栈帧自然就会出栈,此时ebp、esp又会返回维护main() 函数的栈帧。
![](https://img-blog.csdnimg.cn/92a22b8e0b324423876b4c83773e532d.png)
2.理解栈帧
2.1 main函数栈帧的创建
实际上,main() 函数也是由其他函数调用的,其调用链条如下:
![](https://img-blog.csdnimg.cn/6130105d58124771a1b60f77b44b494b.png)
创建main函数的函数栈帧代码如下,汇编语言的注释是用 ; 所以这里 ; 后面的内容是注释,帮助理解代码。
006117A0 push ebp ; ebp 压栈
006117A1 mov ebp,esp ; 将 esp里面的值 赋给ebp
006117A3 sub esp,0E4h ; esp减去0E4h(十六进制),得到的结果赋给esp
006117A9 push ebx ; ebx 压栈
006117AA push esi ; esi 压栈
006117AB push edi ; edi 压栈
006117AC mov edi,[ebp-24h] ; 将ebp往上数,第24h(16进制)个字节开始,向下4个字节的值赋给edi
006117AF mov ecx,9 ; 这里到结束的意思是:
006117B4 mov eax,0CCCCCCCCh ; 将0CCCCCCCCh 赋值给某块空间,这块空间从附加段中 edi 指向的位置开始
006117B9 rep stos dword ptr es:[edi] ; 一共执行九次(0CCCCCCCCh 是四个字节的内容,每次操作四个字节,所以一共操作了36字节)
第一行
我们来开始逐句剖析上方代码,首先执行第一行(图中红色圆圈圈出来的黄色箭头,表示已经执行完其上一行,按F10调试就执行当前行),由于是压栈操作,所以esp的值会有所变化,如下图右边监视窗口,esp的值(十六进制显示的)相较之前改变了,所以变成红色:
![](https://img-blog.csdnimg.cn/dfae96faa5a049b79028b71a1d4fda5b.png)
如下,ebp压栈,同时esp上移:
![](https://img-blog.csdnimg.cn/160ff1bbf82946319b59b8299786ca83.png)
第二行
该行是将esp的值赋给ebp,效果也如下图,右边监视窗口的红色部分所示。
![](https://img-blog.csdnimg.cn/16bb827d54824c3896fe75fb4703a59e.png)
如下,将esp的值赋给ebp之后,ebp和esp指向同一块地方:
![](https://img-blog.csdnimg.cn/5cd67decfc32454184e3e0757c6e8217.png)
第三行
该行是将esp的值减去0E4h(十六进制),得到的结果赋给esp,如下图。
![](https://img-blog.csdnimg.cn/463ecdeaf29248a180892def9bad87b8.png)
如图所示,由于图中从下往上是地址高处到地址低处,所以esp值变小,实际上图中是上移。并且,现在ebp和esp维护的空间,就是main函数的函数栈帧:
![](https://img-blog.csdnimg.cn/a2b64ffc4332452eb9931c5b8b56a078.png)
第四行
压栈,压入ebx,改变栈顶指针esp的值。
![](https://img-blog.csdnimg.cn/a419932c71524721bd117b541b5a42d3.png)
如下,压栈,esp上移:
![](https://img-blog.csdnimg.cn/e653b69ec22547fab2cd4fbe8512aba5.png)
第五行
压入esi,改变esp的值。
![](https://img-blog.csdnimg.cn/3b8f5688ca274755a373a23c5e9c7da7.png)
如下,和上一步类似:
![](https://img-blog.csdnimg.cn/9a4cc549924a4b0a9fbf1fab5a77ed0b.png)
第六行
压入edi,改变esp的值。
![](https://img-blog.csdnimg.cn/1b04dccc85394d439dd4664134136717.png)
和上一步也类似:
![](https://img-blog.csdnimg.cn/24c56caf01c646a8a2f8ec6bcc58597c.png)
第七行
将[ebp-24h] 表示的地址赋值给edi。
![](https://img-blog.csdnimg.cn/2910ce90bc6645aa8fc309ef365342ce.png)
这里就是把 edi 里面的值改变,从函数栈帧看不出什么,看上面的监视图就可以直到确实是改变了。
最后三行
如之前代码里的注释所说。
![](https://img-blog.csdnimg.cn/42e63fb220c44facbe9c3803e6ac1157.png)
![](https://img-blog.csdnimg.cn/57a1e3bed92d4d87b01ea001a749db5c.png)
如下,两个箭头指示的值是一样的,其代表的是edi所表示的地址,从该地址开始,往后9个dw(double word 双字,一个双字等于四个字节)的内容,都赋值为cccccccc (十六进制)。
![](https://img-blog.csdnimg.cn/6437c625cca84e1f947fd0f898f2f88b.png)
这三行代码效果如下:
![](https://img-blog.csdnimg.cn/888ba6148d0a46cfb38cd7da945a2138.png)
整个过程可以用一张动图生动形象地展示:
![](https://img-blog.csdnimg.cn/20210808202830393.gif)
2.2 局部变量的创建
接下来,我们在汇编代码中鼠标右击,然后将下图红色箭头所指示的"显示符号名" 的勾去掉。
![](https://img-blog.csdnimg.cn/27ad9714a8eb4b34907e888a48c30420.png)
发生改变的是下图中红色圆圈圈出来的,可以看出,原本所有的变量名,都变成了寄存器减去某个十六进制数字。他们实际上是等价的,即变量的地址就等于替换后的地址。
![](https://img-blog.csdnimg.cn/7c9fe3bd357141f4ab1e90255006b522.png)
接下来分析局部变量创建过程。
首先,创建变量a,代码如下,其含义就是,将0Ah 这个十六进制数字,从ebp的地址低八位处开始放,占四个字节:
002C17C5 mov dword ptr [ebp-8],0Ah
如下图,可以通过两个红色箭头看到,右边监视的ebp的值就是左边 地址处的箭头指向的地址,说明这就是ebp的地址,然后减去八位,再根据栈从下往上使用以及Visual Stdio小端存储的特点,就成了内存区里面红色方框框出来的内容。(注意,比如 cc cc cc cc ,cc占据的是一个字节的空间,四个cc 就占据四个字节,而汇编语言中,地址-1,只跳过一个c,所以ebp-8是跳过8个c,即四个字节)
![](https://img-blog.csdnimg.cn/9114da058dbe4647a5f6e3cdd576ef23.png)
如下,图中一个小格子代表四个字节,不难看出,变量a存储的位置,在栈底指针往上跳过四个字节的地方。
![](https://img-blog.csdnimg.cn/d8b4838a6d6c451da40ad63b93a5f050.png)
然后创建局部变量b,通过内存图可以看出,变量b和变量a是间隔八个字节的:
![](https://img-blog.csdnimg.cn/2a020cd5d7cc4c1ba0f15734330e0fe0.png)
如下图:
![](https://img-blog.csdnimg.cn/ea6c2d16c0ec495dafd90ac0d43eb2d4.png)
2.3 函数传参
代码如下:
002C17D3 mov eax,dword ptr [ebp-14h] ; 将变量b的值赋给eax
002C17D6 push eax ; eax压栈
002C17D7 mov ecx,dword ptr [ebp-8] ; 将变量a的值赋给ecx
002C17DA push ecx ; ecx压栈
执行前两行代码,确实将变量b的值赋给了eax,然后eax引起的压栈导致了esp改变:
![](https://img-blog.csdnimg.cn/6c7a216e29a24e72b491dbb4e0bcc1e8.png)
如下图,不要忘了eax里面的值和变量b是一样的哦:
![](https://img-blog.csdnimg.cn/e2bc260c564f4d8eb8324ee1bb52c314.png)
执行后两行代码:
![](https://img-blog.csdnimg.cn/862ca98a6a53444cbc7873bf09905eba.png)
其效果和前两行类似,同时ecx里面存的是变量a的值:
![](https://img-blog.csdnimg.cn/850c05933fa44cfd988170462b869a4c.png)
2.4 调用函数
上面的内容执行完之后,要执行如下语句,其意思是,执行 002C10B4 地址处的内容:
002C17DB call 002C10B4
然后我们将其滑倒该地,发现是这样的,意思是跳到002C1740地址处:
![](https://img-blog.csdnimg.cn/7e25b59a6dd84c13a9b2c261d01bba8b.png)
又找到该地址,发现如下,所以,通过这两步调用Add() 函数,如下红色部分,和创建main函数的函数栈帧类似,实际上就是创建了Add() 的函数栈帧:
![](https://img-blog.csdnimg.cn/fc2cb524ddda4a369fe12b09236644b2.png)
效果如下,建立了Add函数的函数栈帧:
![](https://img-blog.csdnimg.cn/76b11b39a19a4b6aa53d34d0196574f8.png)
红色个方框后面两行代码不是很重要,是用来检查bug的,如下代码和图片:
002C1757 mov ecx,2CC003h
002C175C call 002C130C
![](https://img-blog.csdnimg.cn/645b339ad47a4a95b58dc8114b4dd730.png)
![](https://img-blog.csdnimg.cn/0455c74096de4f9090c1e8502778e392.png)
2.5 函数返回
函数返回:
002C1761 mov eax,dword ptr [ebp+8]
002C1764 add eax,dword ptr [ebp+0Ch]
第一行代码: 将ebp+8 地址处的数据放到eax 。
第二行代码:将ebp+0Ch 地址处的数据和eax相加,结果存到eax里面。
如下图中,由于图片从下往上是地址从高到低,所以图片中ebp+8是在ebp下方。实际上就是ecx和eax的值相加,然后存到eax里面。eax里面存储变量b的值,ecx里面存储变量a的值,最后eax的值就是变量a、b之和。并且eax是不会随着Add() 函数的函数栈帧销毁而改变值。
![](https://img-blog.csdnimg.cn/843c9ae089e54e2eae5f24e0701469b5.png)
通过监视也可以看出,eax的值变成0x0000001e,转换成十进制就是30。
此时已经拿到返回值,存储在eax里面,还要执行以下几行代码:
00AA13F1 pop edi
00AA13F2 pop esi
00AA13F3 pop ebx
00AA13F4 mov esp,ebp
00AA13F6 pop ebp
00AA13F7 ret
就是出栈、赋值等等,结果如下,回到了调用Add() 函数之前的状态:
![](https://img-blog.csdnimg.cn/4b3acdf7a75a40e8b4bfc4b7d361b22d.png)
然后执行main() 函数后续代码代码,如下图红色框出:
![](https://img-blog.csdnimg.cn/bb45f1f960634202a5faead690172cc4.png)
第一行:esp加8,即esp在途中向下移动四个字节。
第二行,将eax的值赋给ebp-20h 地址处。
执行完之后,调试图如下,通过对比两个红色方框的内容,左边红色方框的地址,和右边&c 的值一样,说明那就是变量c 存储的地方,其值也是变量c 的值:
![](https://img-blog.csdnimg.cn/35974ae49cdd4150b1ca3cfe00fc4640.png)
示意图如下:
![](https://img-blog.csdnimg.cn/ec66bdcad74240eb8077a5278a1a9aab.png)
通过对函数栈帧创建、销毁过程的剖析使我们不仅了解计算机做了什么,还了解了它是如何做的。通过函数栈帧尝试解析递归等问题相信也会更加直观。由于本人水平有限,不足之处还请大家多多指教。