【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?(2)

简介: 1.函数的调用方式相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。本文以一个最简单的加法函数为例,深入讲解内存空间中的每一条指令。

将从edi开始的9个数量的地址全部改成0CCCCCCCCh。

多读几遍,你就读懂了,接下来验证一下:

ac130b307d934a8d9fc7eaafa4c41fd5.png

执行该汇编代码之后,情况是这样的:


47805dbb9bc942e081cc05dbc46dac3c.png

把刚才那块空间全部复制成cccccccc,现在可以解释上面的问题了:这块空间就是专门为main函数开辟的


ba3cd5d928704a4a842fa7347cd62032.png

接下来执行的汇编语句(黄色箭头)易于理解,把0AC003H这个值存入到ecx寄存器中。

真正厉害的是接下来红色箭头指向的这一条汇编语句,请注意,在执行call指令的同时,
call指令会自动把下一条汇编代码的地址进行压栈!
如下图所示:

4e38578e43a7462aaa8692b669ebc096.png

call指令在执行的同时就会做这件事情,把call指令的下一条指令的地址进行压栈

那么这件事情到底有什么作用呢?

这里先把问题放这里

接下来我们按F11,


06b59d6022574aac9ac9ab29ec13332f.png

似乎此时发现了新大陆!

我们可能看不懂那些代码是什么意思,没关系,这不重要。

重要的是刚刚说的一句话:call指令在执行的时候会把它的下一条汇编指令的地址先进行压栈!

回到call指令那个地方

bcfbc116d1a5464fb4b90af2408fb5dd.png

这里的调用指令,似乎可以理解成call指令调用内存区的已经建立好的函数。

再次点击F11,可以看到确确实实是在调用内存中已经建立好的函数。

74cfc9181cb04db8ae0a0c90e247715a.png

在众多汇编代码中,真正重要的是这一句代码,与刚才的call指令形成一致,在ret就是return的意思,执行完这一句汇编代码之后,一定会出现的事情是:返回到call的下一条指令处。

59f4069a0dfe47979dd2cd969be1b1b6.png

点击F11,,可以看到真的回到了call的下一条指令的位置。

这就是代码的严谨的地方,不仅能出去调用函数,还会记住原来的位置并且回来。

接下来就是把a的值存入内存中:


51d601d3e5714f0497c9891ec74124b9.png

看这两个地方,在执行了这条汇编代码之后,a被存入内存中了。

接下来就存b,然后接下来,就是在为Add函数的调用做准备工作了。

12d5b245550448cdbffdb72873a3ebf0.png

首先push ebp,对ebp寄存器进行压栈操作,为什么压栈前面已经讲过,压栈就是为了我们在开辟栈空间的时候,为了有效地记录栈空间的栈底地址而进行的操作。

与main函数的开辟如出一辙,接下来就是把esp的值给ebp,其实就相当于把ebp移动到esp的位置。

注意:在移动之前,进行的压栈操作,就是为了记录栈底空间的地址,以后调用函数结束后返回时可以找到该地址。

在main函数调用的时候也进行了压栈的操作。这些过程是相当严谨的。

接下来就是把esp的值-0CCh,就是为Add函数开辟了一块空间。

6fbf5b8cf18d44d3808cb873185c9d46.png

Add函数调用完成后,最重要的工作来了,如何销毁栈空间?

是这样销毁的:


aa43d090af9741debf7e6e2124a38aa0.png

pop有删除的意思,在这里是把edi弹出栈空间,然后再把edi的值赋给edi,总的来说就是弹出寄存器。

前面三个均是如此,但是最后一个弹出ebp,不知你是否还记得,我们在创建main函数和Add函数的时候,先是对ebp寄存器进行压栈的!

所以压栈的作用在这里就凸显出来了:

在弹出ebp寄存器的之后,会把ebp寄存器里面的值交给ebp。

也就是说:弹出ebp之后,ebp又记录了当时存在那个地方的值。


25455ba4121743c0b1308851afd5410f.png

所以ebp就回到了之前存的栈底位置的地址。

这样Add函数的销毁就完成了。

因为一块函数栈帧空间,是由两个寄存器共同维护的。现在寄存器esp回去了,那么这块栈帧空间就会归还给操作系统。

同理,对于main函数也是如此。

总结:
每一次函数的调用,都会在栈区开辟一块空间,这块空间是为调用函数准备的,而在开辟的过程中,存在着许许多多的细节,动作,来保证整个过程的严谨性。

在创建栈帧的同时也考虑到调用完函数之后销毁的过程,整个逻辑是很清晰的。

阅读汇编代码,了解汇编指令在函数调用时发挥的作用对我们的帮助是很大的,相当于我们在修炼内功。

相关文章
|
6月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
6月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
1月前
|
编译器 程序员 C语言
精简函数栈帧:优化创建和销毁过程的完全解析(建议收藏,提升内功)
精简函数栈帧:优化创建和销毁过程的完全解析(建议收藏,提升内功)
|
程序员 编译器 C语言
细谈函数栈帧的创建与销毁
我们在写C语言代码时,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数如何调用?函数的返回值如何返回的?函数参数是如何传递的?这些问题都与函数栈帧有关系。
179 0
|
6月前
|
存储 编译器
初识函数栈帧的创建与销毁(笔记)
初识函数栈帧的创建与销毁(笔记)
|
存储 编译器 程序员
C语言代码函数栈帧的创建与销毁(修炼内功)
目录 在前期的学习中我们可能有很多困惑 例如:局部变量是怎么创建的 为什么局部变量的值是随机值 函数是怎么样传参的 传参的顺序是什么 形参和实参的关系是什么 函数调用是怎么做的 函数掉调用结束后怎么返回的 这篇博客我们来修炼自己的内功,掌握好这篇博客的大部分知识就已经很不错了 我们用到VS2013这个编译器,目的是为了看到更详细的函数封装内容 提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器
|
编译器 C语言
【函数栈帧的创建和销毁】(超详细图解)(上)
【函数栈帧的创建和销毁】(超详细图解)
98 0
【函数栈帧的创建和销毁】(超详细图解)(上)
|
存储 编译器
从汇编代码探究函数栈帧的创建和销毁的底层原理(二)
从汇编代码探究函数栈帧的创建和销毁的底层原理
|
C语言 C++
从汇编代码探究函数栈帧的创建和销毁的底层原理(一)
从汇编代码探究函数栈帧的创建和销毁的底层原理
【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?(1)
1.函数的调用方式 相信你对调用函数一点都不陌生,但是在调用函数的过程中,却存在着很多你无法见到的东西,这是底层信息,想要理解透彻,就得深入底层去观察。 本文以一个最简单的加法函数为例,深入讲解内存空间中的每一条指令。