函数栈帧的创建与销毁(转到汇编语言详细解释)

函数栈帧的创建与销毁

前期学习的时候,我们可能会有很多困惑:
1. 局部变量是怎么创建的?创建之后初始化又是怎么做的?
2.为什么局部变量的值是随机值?这个随机值是怎么得来的?
3.函数到底是怎么传参的?参数传递的顺序是怎样的?
4.函数的形参和实参到底是什么关系?
5.函数的调用整个过程到底是怎么样的?
6.函数调用完成之后又是怎么返回的?
只要知道函数栈帧的创建和销毁就懂了这些知识了。

接下来我用汇编代码的形式并结合足够简单清晰的实例来演示一下到底计算机内部是怎么在运转的

铺垫 

1.寄存器。寄存器有eax,ebx,ecx,edx,还有ebp,esp........函数栈帧要了解清楚,esp,ebp这两个寄存器必须了解清楚,这两个寄存器里面存放的是地址。里面存放的这两个地址是用来维护函数栈帧的。什么意思呢?笼统的来讲,那么这两个寄存器是怎么来维护函数栈帧的呢? 

2. 内存分为三个区域,栈区,堆区与静态区。每一个函数调用(不管是相同函数还是不同的函数),都需要在内存里面的栈区创建一个空间,比如说main()函数,栈区里面会开辟一块空间给main()函数(在内存里面,地址的话有高低之分,栈区也不用说),这一块空间就被称为为main()函数开辟的函数栈帧。bty,栈区使用习惯是:先使用高地址,在使用低地址

3. 对于内存当中的栈区来讲,这是一个线性数据结构,有栈顶与栈底,栈顶由寄存器esp维护,栈底由寄存器ebp维护。同时,栈区里面也有高地址和低地址内存栈区的使用习惯是先使用高地址,在使用低地址(由高向低蔓延)

4. (接第二个)那么这块空间到底该怎么维护呢?不能你说是你的就是你的吧。这块main()函数的函数栈帧是由两个寄存器来进行维护,一个是ebp,一个是esp。作为一个寄存器,其实也就是一块存储空间(与内存之间完全独立,可在代码中使用)就是来存储地址的 。

5. 这两个寄存器一上一下就来维护main()函数的函数栈帧。在程序当中,正在调用哪个函数,esp与ebp就正在维护哪个函数的函数栈帧。它们两个之间的空间就是为这次函数调用所开辟的空间,也就是说是这次函数的函数栈帧。

6. 这边得强调一下:栈区的使用习惯是先使用高地址,在使用低地址。也就是说,是从高地址到低地址这样子按样的顺序栈区空间在不断的被消耗。通常称,esp为栈顶指针,ebp为栈底指针(从这边也可以清晰的明白,寄存器用来存放地址,说白了,跟指针变量没啥两样,指针变量不也是用来存放地址的嘛,但是但是,差别还是蛮大的,这两个完全不是一个性质的东西,寄存器是电脑中独立于内存的一种存储器,其可在代码中使用

开始解析汇编之旅(为main()函数创建函数栈帧)

我以下面这个简单明晰的例子开看:

 

1. 当你进行调试的时候,从调用堆栈里面可能看到函数此时此刻的调用情况。比如说我们发现main()函数诶被调用了,但我们特别困惑的一点:main()函数这玩意儿又被谁调用了呢?事实上你会发现,main()函数也是被别人给调用的,在函数__tmainCRTStartup()函数里面调用了我们的main()函数。而这个函数__tmainCRTStartup()又是被mainCRTStartup()调用的。然后你会发现main()函数的return 0又返回到哪里去了,返回到mainret里面去了。反正就是说这个调用逻辑得明白。 

这个不同编译器下可能会不一样。但反正要知道的一点就是:main()函数也是被其他函数给调用的 

2. 在栈区上使用内存的时候,每一次函数调用都要在栈区为其分配内存空间。比如说我main()函数里面有一个add()函数,当程序运行到add()函数的时候,就会在栈区下面低地址处为add()函数开辟它的内存空间(函数栈帧)。同理,在main()上面也应该有那两个函数的函数栈帧 (main()函数也是被其他函数调用的)

接下来进行具体汇编代码一行一行读下去

1. 这时候就需要转到汇编代码(转到反汇编),这时候就会看到C语言对应的汇编代码了。然后把“显示符号名”这个去掉。(注意注意:ebp与esp虽然这两个东西叫寄存器,ebp,esp这两个东西里面就是存放的是地址) 

2. 在调用main()函数之前,就已经调用过了一个函数__tmainCRTStartup(),那现在我既然已经走到了main()里头来了。在这个之前,esp,ebp维护的是__tmainCRTStartup()的函数栈帧,这是此时此刻的情景。接下来就是进入main()函数了。(这边得再次强调一下:栈区使用习惯是先使用高地址,在使用低地址,由高向低蔓延)

原先是这样子:

 3. 接下来

 这个叫做压栈也就是往栈里面放了一个元素)这句代码的意思就是说把寄存器ebp里面的数据拿出来给压到这个栈顶上,注意:是把寄存器里面的数据压到栈上,不是寄存器ebp拿过来压在上面,寄存器与内存独立的,你怎么把寄存器拿过来?是把寄存器里面的数据压到栈顶上。这样完了之后,由于esp是维护栈顶的,这个esp指向的位置就也随之往低地址处奔走了

