当前位置:首页 » 《随便一记》 » 正文

【函数栈帧的创建和销毁】从汇编角度解析_m0_61198445的博客

28 人参与  2022年04月09日 10:18  分类 : 《随便一记》  评论

点击全文阅读


先说结论:“函数形参是实参的一份临时拷贝,改变形参不会对实参进行改变”

本文编译器环境:VS2013

目录

开始介绍       

代码

汇编代码演示        

总结


开始介绍       

在开始之前,我们先要了解计算机中的寄存器:

我们常见的寄存器有:eapebpecpedp等,

而今天重点要关注的是esp(栈顶指针)ebp(栈底指针)这2个寄存器。

这2个寄存器中存放的是地址,这2个地址是用来维护函数栈帧的。每一个非静态区的函数调用,都要在栈区创建一个空间。

这里用反汇编的方式来对这段简单的代码来做例子:

代码

#include <stdio.h>

int add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main(void)
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = add(a, b);
	printf("%d", c);
	return 0;
}

在VS2013中,main函数是被其他函数调用的:

.....(代码)

 main函数被__tmainCRTStartup函数调用,main函数返回值返回到mainret里去;

__tmainCRTStartup函数又被mainCRTStartup函数调用

        接下来我们可以初步画出程序执行时函数调用内存中的关系:

汇编代码演示        

我们回到程序,回到编译器按下F10,代码右键鼠标点击转到反汇编

         点击转到反汇编后,把显示符号名勾选去掉,是为了接下来能清楚看到地址的变化

 接下来我们逐步看通过反汇编代码,内存中会发生什么变化:

第一步,从栈顶上压栈,压入ebp的地址,栈顶指针向上移动:

  • push 压栈:从栈顶放入一个元素
  • pop 出栈:从栈顶删除一个元素


         第二步,把esp的地址赋给ebp,即是把ebp移动到esp处;第三步,将esp加上0E4h(h在vs2013中指的是十六进制),即是把esp往低地址移动一段空间,这段空间即是开辟给main函数的空间;

第四步,往栈上压入ebx;第五步,往栈上压入esi;第六步,往栈上压入edi;


        第七步,反汇编代码中lea的意思为load effective address加载有效地址,即是要找到刚刚给main函数开辟空间的末尾;然后接下来的第一条语句mov是指把39h放在ecx里,再下一条mov语句是指把0CCCCCCCCh(十六进制)放进eax里;

最后包括最后一条语句,这三条语句实际产生的效果是:把从edi往下的39h(十六进制)个空间(四个字节)初始化改成0CCCCCCCCh


         接下来这里分别把a、b、c的值放入内存中;这里ebp减去的数值皆为十六进制,且mian函数中一个内存单元为4个字节,原始内存中的值在上一步被全部初始化为了cccccccc;在实际程序中,若在编写代码时对变量不初始化赋值,则变量内默认储存的就是在开辟函数空间时初始化的值。


         接下来操作的其实就是函数传参;第一条语句的意思是指把ebp-14地址上的内容传给eax,即是把20赋给ecx,接下来把eax push进函数里同时栈顶指针的位置向低地址移动;下面关于ecx的语句同理


 接下来就是call语句,call语句在跳转到add函数的同时,会把call语句下一条语句的地址push进main函数里:

接下来就是add函数开辟空间的过程:

大致与上面main函数的空间开辟大同小异,第一还是push ebp(main函数)的地址,然后将栈底指针ebp移动到栈顶指针esp的位置,然后栈顶指针esp向上移动33h个单位,然后分别push ebx、esi、edi;然后就是同样的操作,把开辟出来的空间初始化为cccccccch

 初始化:

        这里第一句mov语句即是把0的值放在ebp-8这个地址里;这里第二条mov语句的意思是把ebp+8这个地址里的内容放入eax中,接下来的add语句就是把原来eax里的值和新加进去的值加起来再次存到eax中(10+20=30);再下一条mov语句意思是把eax的值放到ebp-8这个地址里,即是把30放入z的地址中;接下来return z的mov语句指把ebp-8的值放入eax里,这里说明了函数的返回值在函数结束销毁时会先把返回值放在寄存器中,因为寄存器不会因为函数的销毁而销毁,最后主函数只需从eax中提取出返回值就可以了:

        这里三条pop语句将栈顶指针esp下移三个单位,之所以要pop是因为add函数结束了,要释放开辟add函数所创建的空间;接下来mov语句把ebp的地址赋给esp,实际作用也是释放add函数的空间;接着再把ebp pop,即是栈底指针ebp回到一开始创建main函数的位置;接下来ret语句即是回到main函数去,即是回到刚刚call语句的下一条语句,这就是因为为什么刚刚call语句要把call语句下一条语句的地址push,这样才能编译器才能准确回到该回去的位置


接下来我们回到main函数里,call语句的下一条语句指把栈顶指针esp + 8,即是把栈顶指针向下移动两个单位,释放add函数的形参(ecxeax);然后把刚刚add函数的返回值放入ebp-20这个地址里,即是把30传回给c: 


总结

最后main函数的销毁也是如出一辙,博主则不赘述了。

从一系列的汇编代码可以看出,正如最开头经典的一句话:“函数形参是实参的一份临时拷贝,改变形参不会对实参进行改变”,相信看到这里的同学你对函数的创建和销毁有了更深一步的认识,原创不易,你的点赞是对我努力的最大肯定!


点击全文阅读


本文链接:http://zhangshiyu.com/post/37586.html

函数  语句  地址  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1