eax、ebx、ecx、edx { esp、ebp }(这两个重点,存放的是地址,这两个地址是用来维护函数栈帧的。)
2.每一个函数调用,都要(在栈区上)创建一个空间
接下来,用下面简单程序来向大家解释函数的栈帧的创建与销毁
现在mian函数被调用,所以要在栈区创建一个空间
这时我们按下F10 进入调试,点击上方 调试 空能选择 窗口 - 调用堆栈
一直按F10 知道出现以下界面
这里表示main函数在vs2013中是被调用的
那么是谁调用了main函数呢,往上翻,你会发现
所以main函数是在__tmainCRTstartup函数中被调用,而__tmainCRTstartup函数是被mainCRTstartup调用的。
即 main --------> __tmainCRTstartup -------> mainCRTstartup
所以,有这么个东西的,在调用main函数之前,也要为它们准备空间
以上就是大家讲一下,栈空间存储模式。
接下来我们开始详细解释 函数的创建与销毁。
首先我们回到主界面,按下 F10 进入调试,直接反键,选择转到反汇编
这样我们就进入了C语言的汇编代码了
接下来 我们来理解这些代码的意思
首先设置成如下情况
方便我们一会通过地址来解释。
现在大家注意,我们马上要调用mian函数了
然后我进入 main函数
它的第一步 是push sbp / ebp(压栈操作),pop(出栈操作)
// 压栈:给栈顶放一个元素, 出栈从栈顶删除一个元素
意思就是说想栈压了一个数据
这是没有进行压栈之前
当压了一个数据ebp之后,它成了栈顶,所以esp栈顶指针,要指向它了
你会发现xsp的值变量,是因为压栈使用习惯是先高后低,也就意味地址减去4个字节。
这时我们通过调式 - 内存会发现,ebp的值确实压进了esp
接下来第二条语句 mov,mov在这里的意思就是把esp的值赋给ebp
即ebp不再指向下面位置,而是跟esp指向同一个位置
我再通过监视发现,刚才我想法是对的。
下面就是第三条语句
意思为esp减去一个十六进制数 0E4h
即减去 288
意味着esp的地址变小了,esp不再指向原来的位置,如下图:
接下来 就三条push语句,意思就是给顶上压进三个元素
同时 esp要指向栈顶edi
跟监视和内存也证明了这一观点,esp确实把这三个数压进了。
然后就是下一条语句
lea 等价于 load effective address(加载有效地址)
即 把 有效地址 [ebp+FFFFFF1Ch] 加载到 edi里面去。
我把显示符号位勾选上,这样方便你们理解
edi减去 0E4h 再根据上文
等于就是说
然后接下来三条语句
ecx。39h 意思就是:把 39h 放ecx里面去
eax.0CCCCCCCCh / eax = 0CCCCCCCCh
最后一句的意思是,把从edi这个位置开始,向下的 39h次的数据,全部都改成0CCCCCCCCh
(也就是吧main函数初始化)
dword 的意思double word ,4个字节,也就是说每次操作4个字节
到目前为止,我们的main函数栈空间就开辟好了。
接下来就轮到我们 a 和 b ,c了(再取消勾选符号名)
意思就 a 放在地址 ebp-8 的位置上, b放在地址 ebp-14h位置上,
c放在地址 ebp - 20h
( 注意c的位置不是最后一个栈空间。只是画的不够大。)
接下来就函数调用(传参)
第一句 mov 把 ebp-14h的值赋给eax,就是先把b的值传过去,eax=20
然后它要push eax
然后把ebp-14h的值赋给ecx,即 ecx =10,然在再压栈
(注意 现在 esp指向ecx栈顶)
以上两个动作就是在传参。
就下来就是重要 的 call 这条语句 call(调用函数的意思/另外记住call的地址)
到这里 想看得更仔细一点 按F11
你会发现call里面放的是下一条指令的地址,也就是压栈了地址
为什么要这么做呢?,因为call 去调用add函数,调完了,你要回来。你要回到call语句的下一条指令的位置,然后向下执行,而恰好把call语句下一条指令记住了,放在这。一会回来的时候,我们就能用上,回来的时候我们就会找到这个地址,往下继续执行
然后我们按下 F11,进入如下界面
这时候我们真正来到add函数内部,你会发现跟为main准备函数空间一样。这里我就不再赘述。不然不懂,可以参考前面的过程。
由此可见 z =30的
接下来就是返回参数
这句话的意思就是 把 ebp-8 (c=30),存入寄存器 eax中,应该局部变量用完是要销毁的,所以需要先存起来。 到后面要用的时候再拿出来
然后 三pop语句,(出栈操作)
接下来 mov语句,把 ebp的值赋给esp
即
这里相当于吧 a和b的空间也还给操作系统了。
这里吧 eax的值(z= 30)赋给 ebp - 20h 即赋给c,故 c = eax =30
最后程序的结果显而易见 打印 30
总结
本文从内存层面上主要讲解函数的以下几个问题
首先我们给这个函数分配号栈帧空间,栈帧空间初识化空间之后,然后给我局部变量分配一定的空间
因为随机值使我们放进去的
当我们要去调用函数的时候,我们已经push push 把这两参数 a,b,从右向左开始压栈,压进去。当我们真正进入函数时,add的函数栈帧通过指针的偏移量,找到我们的形参
形参是实参的一份临时拷贝,改变形参,不会改变实参。
我们在调用之前我就把call指令,下一步指令记住了,压进去了,把ebp调用这个函数的上一个函数的栈帧的ebp存进去了,当我们函数调用完了,返回的时候,弹出(pop)ebp,就能找原始上一个函数调用的ebp,然后指针往下走的时候就能找到esp的顶,然后回到栈帧空间,然后就是我们记住call下一步的命令的地址,当我们往回返的时候,就可以跳到call的下一步的指令的地址,让我们函数调用完了之后,可以返回,
返回的结果是通过寄存器返回得到的。