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

12|标准库:非本地跳转与可变参数是怎样实现的?

清理资源
获取参数值
初始化参数列表
恢复环境并跳转
保存当前环境到 jmp_buf
编写宏模拟异常抛出和捕获
自定义格式化输出
printf 函数
va_list 结构管理参数
参数存放在寄存器和栈中
va_end
va_arg
va_start
通过 stdarg.h 实现
接收不定数量的参数
链接到 C 程序
汇编代码示例
协程
异常处理
恢复执行流至 setjmp 之后
保存 Callee-saved 寄存器
longjmp
setjmp
通过 setjmp.h 实现
与本地跳转对比
使用非本地跳转实现 try...catch
需要遵循 ABI 规范
非本地跳转和可变参数函数增强 C 语言灵活性
应用场景
实现原理
关键宏函数
概念
自定义实现
应用场景
实现原理
关键函数
概念
思考题
总结
可变参数函数
非本地跳转
C 语言高级特性

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

你好,我是于航。
我曾在第 05 讲 中介绍过,C 语言中的函数调用是在 callret 两个指令的共同协作下完成的。这个过程包括程序执行流的转移、栈帧的创建、函数代码的执行、资源的清理,一直到函数调用完毕并返回至调用点的下一条指令上。总的来看,函数在正常情况下的调用流程是稳定有序的。
但实际上,这种以函数为单位的“顺序”执行流并不能完全满足 C 语言在使用时的所有应用场景。因此,C 标准从 C90 开始,便为我们提供了名为 “setjmp.h” 的标准库头文件。通过使用该头文件提供的两个接口 setjmp 与 longjmp,我们能够在函数调用过程中,实现对执行流的跨函数作用域转变。而对于上述这种函数执行流程上的变化,我们一般称它为“非本地跳转(Non-local Jump)”。
除此之外,在正常的 C 语法中,函数在被实际调用时,只能接收与其函数原型和函数定义中标注的,类型及个数相同的实参。而为了进一步增强 C 函数在使用上的灵活性,同样是在 C90 之后的标准中,C 语言还为我们提供了名为 “stdarg.h” 的头文件。配合使用在该头文件中定义的宏,我们便可以在 C 代码中定义“可变参数函数(Variadic Function)”。而可变参数函数与普通函数的最大区别就在于,它们在被调用时可以接收任意多个实参,而无需提前在函数原型或定义中声明这些参数的信息。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

C语言中的非本地跳转和可变参数函数是本文的重点内容。非本地跳转通过setjmp和longjmp函数实现,允许程序在执行过程中跳转到不同函数的特定位置,实现了跨函数的程序执行流程跳转。而可变参数函数通过stdarg.h头文件提供的宏定义,使得函数能够接收任意数量的参数,增强了函数的灵活性。文章详细介绍了非本地跳转的概念、setjmp和longjmp函数的使用方式,以及它们的运作原理。同时,通过示例代码和汇编代码的分析,展示了非本地跳转的实际执行方式和机器代码层面的执行顺序。此外,文章还介绍了自定义实现setjmp和longjmp函数的方法,并提供了相应的汇编代码和C代码示例。另外,文章还详细讲解了可变参数函数的基本使用方法,通过一个计算传入参数之和的示例函数展示了可变参数函数的定义和使用。整体而言,本文深入浅出地介绍了C语言中非本地跳转与可变参数函数的基本用法和实现原理,为读者提供了深入了解这两个重要特性的机会。文章还提出了一个思考题,引发读者思考如何使用非本地跳转来实现try...catch语句,并鼓励读者分享实践经验。

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