结果也不出意外

4 .接下来

mov   ebp,esp   mov是把右边的东西给左边,在这边是把esp寄存器里的值给ebp,注意这时候ebp寄存器里面的值已经被改掉了,ebp指向的位置也就发生了变化!!变成了esp!!

搜索ebp的值,果然

5. 接下来

接下来执行下一条语句:sub    esp,0E4h    sub是减的意思,也就是说把esp的值减去0E4h,于是乎,esp指向的位置也就向着低地址奔走了0E4h。 

esp值(吻合预期)

你就会发现在esp与ebp中间已经有了一块内存空间,这块空间就是为main()函数预开辟好的空间

6. 接下来

这个就是压栈压了三个值进去,(虽然ebx,esi,edi都是寄存器,但你不要认为是压了寄存器进去,寄存器与内存完全独立!!),至于这三个值干啥用,你不用去管(但是要注意,1. 每压完一次栈,esp都要向前更新一下。因为它总是去维护栈顶的,2. 此时随着栈区使用向低地址处蔓延,这个main()函数的函数栈帧是在不断扩大的) 

7. 接下来

接下来再执行一条语句: lea    edi , [ebp -  0E4h]     什么叫做lea,也就是load effective address ,就是说把后面的有效地址加载(放)到edi这个寄存器里面去,此时这个edi寄存器指向的是ebp-24h这个地址了 

edi的值

8. 接下来

接下来在执行两条语句:
mov    ecx,9
mov    eax,0CCCCCCCCh
这两个执行完后并不会出现效果,反正这两个语句执行的意义就在于把逗号后面的值给逗号前面的ecx,eax这两个寄存器,使得寄存器里面的值发生了变化。真正会起到作用的是下一条语句。

我搜一下ecx与eax,不出意外,吻合预期

 

 

9. 接下来

 接下来执行下一条语句:
rep  stos     dword  ptr  es:[edi]
首先要知道,word一个字也就是两个字节,然后dword也就是双字,就是四个字节。这句语句的意义在于从edi这个位置开始(此时,edi里面放的是什么你自己心里要清楚)一直向后(高地址方向)的ecx个双字(也就是四个字节)数据全部改成eax的内容即:0xCCCCCCCC。

内存中一目了然

 至此,为main()函数开辟栈帧已经全部ok了。当main()函数的栈帧开辟好了之后,接下来我要执行正式有效的代码了,刚才前面搞了半天,竟然没有执行一条有效的代码。接下来才开始真正执行C语音的代码。

开始解析汇编之旅(具体执行C语言代码,创建局部变量)

1. 

 

