深入 C 语言和程序运行原理
于航
PayPal 技术专家
21121 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 49 讲
深入 C 语言和程序运行原理
15
15
1.0x
00:00/00:00
登录|注册

05|代码封装(上):函数是如何被调用的?

栈帧中的信息
栈帧的概念
其他约定
堆栈清理
寄存器使用
返回值传递
参数传递
函数的定义与使用方式
函数的概念
enter 指令的作用
如何进行栈清理并恢复寄存器 rbp 与 rsp 的值
保存函数调用信息的栈帧
C 函数的调用约定
C 函数的使用方式
思考题
介绍 C 语言中函数调用的处理细节
C 函数调用的处理细节

该思维导图由 AI 生成,仅供参考

你好,我是于航。
在前两讲中,我介绍了 C 语言中的运算符、表达式、语句是如何被编译器实现的。不知你是否还记得,在介绍运算符时,我没有展开讲解有关函数调用运算符的内容。接下来,我就用专门的两讲内容,来带你深入看看 C 语言中有关函数调用的那些事儿。
这一讲,我们首先来看 C 语言中,编译器实现函数调用时所遵循的一系列规则。这些规则实际影响着函数调用时,在如何传参、如何使用寄存器和栈内存等问题上的处理细节。
除此之外,由于 C 语言中的函数调用过程与栈内存密切相关,我还会介绍栈和栈帧的概念。栈是 C 程序在运行时用于存放临时数据的一块内存,而每一个栈帧都对应着栈内存中的一段数据,这些数据是在函数调用过程中所必须使用的。通过这一讲的学习,你能了解到编译器对 C 函数调用的处理细节。而在下一讲中,我们将以此为基础,来深入探讨尾递归调用优化等更多函数调用的相关内容。

快速回顾 C 语言中函数的使用方式

函数的概念相信你已经十分熟悉了,这里我们先来快速回顾一下。
在编程语言中,函数是一种用于封装可重用代码的语法结构。函数可以接收从外部调用环境传入的数据,并在函数体内以复合语句的形式,使用这些数据构建独立的功能逻辑单元。借助函数,我们可以将一个程序的实现过程拆分为多个子步骤,并以结构化的方式来构建程序。这种方式可以减少程序中的重复代码,并通过抽象和替换来提高代码的整体可读性,以及可追溯性。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

