05|代码封装(上):函数是如何被调用的?
该思维导图由 AI 生成,仅供参考
快速回顾 C 语言中函数的使用方式
- 深入了解
- 翻译
- 解释
- 总结
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-2825 - cc置顶在 C 语言中,函数有两种传递参数的方式,即通过“值”传递和通过“指针”传递。 --- 参数传递的方式,不都是传值吗
作者回复: 是这样的,这里可能确实会有些让人误解。本质上都是传值,只不过一个是指针的值,一个是源值拷贝后的值。这里其实想表达的是说“将源值拷贝后的新值”,以及“源值对应的指针”这两种不同方式。
2021-12-224 - 友置顶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-182 - =不使用leave,可以使用 mov rsp,rbp pop rbp 来恢复rsp和rbp的值。 对于enter,它和leave相反,用于自动创建栈帧,运用相当于 push rbp mov rbp rsp
作者回复: 回答正确!
2021-12-1611 - 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-183 - 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-1732 - =老师好,请问如果一个函数现有的栈帧大小不够用了,当继续向栈中push数据的时候,rsp中保存的值是动态变化的吗
作者回复: 是的,push 指令会自动修改 rsp 中的值。对于它的执行,你可以理解为两步:第一步是减小 rsp 中的值,“腾出”足够的栈内存;第二步是把值放入这块内存中。
2021-12-1522 - Samaritan.老师你好,请问下图A中的一个问题: 右边图的第40行,为什么还要在edi寄存器保存eax寄存器的值呢,直接使用第33行的eax的值不可以吗?
作者回复: 这里 rdi 保存的是 foo 函数第一个传入参数的值(也就是 1),这个是 SysV 调用约定中规定的。不过这里确实可以在第 33 行直接将 1 存放到 rdi 中,而不需要通过 rax 暂存,具体为什么这么做跟编译器的寄存器分配策略有关。
2022-03-131 - 谭渊__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