全部留言(12)

  • 最新
  • 精选
  • 白凤凰
    setjmp和longjmp的应用场景是什么呢?实际开发过程中什么时候可以用这两个函数,是为了解决什么问题呢?

    作者回复: 除了我们在文章中提到的可以用来实现 try...catch,协程以外,也可以用在信号处理程序中。比如参考这边的例子:https://www.gnu.org/software/libc/manual/html_node/Longjmp-in-Handler.html。除此之外,我好像没有再见到过其他用例。其他同学如果有另外的使用场景也可以补充哈。

    2022-01-10
    2
    3
  • liu_liu
    对 setjmp 和 longjmp 的理解: setjmp 将寄存器的值保存到全局变量中,同时保存 rsp 和返回地址的值,设置 eax = 0。 在上面的例子中,返回地址就是 cmp eax, 73 这句指令的地址。 longjmp 的作用: 1. 将保存的值从全局变量中全部恢复,其中最重要的是返回地址和 rsp。因为之后的 jmp 指令会跳转到返回地址去继续执行,也就是重新回到 cmp eax, 73。 2. 根据第二个参数更新 eax 的值。 那么当回到返回地址继续执行时,数据也都准备好,这样就达到了目的。 ------------------------- 另外有个问题,对于 rsp 的保存,不知理解的对不对? lea rdx, [rsp+0x8] mov QWORD PTR [rdi+0x30], rdx 在 call _setjmp 时,会将返回地址压栈,此时 rsp -= 8。所以需要加上 8 得到之前的 rsp,也就是 rdx = rsp + 8。

    作者回复: 没错的哈。

    2022-01-07
    2
  • 红军
    请问一下setjump和longjump为什么要保存&恢复ebx呢?其他的ecx、edx等寄存器为啥可以不用保存&恢复呢

    作者回复: 因为 rbx 属于 callee-saved 寄存器,需要由被调用方保存状态,并在结束前重置。

    2022-09-16归属地:上海
  • TableBear
    很好奇老师截图里面的工具是什么开发工具?有小伙伴知道吗?

    作者回复: 是这个:https://godbolt.org/

    2022-08-05归属地:广东
  • shk1230
    那么if (setimp(jb) < 'J' ),不就是恒等式?setjmp(jb)返回0时

    作者回复: setjmp 初次调用会返回 0,而后续 longjmp 在每次返回到 setjmp 时都会携带更新后的值。

    2022-02-26
    2
  • ZR2021
    对了,还有个感觉比较怪异的点,就是setjmp 函数的参数是直接传递的值,而非指针,然后setjmp 函数里还真修改了传入的值到jb 变量中! 按照我之前的理解,值传递的时候调用函数先将变量传给rdi 寄存器,然后被调函数里将rdi 里的内容拷贝至本函数的传参变量所在的栈,再使用这个变量的值。 但是setjmp函数貌似没有新开辟传参变量的栈,而是直接找到调用函数传参的地址,然后对其进行修改,这样,调用函数里的变量就得到了修改,不知道我理解的对不对。还有,这种通过值传递的而不是指针,但最后将传入的变量内容修改的形式,哪些地方用到的比较多呢?

    作者回复: setjmp 调用时传递的 jb 是一个指针哈。jb 对应的类型 jmp_buf 是这样定义的:typedef long jmp_buf[8];

    2022-01-10
    2
  • ZR2021
    老师,可变参数里面va_start初始化ap的时候,那个count参数需不需要的?理论上也被存到RSA里面了吧,看起来好像有点多余,但是看到很多可变参数都会用到这个参数,很是奇怪

    作者回复: va_start 在调用时的第二个参数,需要传入函数参数列表中显式定义的最后一个带名字的参数,这个是必须要传的。

    2022-01-09
    2
  • Ping
    老师,yield函数是不是用这里的setjmp和longjmp实现的?

    作者回复: setjmp 和 longjmp 是在 C 语言中实现协程这种语言特性的一种方式,但其他语言比如 Python、JavaScript 中的 yield 关键字是怎样实现的,这个就要具体情况具体分析了。当然,在 C 中也可以实现 yield,比如参考这个 Post:https://stackoverflow.com/questions/17478264/implementing-yield-in-c

    2022-01-09
  • liu_liu
    https://gist.github.com/silan-liu/47cf4d65e5f49b84c7f499a7d4fd24f2 尝试写了一个版本,不知思路对不对?

    作者回复: 思路是对的,不过可以用宏封装一下,让使用方式更自然一些。可以参考这篇文章:http://groups.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

    2022-01-07
  • liu_liu
    cmp esi, 0x1 adc eax, esi 如果 longjmp 传入的第二个参数为 0,那么此时 esi = 0。 cmp 之后的结果,会让进位 CF = 1。 adc 表示进位加和,eax = eax + esi + CF = 1。 这样就达到了令 eax = 1。
    2022-01-07
    4
收起评论
显示
设置
留言
12
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部