接下来执行c语言代码:int a = 10
这句c语言代码翻译到汇编代码就是
mov   dword  ptr  [ebp-8] , 0Ah
顺便说一下,在看汇编代码的时候,一定要把显示符号名去掉,这时候显示的就是地址。这条汇编指令的意义就在于:把0Ah这个数字放到ebp-8这个地址里面去。这时候相当于已经有四个字节的内存栈区空间已经被放进去a了。然后假设如果创建变量的时候没有给它赋值,那我里面放的就是CCCCCCCC,这里面放的这玩意儿不同的编译器五花八门,就是随机值了。因此一定要创建变量的时候初始化。 

 2. 

接下来执行c语言代码:    int  b = 20.
与它相对应的汇编指令就是mov    dword  ptr  [ebp-14h],14h
相当于就是说把14h放到ebp-14h这个地址里面去。放进去之后,这时候相当于在内存栈区上,又有四个字节的空间已经被放入数据14h然后相当于这四个字节的空间就相当于分配给了b

3. 

接下来执行c语言代码:    int c = 10
与它相对应的汇编指令就是mov   dword  ptr  [ebp-20h],0
相当于就是说把0放到ebp-20h这个地址里面去。放进去之后,这时候相当于在内存栈区上,又有四个字节的空间已经被放入数据0然后相当于这四个字节的空间就分配给了c

开始解析汇编之旅(调用函数前函数传参)

1. 接下来就是函数调用,函数调用需要传参,那到底是怎么传参的呢?

接下来执行汇编语句:
mov   eax,dword  ptr  [ebp-14h]。也就是说把ebp-14h这个地址及其后面总共双字(四个字节)的数据放到eax这个寄存器里面去。然后你去看一下ebp-14h,诶,现这不正就是b的值吗? 

搜了一下eax

 

 2. 

然后就是push   eax。这时候就是压栈,但我们事实上已经知道,eax这个寄存器此时里面放的就是b的数据,现在把b的数据给压栈压上去

 3. 

接下来执行汇编语句:
mov   ecx,dword  ptr  [ebp-8]。也就是说把ebp-8这个地址及其后面总共双字(四个字节)的数据放到ecx里面去。然后你去看一下ebp-8,诶,现这不正就是a的值吗?

4. 然后就是push   ecx。这时候就是压栈,但我们事实上已经知道,ecx这个寄存器此时里面放的就是a的数据,现在把a的数据给压栈压上去

那么刚刚这两个动作到底是在干什么?这两个动作是在给我传参吗?答案是确实就是在传参,但是你可能现在还感受不到。 

开始解析汇编之旅(进入调用函数内部)

1. 接下来的汇编指令是: call,F11按一下后


call就是说我要调用函数去。但是调用函数的时候一定要注意一个东西。事实上这个call的动作又把call指令的下一条指令的地址压上去了。那么为什么要记住这个地址呢?可以想象一下,一旦我执行call这个命令,相当于我马上去调用add这个函数去了,那么你这个函数调用完之后不是还要回来吗?那么回来的时候,你到底要回哪去?你要回到call指令的下一条指令,等你从add函数里面跳出来,你还是要执行call指令的下一条指令。而我这里恰好把call指令的下一条指令的地址记住,然后给它放在栈顶上面。然后等会儿从函数调用里面出来,我就会用得上这个地址。回来的时候就会找到这个地址,然后从这个地址往下执行。

2. 真正的走到函数里面去。然后你会发现前面那一堆汇编指令与main()函数的极为类似。其实就是在为add函数准备函数栈帧,当然,顺便提一下:现在main()函数的函数栈帧已经增长到了很多了,你自己应该有感觉的,看看上面压栈了多少东西进去。看看你的esp往前移了多少,反正总而言之就是说main()函数的函数栈帧已经增长到了很多了。  

3. 

 

这个时候把ebp此时此刻里面的值给压栈压进去了,这一步具有决定性意义,因为此时此刻ebp的值是指向main()函数的函数栈底,非常非常关键,后面会用到

4. 接下里的汇编指令是类同于main()函数的,懂得都懂

5. 

接下来汇编指令要执行我的计算任务了。首先是创建临时变量z,C语言代码为int z = 0,汇编指令为:mov   dword  ptr   [ebp-8],0    也就是说把0放到ebp-8这个地址里面去,这事实上相当于已经在add函数栈帧里面划分了四个字节的空间给z 

