0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

函数调用时底层会发生什么

jf_78858299 来源:码农的荒岛求生 作者:码农的荒岛求生 2023-02-17 14:47 次阅读

有读者问题函数调用是如何实现的,今天就来聊聊这个比较简单的问题。

大家都应该打包过东西吧,搬家之类的,通常都是找几个箱子一股脑装进去,为了不让箱子占地方,你通常会把它们摞好,就像这样:

最先被打包好的箱子被摞在最下方,刚打包好的箱子总是放在最上方,这就形成了一种first in last out的结构,也就是我们所说的栈,stack,上面的这些箱子就形成了栈。

如果你懂得用箱子打包东西,你就能明白函数调用是怎么一回事。

原来,在程序运行时每个被调用的函数都有自己的一个箱子,假设这段代码是这样写的:

void D() {}
void C() {
D();
}
void B() {
C();
}
void A() {
B();
}

函数A调用函数B、B调用C、C调用D,那么当函数D在运行时内存中就会有四个箱子,每个函数一个:

图片

每个函数占据的这个箱子——也就是这块内存,就被称为栈帧,stack frame,只不过由于引力的作用,我们摞箱子时是从下往上增长,而出于内存布局的需要,函数调用时的栈是从高地址向低地址增长。

这些箱子中都装有什么呢?你在函数中定义的局部变量就装在这里,关于栈帧内容更详细的讲解你可以参考这里《函数调用是在内存中是什么样子》,这些不是本文的重点,这里更关心的是这些栈帧是怎样增长以及减少的。

仔细观察上面这张图,每个箱子最重要的信息有两个, 你至少需要知道箱子的底部以及箱子的顶部在哪里

图片

在计算机中,每个函数栈帧的“底部”和“顶部”的信息——也就是内存地址,分别存放在两个寄存器中:BasePointer(BP)寄存器以及StackPointer(SP)寄存器,即我们熟悉的rbp以及rsp,32位下为ebp以及esp,注意本文以x86_64为例。

图片

只要确定了rbp和rsp你就能得到一块栈区,在这块栈区上就可以进行函数调用:

图片

读到这里肯定有的同学可能会问,CPU中的寄存器不是有限的吗?从这里的讲解看每个栈帧都需要维护一个“栈顶”与“栈底”的信息,每个核心中的rbp以及rsp寄存器就一个,我们该怎样确保函数运行时相应栈帧使用的rbp以及rsp是正确的呢?

方法非常简单,调用函数时会创建新的栈帧,此时需要将原有rbp寄存器中的值保存在新的栈帧上,就像这样:

图片

上图就是函数调用时第一件要完成的事情,把rbp的值push到栈上,rsp下移,然后呢?然后也很简单,只需要把rsp指向的地址也赋值给rbp即可,这样就开启了一个新的栈帧:

图片

完成上述操作的有两条机器指令(gcc编译器):

push   %rbp
mov %rsp,%rbp

如果你去看编译器为每个函数生成的机器指令,那么开头几乎都是这两条指令,现在你应该明白这两条指令的作用了吧。

这两条指令就把上一个栈帧的rbp的保存到了新的栈帧,由于此时rsp已经指向了新的栈帧栈顶,由于此时栈为空,因此栈顶和栈底的地址是一样的,可以直接把rsp赋给rbp,这样一个全新的栈帧就创建出来了。

如果我们在被调函数内部创建一些局部变量:

void funcB() {
int a = 1;
int b = 2;
int c = 3;
...
}

那么此时栈会进一步扩大,并把局部变量存放在该函数的栈帧中:

图片

现在我们的栈可以随着函数调用而增长,可以看到,栈帧和你搬家时用的纸箱子还是不太一样的,函数栈帧不会一开始就大小固定好,而是随着指令的执行动态增加,也就是如果你往栈上push一些数据,栈帧就会相应的增大一点。

那么函数调用完成时该怎么办呢?这也非常简单,只需要一条机器指令:

leave

我们在上一篇栈区分配内存快还是堆区分配内存快中讲解了一部分,leave指令的作用是将栈基址赋值给rsp,这样栈指针指向上一个栈帧的栈顶,然后pop出rbp,这样rbp就指向上一个栈帧的栈底:

图片

看到了吧,执行完leave指令后rbp以及rsp就指向了上一个栈帧,这就相当于栈帧的弹出,这样stack 1占用的内存就无效了,没有任何用处了,显然这就是我们常说的内存回收,因此简单的一条leave指令即可把栈区中的内存回收掉。

图片

而在x86平台,leave指令后往往跟上一条ret指令:

leave
ret

我们已经了解了leave指令的作用,这条指令让rbp以及rsp指向上一个栈帧,然后呢?显然CPU应该从funcA调用函数funcB之后的一行代码处继续运行,那么这行代码的地址在哪里呢?显然就在funcA栈帧的栈顶:

