龙空技术网

CPU眼里的:{函数括号}

阿布编程 1824

前言:

目前姐妹们对“c语言括号怎么打”大约比较看重,你们都需要分析一些“c语言括号怎么打”的相关文章。那么小编在网上收集了一些关于“c语言括号怎么打””的相关文章,希望朋友们能喜欢,姐妹们一起来学习一下吧!

函数括号{}在语法上的存在感几乎为:0,但它却是:最艺术、重要的软件设计之一,让我们用CPU的视角,看看究竟

01

提出问题

在你写过无数个函数后,是否会考虑一个问题?当你在函数里面,一顿神操作后,为什么函数返回后,一切还能恢复如初?刚刚定义的临时变量,最后会去哪里呢?

答案就是你写了无数遍的:括号{}。可遗憾的是这种灵魂级的操作,却很少有人提及,毫无存在感。

让我们用 CPU 的视角,感受一下函数的灵魂,一起窥探{}背后的秘密。

02

代码分析

打开Compiler Explorer(编译器版本信息:x86 msvc v19.latest),写一个简单的函数;定义 2 个临时变量 a,b;最后,作一下函数调用。

如你所见,正反括号的戏份并不少,分别对应了 3 条汇编指令。函数之间,能随意调用,还能顺利收场,就完全仰仗它们了。

为了便于展示堆栈,我们稍微调整一下源代码和汇编指令的布局:

假设右边的内存块就是当前任务的“堆栈”,为了方便展示堆栈的堆叠结构,下面是高端地址,上面是低端地址。如果更习惯高端地址在上的观感,也可以旋转180度,倒着看“堆栈”插图;它们只有视角的差异,并无本质的区别。

初始“栈帧”,是 main 函数的“栈帧”,在 红、蓝 两条线之间。红色水位线,是CPU寄存器 esp 的值,用来标识:“栈顶”的内存地址,有些类似C语言的指针变量;蓝色基准线,是CPU寄存器 ebp 的值,用来标识:main 函数的“栈帧”基地址,也类似C语言的指针变量。

不用关心 main 函数的“栈帧“,一切从函数 func 开始。首先执行第 1 条指令:

push指令把 CPU寄存器ebp的值,压入“栈顶”;“栈顶”红色水位线(CPU寄存器esp),也随之升高;至此,main 函数的“栈帧”保护工作完成!

然后,通过mov指令,更新一下“栈帧”基准线,让它与“栈顶”水位线齐平:

随后,通过sub指令,把红色水位线提升 8 个字节,用来给 2 个临时变量,分配“堆栈”内存:

至此,函数func的“栈帧”设置工作,完成!

随后,就是对临时变量 a,b 赋值:

如你所见,a、b 相对于蓝色基准线的偏移,分别是:4 和 8;正好用完函数的“栈帧”,一点不多,一点不少。

好了, 函数执行完毕,是该恢复:main 函数的“栈帧”了。通过 mov 指令,把红色水位线(寄存器esp),先降低到蓝色基准线(寄存器ebp)的位置:

最后,执行pop 指令:

把事先压入“栈顶”的 ebp 值,返还给CPU寄存器 ebp;这样蓝色基准线,就恢复到了最开始的位置;同时,随着“栈顶”的下降,红色水位线也随之下降。

至此,红、蓝两条线都恢复到了最开始的位置,也就是:CPU寄存器esp、ebp的值,恢复如初。这样,main 函数的“栈帧”恢复工作,就完成了。

不准确的说:函数的“栈帧”,就是:红、蓝两条线之间的内存块。它用来存放:函数的临时变量、参数和返回地址。所谓的:保护“栈帧”、恢复“栈帧”,不过是在:保存和恢复:寄存器 esp 和 ebp 的值。

03

总结

操作系统会为每个任务(进程或线程),分配一段内存,当作任务“堆栈”;CPU 则提供两个寄存器 esp、ebp,用来标识当前函数对“堆栈”的使用情况。随着函数的逐层调用,函数的“栈帧”,会逐次堆叠,互不重合;随着函数的逐层返回,函数的“栈帧”,会被就地放弃,但不会清理内存。

2. 正括号,用来保护上层主调函数(main)的“栈帧”,并设置被调函数(func)的“栈帧”;反括号,用来放弃被调函数(func)的“栈帧”,同时,恢复主调函数(main)的“栈帧”;这样,被调函数执行完后,主调函数就能继续执行。

3. 寄存器ebp作为当前函数的“栈帧”基地址,配合一定的偏移,就可以读、写函数体的临时变量。如果一个变量是通过 ebp 寄存器间接访问的,那么它往往是临时变量,也叫“栈”变量。

4. 不同编译器,对“栈帧”的实现方法,略有不同;但思路一致,一通百通。

所以,你现在知道为什么很少提及函数的括号了吧?因为里面的水,很深!不得不感叹C/C++的精妙,通过一对括号,让如此复杂的函数,变得特别易于使用,它让我们如此习以为常,以至于都快忘记了:是“堆栈”承担了每一次的函数调用的内存开销,并在函数返回时释放、归还这些内存开销。

04

热点问题

Q1:同样是堆栈,为什么编译器的实现方式,跟我们常见的代码,如此不同?

A1:本质是相同的,都是先进后出的存储、释放数据。只是,C/C++代码常使用指针变量来标识栈顶,并进行压栈、弹出操作;

而编译器使用CPU寄存器esp、ebp并配合push、pop指令进行相应的压栈、弹出操作,但执行效率显然比普通编程语言更高。

为了便于理解,我们将这些CPU指令C语言化:

如你所见,左边的push rbp指令和pop rbp指令,用来对某段内存,作的“堆栈”操作;它们跟右边的函数push()、函数pop()并没有实质差异。它们的操作效果,是完全一致的。

C/C++语言,用函数括号{},为程序员屏蔽了相对复杂的“堆栈”操作,可以让程序员专注于函数主要功能的开发,提高工作效率;但也隐藏了函数调用、返回的秘密,造成了一个无形的知识空洞。

05

参考视频

CPU眼里的:{函数括号}

标签: #c语言括号怎么打 #c括号里面套括号 #c语言函数括号里面存放的代表什么意思