在学完了前面的函数之后不知你有没有疑问?今天我就浅谈一下我的疑问!
1.都知道局部变量在栈区但局部变量是如何创建的?
2.为什么局部变量不初始化是随机值?
3.函数是如何传参的?传参顺序是怎样的?
4.形参和实参的关系?
5.函数是如何调用的?
6.函数是如何返回的?
今天我们就从不一样的角度即函数的栈帧的创建与销毁的角度来剖析一下这些问题!在开始之前,先来介绍两个寄存器(register):esp 和 ebp他们两个是存地址(指针)的。
esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
也就是说esp和ebp这两个寄存器是专门用来维护函数的栈帧的!而每一个函数都要在栈区开辟内存空间,随着esp和ebp里面存储的指针的不同而维护不同的空间!
另外main函数也是函数,是函数就能是被调用但他是被谁给调用的呢?其实他是被__tmainCRTstartup这个函数调用的!而这个函数的也是被其他函数调用的!有兴趣的话可以自己了解一下,由于小编只有VS2019,没有VC6.0或VS2013 无法观察,如果您的电脑上有的话,您可以在调试起来后 看调用堆栈,这里就会看到!
OK,画个图简单看一下描述一下esp和ebp!
我们今天演示的环境是win11+x86+vs2019! 有可能编译环境不一样结果可能有差异!这取决于编译器!
栗:两数之和函数实现:
#include
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 ", c);
return 0;
}
我们直接进入正题:调试起来鼠标右键,点击反汇编:
过来是这样一个页面,下来我们就可以观察函数栈帧了!
ok,我们先来看一下前六条指令:
push 是压栈 push ebp 就是说把 ebp 压到栈顶
如何知道有没有压进去呢?
因为esp永远指向栈顶,而栈区的使用习惯是先试用高地址在使用低地址!也就是说如果压进去了,esp的值会减小!
果然减小了!!!所以证明压栈成功!
mov 是移动 mov esp 就是说把 esp的值赋给 ebp 也就是说esp 和ebp 目前指向同一位置
sub 是减 sub esp,0E4h 是说esp - 0E4h(16进制数)
我们发现此时esp和ebp 已经不指向__tmainCRTstartup 了其实他这会指向的空间正是为main 函数预开辟的空间!
然后有压栈了三个寄存器 ebx esi edi
lea (load effective address) 加载有效地址 lea edi,[ebp-24h] 这条指令意思是说将ebp - 24h的地址加载到edi里面 mov 是移动 mov ecx,9 这条指令的意思是给寄存器ecx里面放个9 mov eax,0CCCCCCCCh 给eax寄存器里面放的是0CCCCCCCCCh rep stos dword ptr es:[edi] 这条指令的意思是将edi 到ebp 之间的所有都初始化为c!!!
由于为main 函数开辟的空间很大所以esp到ebp之间的空间我画的时候省略了一点内存图!
这两条指令是说将0A3C003h放到ecx,call 是调用,意思是调用下一条地址!到此main函数栈帧已开辟完毕!下面就来执行后续操作!
mov dword ptr [ebp-8],0Ah 这条指令是说,将双字节(dword)指针ebp - 8 里面放 0Ah(10)
我们看一看内存是不是呢?果然是!
这就是创建a = 10; b = 20; c = 0;的
其栈区图如下:
然后
这四条指令是说:将ebp - 14h(b)这个指针指向的值放到eax然后压栈,同理将ebp - 8(a)的值放到ecx后,压栈:
call 是调用,这里不能再按F10了要按F11了才能进入Add函数:
我们进来发现,这里和刚刚main函数的过程极其相似:
先把ebp 压栈,然后esp放到ebp里面,然后esp - 0CCh,其实这里esp和ebp已经维护Add函数了然后压edi,esi,ebx三个寄存器上去:
注意此时main函数的栈帧可以理解为是增大了:
Add函数栈帧内存视角:
创建了 z = 0;
然后是进行 z = x + y;
啊!~我们发现居然没有给形参x,y开辟空间而是利用寄存器找回去提前压在main函数栈上a和b放在寄存器里的值!
把a(x),放到寄存器eax里,然后在找到b(y),加到eax上,
这就实现了相加!然后下一条指令,把eax(x+y的和)放到ebp - 8里面。
那么该如何返回呢?
返回时,原来是把计算的结果放到了寄存器eax里面。
继续看下面:
pop 是从栈顶上把数据弹出!!!所以下面三条指令的意思是把三个寄存器弹出:
当前栈帧图:
此时,esp和ebp指在同一位置,也就是说此时Add函数栈帧已经不在是esp和ebp维护了,即销毁了!!!
这几条指令是说,把ebp的值给esp,然后弹出ebp,然后返回到压到栈上的call的下一条地址:
这里很精妙的一点是:提前把地址压到栈上是为了后期返回的是找回来的!!!!
返回之后的后续指令:
先esp加8把上面的那块空间给释放了,然后把eax里面的值(a + b)给到ebp - 20h也就是c里面!
然后后续就是执行打印了,和上面Add函数的栈帧销毁过程一样会把main函数的栈帧也会销毁。说道这里也就能解释上面所有的疑问了!!
1.都知道局部变量在栈区但局部变量是如何创建的?
局部变量是在函数栈帧开辟好了以后创建在函数栈帧上的,这也解释了为什么局部变量只能在他那个定义的范围内有效了,因为出了范围函数栈帧销毁,空间还给操作系统了!就无法访问了!
2.为什么局部变量不初始化是随机值?
因为在函数开辟好栈帧后,会先初始化为CCCCCCCC如果你定义局部变量是没有初始化那他里面就是CCCCCCCC,是不可知的一个值!
3.函数是如何传参的?传参顺序是怎样的?
其实函数传参是在开辟另一个函数栈帧前就把她压在栈上了!而且是从右向左压的!
4.形参和实参的关系?
我们以前学习函数的时候说过:形参是实参的一份临时拷贝,改变形参不改变实参!当我们了解了函数的栈帧的创建与销毁后,发现这句话是真的总结的好啊!
5.函数是如何调用的?
通过call指令先把它的下一条地址压到栈顶,然后为新函数开辟相应的空间从而执行相应的操作!这里他这个提前把call下的一条地址压上去,为后来找到继续执行调用函数后的操作铺好了路,逻辑很严密!!!
6.函数是如何返回的?
在看这篇文章不知道你有没有想过这个问题?函数调用完了他不是把栈帧销毁了,在它里面的是局部变量啊,为什么还能再main函数中用呢?看到这里我相信你已经有了答案!没错,他是把返回的结果放到寄存器里面了,函数栈帧虽然销毁了,但寄存器不可能销毁!等到pop到主函数后会把寄存器的返回值放到相应的空间里面!
这就是本期内容,好兄弟下期再见!
如果小编有错的地方还劳烦大家不吝发私信或评论斧正!谢谢您!!