汇编语言,一种能无死角的语言(通过c语言函数的简单运行助你了解函数栈帧的创建和销毁)

首先,我们来看一个求和函数:

如图所示:在main函数中调用了求和函数ADD,ADD在得到和后,将其返回给c,printf函数再将其打印在显示屏上。那么,对于变量a,b,c是如何创建,赋值,如何传递参数给ADD函数,ADD函数又是如何进行求和运算,如何将和值返回给c的,接下来我将一一陈述。

要想知道底层代码如何运行,不可避免的我们需要了解汇编语言,以及函数栈帧的创建。所谓汇编语言:就是用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。将c语言的每一条代码细化为机器指令的操作码,从而使得我们可以更好地了解机器实现代码的过程。

那么知道什么是汇编语言之后,我们来逐步分解上面的求和函数。

首先是main函数的调用:

我么知道,main函数称为主函数,但是其实main函数也是被函数调用的,该函数就是__tmainCRTStartup函数,虽然该函数也是被调用的,但是在此我门不深究。那么__tmainCRTStartup在创建时,系统为其分配一块内存空间,如图所示:

 我们知道,内存地址空间是从高到低存放的,在分配内存空间时,机器会为其分配两个寄存器edp和esp分别用来存放改地址空间的首地址及末地址。

接下来进入main函数,如图所示: 

 

 首先,push指令对ebp寄存器进行亚栈操作,此时需要知道的是,指向栈顶的esp寄存器会跟着指令的进行更新栈顶的地址。所以此时esp寄存器的地址应向低地址移动到ebp压栈后的地址。

mov指令意为将esp寄存器的地址赋给ebp寄存器,则此时原本指向__tmainCRTStartup首地址的ebp寄存器将更新为此时esp的地址,即栈顶的地址。

第三条sub指令,意为将esp指向的地址想后移动0E4h个地址。此时,esp和ebp又分别指向了新的首地址和末地址,此时两个寄存器之间的空间块即为main函数的栈区,称为main函数的栈帧。如图所示

接下来,下面代码所显示,我么需要压入三个寄存器ebx,esi,edi。汇编语言中,push意为压栈,即在栈定依次将这三个寄存器压入栈中。

 之后,后面这四条代码lea指令为load effective address,即将ebp-24h这个地址下载到edi寄存器;mov同之前的一样,不再过多赘述;然后最后三条语句意为将edi指向的ebp-24h这个地址向下(即高地址)的9个块(这里的9虽然汇编语言写法可能不太一样,这个有个大概意思就可以了,可以将其理解为9个4字节的空间)的地址空间的内容全部改为cccccccc。

 则上面这七条代码运行完后产生的结果如图所示:此时ebp以及esp之间的内存空间即为main函数的栈帧。

上面所有的操作都是在做准备,并没有真正进行求和过程,只是为main寒暑以及调用main函数的

 函数分配内存空间,接下来进入main函数:

 mov指令分别将0Ah和14h这两个十六进制数值存入ebp-8以及ebp-14h这两个地址中,而如下图所示,我们假设一个红色的小框代表一个4字节的空间,则第三条指令,将0存入ebp-20h也和上面的一样了。(在本文中需要注意的是,所有的地址箭头指向的上沿即为该空间的地址)

 接下来随着变量定义以及初始化之后,下面ADD函数进行求和操作:

 该四行代码显示:将ebp-14h地址的内容存入eax寄存器,然后将eax压栈,再然后将ebp-8地址的内容存入ecx寄存器,ecx也压栈。那么根据上面将a和b的值存入的地址我们知道,这四步应该是在传参。

传递完参数之后,我们看到,执行了call指令,所谓call指令,即调用指令,此时我们按F11进入call指令内部,发现call指令进行了两次调用,第一次调用图中1 地址,然后再从1地址调用2 地址,进入ADD函数内部,进行操作,所以其实call指令是将2地址进行压栈操作,并且进入ADD函数内部进行操作。

 那么进入ADD函数内部之后,接下来我么看到的是不是很眼熟呢,是的,这里ADD函数创建栈帧的过程与main函数其实是大同小异的。 

 

 那么我们废话不多说,直接上结果图:这里我么需要注意的是:函数在传参的时候,是从右往左一次传递,所以先传递的是 b的值,然后传递的才是a的值。

 接下来当为ADD函数创建好堆栈之后,进入ADD函数内部执行。如图所示,mov指令将0存入ebp-8这个地址;然后将ebp+8这个地址内容存入eax寄存器,然后add指令将ebp+0Ch的值加入到eax里面,此时,eax的值变为和30 ,再然后mov指令将eax的值存入ebp-8里面。

那么这一步骤我么细看,会发现,其实ADD函数找到了临时存放a和b的地址,然后取值,将求得的和存入z中,所以我么知道,这里只是进行了传值调用。

 

 此时如下图所示,ebp-8这个地址存放的是z,所以z的值此时等于30。

那么我们知道,一旦函数调用结束,被调用的那块地址将被释放,所以此时z的值存放入eax寄存器中,而寄存器是不会随着调用结束释放的。所以此时和值保存在eax寄存器中。

 

好了,当我们求出和传递给z之后,我么需要做的就是将其返回给主函数的c,所以接下来: 

 

 pop指令弹出指令,即从栈顶弹出某个地址空间,即从栈顶弹出edi,esi,ebx三个寄存器,我们知道,其实指向栈顶的esp指针也跟着变化。然后接下来的三个步骤将ebp赋给esp,即esp指向ebp的地址,所以此时ADD函数的栈帧由于没用了,所以被销毁。

 

 然后,当我们执行下一条代码时,出现了如下图所示的场景:

 

 什么意思呢,就是函数调用结束,现在返回到main函数里面,此时call指令的作用就有了,根据原先压栈存放的指令,我们找到了call指令后面紧接着的add指令移动esp的位置。最后将eax存放的值存入ebp-20h的地址,此时c的值变为了30。

然后,就是将结果打印在显示屏上,在后面就是main函数的栈帧的销毁以及__tmainCRTStartup函数栈帧的销毁,跟前面ADD函数的销毁一样的,这里不过多赘述。

以上就是整个函数调用栈帧的创建和销毁过程。一张基本完整的图奉上。

 

如有问题,可以留言哦,阿婆主会一一回复的呦,如果文章哪里写的不对,还请各位大神指正呀!

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值