6. 

接下来执行c语言代码z=x+y,汇编指令为:mov    eax,dword  ptr  [ebp+8],这时候相当于它又回去找参数去了!!先把ebp+8这个地址及其后的四个字节的数据放到eax里面去。然后 add     eax,dword    ptr    [ebp+0Ch],把ebp+12这个地址及其后的四个字节的数据加到eax里面去。这时候eax变成了30了。然后:mov      dword   ptr  [ebp-8],eax,也就是说把eax寄存器里面已经加好的数据在放到ebp-8里面去,也就是放到z里面去。 

我对z进行取地址(吻合预期)

 注:

1.这时候你会发现形参根本就不是我add里面创建的,而是我回来去找原先压栈进去的那几个空间。

2. 那函数传参是怎么传的?也就是说事实上我函数还没有调用的时候,我参数已经压栈压进去了,压进去之后,等我真正进入函数内部,其实我又是找回了之前压进去的数据进行计算。那然后我们之前有讲过:如果是传值调用,形参是实参的一份临时拷贝,这句话完全正确。因为我压栈压进去的a,b是不是就是我实参的一份临时拷贝?很显然是的。那我到时候我对形参改来改去,压根儿与我实参半毛钱关系都没有。

开始解析汇编之旅(函数返回)

1. 

  

那么上面我只是让函数调用了,我还没有返回呢。那我该怎么返回呢?
C语言代码为return z
mov     eax,dword ptr [ebp-8],这时候需要注意(寄存器是不会程序退出然后销毁的),这汇编语言意思就在于把ebp-8的值(z)放到eax这个寄存器里面,相当于我用了一个全局一样的寄存器先把这个值保存起来这样就安全了,等我再回到主函数的时候,再把eax里面的值拿起来用就可以了。

对eax取地址(吻合预期)

 2. 

接下来pop     edi, pop    esi,  pop     ebx。就是说把栈顶的值全部依次弹出去放到寄存器edi, esi,  ebx里面去,这时候esp都会+4+4+4这样子。 然后mov    esp,ebp   这时候两者指向同一个地方了

我这个add函数已经调用完了的,我的结果都已经放在eax里面了,那么原先add的函数栈帧已经没有必要存在了,该回收了。只需要一个汇编指令就可以搞定,你看:mov       esp,ebp。把ebp赋给我的esp这意味着什么?意味着我的esp已经不指向原先那个位置了。

3.

接下来是pop     ebp,pop就是在栈顶弹出一个元素嘛,就是相当于说从栈顶弹出一个元素放到ebp寄存器里面去,这时候,绝妙的是这个pop出来的东西正是main函数栈底的地址。于是ebp又指向栈底去了

因为main函数的栈顶是很容易找到的,当我pop ebp也就是相当于从栈顶弹出一个元素然后放到ebp这个寄存器里面,然后由于弹出的那个元素里面放的是main函数的栈底地址,于是这个ebp又回去了,这个时候esp也不指向原先的地方了,朝着高地址的地方移动了四个字节,于是,这就又回到了main函数的函数栈帧。

这个时候main函数的函数栈帧就又开始由ebp与esp开始维护了。

4. pop出去的时候只是让我找到了main函数的ebp与esp。我回到main函数里面去的时候,我还是得从call指令的下一条指令地址开始执行,1. 而我恰好此时在栈顶上面放着call指令的下一条指令地址。2 .然后接下来还有一条ret语句-->就是从栈顶上弹出那个地址,然后就跑到那边去

 

总算渡劫从add()函数里面出来了,

5.

 

渡劫出来后,我的esp这时候就来到了形参的位置,但这个时候,形参已经没有任何用了。这时候有一条新的语句:add    esp,8  这个时候,esp又朝高地址处移动八个字节,恰好也跳过了那两个形参,这时候两个形参的内存空间也已经还给操作系统

我搜一下esp的地址(吻合预期)

6.

然后之前走出add函数的时候,把值委托给了eax,然后现在我要把这个结果给到c里面去,于是
mov      dword   ptr   [ebp-20h],eax 

 看一下ebp-20h的数据

吻合!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

絕知此事要躬行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值