图片

当CPU执行call指令时会把该函数的返回地址push到栈中,而ret指令的作用正是将栈顶弹出(pop)到rip寄存器,rip寄存器告诉CPU接下来该从哪里执行机器指令,这个返回地址是funcA调用funcB时push到栈上的,这样当从函数funcB()返回后我们就知道该从哪里继续执行机器指令了,这就是ret指令的作用,当然这里也是函数调用实现的基本原理。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 程序
    +关注

    关注

    117

    文章

    3785

    浏览量

    81004
  • 函数
    +关注

    关注

    3

    文章

    4327

    浏览量

    62571
  • 函数调用
    +关注

    关注

    0

    文章

    19

    浏览量

    2585
收藏 人收藏

    评论

    相关推荐

    C语言使用函数调用的知识点

    C语言使用函数调用,我们再熟悉不过了,但是函数调用在内存中究竟发生了什么真的清楚吗?只有搞清楚内存里的内幕,才算完全搞懂
    发表于 09-07 11:47 829次阅读

    C函数调用机制与栈帧原理详解

    当一个C函数调用时函数的参数如何传递、堆栈指针如何变化、栈帧是如何被建立以及如何被消除的,一直缺乏系统性的理解,因此决定花时间学习下函数调用时
    发表于 06-08 10:49 1290次阅读
    C<b class='flag-5'>函数</b><b class='flag-5'>调用</b>机制与栈帧原理详解

    如何查看及更改函数/函数块的调用环境

    是循环执行,当一个功能块被多个外部函数/函数调用时,我们应如何查看某一次调用时的内部变量呢?这涉及到函数块的
    的头像 发表于 11-17 09:08 938次阅读
    如何查看及更改<b class='flag-5'>函数</b>/<b class='flag-5'>函数</b>块的<b class='flag-5'>调用</b>环境

    如果使用FCALL调用函数而使用RET返回的话, 就会发生CSA泄露怎么解决?

    FCALL调用函数不会自动存储Upper Context, 需要使用FRET进行返回, 如果使用FCALL调用函数而使用RET返回的话, 就会发生
    发表于 01-26 07:57

    应用程序调用底层驱动

    本片主要讲述了嵌入式linux操作系统的上层应用程序是如何调用底层驱动程序的。
    发表于 03-14 15:00 0次下载

    03 底层函数

    03 底层函数
    发表于 10-11 09:29 7次下载
    03 <b class='flag-5'>底层</b>库<b class='flag-5'>函数</b>

    内联函数和外联函数有什么区别

    内联函数是指用inline关键字修饰的函数。在类内定义的函数被默认成内联函数。内联函数从源代码层看,有
    发表于 12-15 11:52 5865次阅读
    内联<b class='flag-5'>函数</b>和外联<b class='flag-5'>函数</b>有什么区别

    详解python普通函数创建与调用

    函数是一种仅在调用时运行的代码块。您可以将数据(称为参数)传递到函数中,然后由函数可以把数据作为结果返回。
    的头像 发表于 03-01 16:32 1856次阅读

    C语言使用函数调用在内存中究竟发生了什么?

    C语言使用函数调用,我们再熟悉不过了,但是函数调用在内存中究竟发生了什么真的清楚吗?只有搞清楚内存里的内幕,才算完全搞懂
    的头像 发表于 01-13 14:09 1164次阅读

    C语言函数调用的形式及过程

    C语言函数调用时的数据传递 在调用有参函数时,主调函数和被调函数之间有数据传递关系。
    的头像 发表于 03-10 14:28 1735次阅读

    什么是函数调用

    函数调用,就是使用我们已经定义好的函数,或者C语言自带的库函数
    的头像 发表于 04-04 17:21 5812次阅读

    SCL中调用函数的示例

    在此,可插入函数 (FC) 调用函数块 (FB) 调用函数块可作为单实例、多重实例或参数实例进行调用
    的头像 发表于 06-06 10:18 2182次阅读

    网络系统调用网络套接字入口函数

    调用的应用层接口函数,第二个参数是一个指针,指向具体被调用函数(如accept函数)所需要的参数。 这些在用户系统
    的头像 发表于 07-24 11:02 470次阅读

    ES32F36xx芯片发生HardFault异常时的函数调用关系及问题定位

    ES32F36xx芯片发生HardFault异常时的函数调用关系及问题定位
    的头像 发表于 11-06 17:13 762次阅读
    ES32F36xx芯片<b class='flag-5'>发生</b>HardFault异常时的<b class='flag-5'>函数</b><b class='flag-5'>调用</b>关系及问题定位

    python定义函数调用函数的顺序

    定义函数调用函数的顺序 函数被定义后,本身是不会自动执行的,只有在被调用后,函数才会被执行,得
    的头像 发表于 10-04 17:17 1380次阅读