写在前面:
你想知道为什么用vs在越界的时候会打出“烫烫烫烫烫”?
你想知道临时变量在函数调用结束后是怎么销毁的吗?
欢迎关注我♥,订阅专栏 0基础C语言保姆教学,
就可以持续读到我的文章啦😀🐕~~~~
这里都是满满的干货,从零基础开始哦~,循序渐进😀,直至将C中知识基本全部学完🐂。
本文为第九节——从底层汇编的角度简单理解函数栈帧的创建和销毁 文末附前八张链接哟👉
我们在现在,其实已经比较清楚函数是怎么样运行的了,包括怎样传参 、函数调用等等。但是呢,这样也只是理解到了会用的地步,
其底层的原理是怎样的,到底是如何调用的?我们本节内容将会来做详细探讨。
首先,我们需要知道,函数栈帧的创建和销毁是在栈区中完成的。每一次地函数调用都有栈帧的创建和销毁。
而系统在栈区内使用地址时是从高地址往低地址使用。就是说,先使用高地址,再使用低地址。
我们简单地画一个图
然后,我们需要了解这两个寄存器:ebp 和 esp
它们都是在函数创建栈帧的时候来去使用。用来维护函数栈帧。
其中,
ebp(栈底指针),存储着栈底的地址
esp(栈顶指针),存储着栈顶的地址
我们简单地来去写一下这么个程序:
为了便于理解,我将此代码拆分地足够细。
ok,现在我们开始来看其底层到底是怎么实现的。
我们按住F10,让代码运行起来,然后转到反汇编,打开内存和监视。
反正就看到这样一个乱七八糟的东西。
左边一行一行的实际上都是汇编代码,需要注意一下的是,第11行和12行我们暂不分析,因为这是vs2019自己弄的东西,进行了一些优化。如果用vs2013甚至更老的版本就基本不会出现。(不同的编译器、环境对函数栈帧创建销毁的过程大同小异)
要注意,首先需要为main函数创建栈帧,所以,我们前面的若干行都是在为main函数搞事情。
我们来一点一点分析:
前三行:
002617C0 push ebp
它的意思是压栈,将ebp压栈。
002617C1 mov ebp,esp
意思是将ebp的地址的那个值给esp。
此时,我们的栈区的图可以理解为这样:
(此时的栈区图)
接着,
002617C3 sub esp,0E4h
表示将esp的值减掉0E4h,0E4h是一个十六进制数字,代表的是0x00 00 00 e4
那么这个时候,这个图就变成了这样:
我们可以让代码走起来,来看看ebp,esp的值是不是像我们所说的那样。
确实是这样。
我们接着往下看:
4-6行:
002617C9 push ebx
002617CA push esi
002617CB push edi
它们的意思都是一样的。push... 就是将...压入栈中
他分别将ebx esi edi压入栈中(它们都是寄存器的类型)
那么此时,我们得到的栈区图就是这样:
第七行到第十行是为了干一件事情,我们来看:
002617CC lea edi,[ebp-24h]
002617CF mov ecx,9
002617D4 mov eax,0CCCCCCCCh
002617D9 rep stos dword ptr es:[edi]
002617CC lea edi,[ebp-24h]
//含义为读取ebp-24h到edi之间的地址,将ebp-24h赋给edi
mov ecx,9
//含义是将ecx的值变为9
同理,
mov eax,0CCCCCCCCh
//含义是将eax的值变为0CCCCCCCCh
继续,
002617D9 rep stos dword ptr es:[edi]
//意为把从edi下ecx(0Ch)个数据
(或者说dword这么多次、这么多个数据)全部都改为eax的0CCCCCCCCh
然后让edi存储着ebp的值
另外注意,它这里是弄了9次,但每一次是一个dword,double word,4个字节。一个cc是一个字节。所以你应该看到的是36个cc。所以恰好是对应的次数关系,并没有多或者少。
接着,我们刚刚所画的栈区图就可以表示成了这个样子:
这也就解释了为什么我们有的时候越界会打出来“烫烫烫烫”,因为0xCCCCCCC所对应的就是“烫”字
来看接下来的三行
int a = 10;
002617E5 mov dword ptr [ebp-8],0Ah
int b = 20;
002617EC mov dword ptr [ebp-14h],14h
int c = 0;
002617F3 mov dword ptr [ebp-20h],0
我们将我们的源代码也复制了上来。
还是一行一行来分析:
002617E5 mov dword ptr [ebp-8],0Ah
//意为将ebp - 8的位置dword(通俗来说就是赋值)成0Ah
(0Ah是一个十六进制数,就是0x00 00 00 0A,刚好是我们a的值10)
我们此时的栈区的图可以画成这样:
下面的两行就同理了:
002617EC mov dword ptr [ebp-14h],14h
//将ebp - 14h的位置赋值为14h
002617F3 mov dword ptr [ebp-20h],0
//将ebp-20h的位置赋值0
那么这个图再添加两个变量值
好的,接下来我们来看一看如何调用Add函数呢?
我们两行两行来看:
002617FA mov eax,dword ptr [ebp-14h]
002617FD push eax
//将 ebp -14h 位置的值赋值给到eax里
然后让eax压栈
注意到,ebp - 14h恰好就是我们要传的参数b的位置。
下面两行同理:
002617FE mov ecx,dword ptr [ebp-8]
00261801 push ecx
将 ebp -8 位置的值赋值给到ecx里
然后让ecx压栈
那么,现在的栈区图就可以理解成这样:
继续往下,然后我们需要按住F11
call 002613BB
表示调用函数,并记住call下面的一行指令的地址(也就是00261807)
F11按进去,
call 002613BB
我们便来到了指令为002613BB的这么一行汇编代码
jmp 002625D0
意为跳转到002625D0
继续按住F11
此时正式进入自定义函数中。
由刚刚的jump 002625D0,我们便跳转到这么一行汇编指令上去。
从这一行开始分析。
我们可以看到,从第一行到第十行与在main函数的如出一辙
同样,让ebp的值压栈;
然后将ebp的值给esp;
让esp减去0CCh
压栈ebx、esi、edi;
将ebp-0Ch赋给edi,然后向下读出3个dword,赋值成eax的0CCCCCCh;
然后再将edi变成ebp的值。
接下来,又是同样的配方,
int z = 0;
002625F5 mov dword ptr [ebp-8],0
//让ebp-8位置的值变成0
z = x + y;
002625FC mov eax,dword ptr [ebp+8]
//将ebp+8的位置的值复制到eax中
002625FF add eax,dword ptr [ebp+0Ch]
//再将ebp+0Ch的位置的值和eax相加,存放到eax中
//ebp+8和ebp+0Ch恰好一个是x,一个是y
00262602 mov dword ptr [ebp-8],eax
//将eax里的值赋值到ebp-8(就是z)中
继续来看:
00262605 mov eax,dword ptr [ebp-8]
将ebp-8的位置的值再赋给eax;
00262608 pop edi
00262609 pop esi
0026260A pop ebx
pop三次,弹出三次,就是说弹出edi、esi、ebx的值,
就是这样:
然后
00262618 mov esp,ebp
把ebp给esp
就变成了这样
0026261A pop ebp
//弹出ebp(注意,弹出时会将ebp的值还给原先存储的值)
就变成了这样。
然后
0026261B ret
返回原先记住的call指令下面的地址
继续执行
回来后继续:
00261807 add esp,8
//将esp加8 具体作用尚不清楚
0026180A mov dword ptr [ebp-20h],eax
//将eax的值给ebp-20h。
//就是将刚刚算的z的值给ebp-20h的位置,也就是c。
然后就是返回0.
下面还是熟悉的配方,
pop三次;
将ebp的值给esp;
弹出esp;
返回,结束本函数。
我们执行下去,会发现
其还会调用到别的地方。
这是因为main函数其实也是被其他函数调用的。
这个我们可以不用管了。
剩下的是编译器自己的事情了,我们简单地理解至此就可以了。
好啦,到此为止,我们函数栈帧有关的知识就结束了。
欢迎各位看官关注我@jxwd,订阅专栏,就能持续看到我的文章啦😀😀
0基础C语言自学教程——第八节 函数指针数组的各种关系_jxwd的博客-CSDN博客
0基础C语言自学教程——第七节 初始指针_jxwd的博客-CSDN博客
0基础C语言保姆教程——第六节 操作符、表达式和语句_jxwd的博客-CSDN博客
0基础C语言保姆教学——第五节 数组_jxwd的博客-CSDN博客
0基础C语言保姆教程——第4节 函数_jxwd的博客-CSDN博客
0基础C语言自学教程——第三节 分支与循环_jxwd的博客-CSDN博客
0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客_c语言全部框架
C语言自学保姆教程——第一节--编译准备与第一个C程序_jxwd的博客-CSDN博客