我们在现在,其实已经比较清楚函数是怎么样运行的了,包括怎样传参 、函数调用等等。但是呢,这样也只是理解到了会用的地步,
其底层的原理是怎样的,到底是如何调用的?我们本节内容将会来做详细探讨。
首先,我们需要知道,函数栈帧的创建和销毁是在栈区中完成的。每一次地函数调用都有栈帧的创建和销毁。
而系统在栈区内使用地址时是从高地址往低地址使用。就是说,先使用高地址,再使用低地址。
我们简单地画一个图
然后,我们需要了解这两个寄存器: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函数其实也是被其他函数调用的。
这个我们可以不用管了。
剩下的是编译器自己的事情了,我们简单地理解至此就可以了。
好啦,到此为止,我们函数栈帧有关的知识就结束了。