C语言函数调用的底层实现原理及SysV调用约定是本文的重点内容。文章详细介绍了函数调用时的参数传递方式、寄存器和栈内存的管理,以及栈帧的概念。此外,还涉及了返回值传递、寄存器使用、堆栈清理等方面的规定。在函数调用过程中,每个函数都会在栈内存中存放对应的栈帧结构,包含函数调用所需的关键信息。文章还提到了栈溢出问题以及尾递归优化技巧。总的来说,本文通过介绍C函数调用的底层实现原理和SysV调用约定,帮助读者了解编译器对C函数调用的处理细节,以及不同平台下的调用约定规则。这对于理解函数调用的底层实现原理以及提高代码的性能和可移植性具有重要意义。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《深入 C 语言和程序运行原理》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(11)

  • 最新
  • 精选
  • 龍蝦
    置顶
    老师,Stack Frame(main) 这个图是不是有无误呢? 1、2 与 8、7 之间是不是有 8 个字节的空洞? rsp - 16,空出了 16 个字节,rbp-4、rbp-8 用掉两个4字节,rbp-9 ~ rbp-16 这 8 个字节未用到。

    作者回复: 很好的发现!这里为了便于理解我没有画出实际在 x86-64 平台上的栈布局。但实际情况是 1、2 与 8、7 之间有 8 字节 padding,然后又由于 push 在这里是按照 8 字节存放数据,所以 8 和 7 每个数字实际“占用”了 8 字节。

    2022-02-28
    2
    5
  • cc
    置顶
    在 C 语言中,函数有两种传递参数的方式,即通过“值”传递和通过“指针”传递。 --- 参数传递的方式,不都是传值吗

    作者回复: 是这样的,这里可能确实会有些让人误解。本质上都是传值,只不过一个是指针的值,一个是源值拷贝后的值。这里其实想表达的是说“将源值拷贝后的新值”,以及“源值对应的指针”这两种不同方式。

    2021-12-22
    4
  • 置顶
    push 指令等价 先 sub 然后 mov rbp,(rsp) 括号就是 将rbp保存到rsp sub之后的地址去 这里我就感觉 每次push之后 把上一个函数的 rbp保存了起来 这里的rbp应该存的是当前函数帧首地址 应该就是保证回到了当前函数并且里面的数据也还保存在。 至于 mov rbp rsp 应该就是将当前帧首保存到了 rbp里面 然后之前的rbp已经push到了rsp中 感觉说的有点思路不清晰了 害还是没有理解的很好 哦对了 关于 leave 如果按照上面所述 我只需要将这个 mov rbp rsp 就破坏了当前函数的栈结构 然后 pop 就回到了上一个函数的栈结构的首地址 此时 rsp应该是指向的返回地址 call 指令我想应该是 push rsp ,返回地址 ret 是 jmp (rsp) 老师上面是我的一些小理解。我感觉肯定有地方有错误希望老师指导一下我🌝🌝

    作者回复: 大部分理解都是对的哈。不过关于 rbp 里面保存的内容,只要知道它是上一个栈帧需要使用的、用来计算栈高度的信息就好。而 rsp 指向的是栈内存的顶部,也不用把它跟栈帧混在一起理解哈。因为其实所谓的“帧首”很难去界定,理论上按照小端序理解,可以认为“当前函数返回地址+8”(64位)的位置是上一个栈帧的开头。但一般很少这么讲。

    2021-12-18
    2
  • =
    不使用leave,可以使用 mov rsp,rbp pop rbp 来恢复rsp和rbp的值。 对于enter,它和leave相反,用于自动创建栈帧,运用相当于 push rbp mov rbp rsp

    作者回复: 回答正确!

    2021-12-16
    11
  • zxk
    老师好,这里我尝试观察函数返回后 rsp 的变化,做了以下调整: 调整前: foo(x, y, 3, 4, 5, 6, 7, 8),其中 7、8 入栈,函数返回后 add rsp, 16 调整后: foo(x, y, 3, 4, 5, 6, 7, 8, 9),其中 7、8、9 入栈,函数返回后 add rsp, 24 但 int 不应该是 4 字节么,为什么这里是按照 8 递增的,希望老师解答下,版本为 gcc 11.2。

    作者回复: 好问题!这是由于在 x86-64 平台上,push 指令在通常情况下会以当前平台的寄存器宽度(这里为 8 字节)为单位来调整 rsp。

    2022-01-18
    3
  • ZR2021
    老师,请教文章里的几个问题: 1. 函数传参时参数的传递顺序是从右到左,也就是从8到7到6,最后到y和x,但这里是先y再x,然后再从右到左,这是为啥?还有传递x的时候为啥先放到eax ,最后再放到edi 里的,编译器做的什么优化吗? 2. 清理栈上的数据就是一个leave或者操作rsp指令,清理是指将所有数据置为0吗?还有,如果函数退出都会清理的话,那下一次再次进入,为啥局部变量建议要置0的,这个操作rsp 自动清0是跟操作系统有关吗?

    作者回复: 很好的问题,下面是回答哈: 针对第一个问题:需要注意的是按照 SysV 调用约定,整型实参会先使用 rdi、rsi 等寄存器按顺序存放。所以这里只要函数在被调用前,这些寄存器中有按顺序存放的值就可以,汇编层面实际先存储哪个这个取决于编译器的选择。而超过寄存器数量以外的参数需要按照从右到左的方向被放入栈中,所以会从最右侧的 8 开始 push。至于为何 x 的值会被先放到 eax,这里我没看到比较特殊的优化策略,应该是取决于编译器的具体实现。 针对第二个问题:清理实际上只会恢复栈的可见状态到函数被调用之前,其中最重要的就是 rsp 的值,但栈中存放的值实际上不会通过可见的指令进行诸如“清 0”等处理。比如下面这个例子: #include <stdio.h> void foo(void) { int x; printf("%d\n", x); x = 10; } int main(void) { foo(); foo(); return 0; } 至于你提到的“局部变量建议要置 0”可以举个例子说明一下不?

    2021-12-17
    3
    2
  • =
    老师好,请问如果一个函数现有的栈帧大小不够用了,当继续向栈中push数据的时候,rsp中保存的值是动态变化的吗

    作者回复: 是的,push 指令会自动修改 rsp 中的值。对于它的执行,你可以理解为两步:第一步是减小 rsp 中的值,“腾出”足够的栈内存;第二步是把值放入这块内存中。

    2021-12-15
    2
    2
  • Samaritan.
    老师你好,请问下图A中的一个问题: 右边图的第40行,为什么还要在edi寄存器保存eax寄存器的值呢,直接使用第33行的eax的值不可以吗?

    作者回复: 这里 rdi 保存的是 foo 函数第一个传入参数的值(也就是 1),这个是 SysV 调用约定中规定的。不过这里确实可以在第 33 行直接将 1 存放到 rdi 中,而不需要通过 rax 暂存,具体为什么这么做跟编译器的寄存器分配策略有关。

    2022-03-13
    1
  • 谭渊
    __fastcall和SysV啥关系

    作者回复: __fastcall 是微软提出的一种特殊的调用约定;而 SysV 本质上是一套 ABI,它内部包含有对 SysV 调用约定的具体描述。除此之外,还有很多其他有关 ABI 的细节内容。

    2022-05-04
  • 爱新觉罗晓松
    在不使用 leave 指令的情况下,应该使用mov rsp, rbp 和 pop rbp, 将rbp寄存器的内容复制到rsp寄存器中,以释放分配给该过程的所有堆栈空间。然后,从堆栈恢复rsp寄存器的旧值。 enter跟 push rbp 和 mov rbp, rsp等价,在调用函数时,创建堆栈帧。

    作者回复: 回答正确!

    2021-12-15
收起评论
显示
设置
留言
11
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部