编译器对setjmp/longjmp的特殊处理及自定义实现问询
关于setjmp/longjmp编译器处理逻辑、自定义实现可行性的相关说明
一、相关观点的公开佐证依据
- 首先是C语言标准的明文规则:从C89到最新的C23标准,都对
setjmp/longjmp的使用场景加了明确约束:调用setjmp的函数里,凡是没加volatile修饰、且在setjmp调用之后被修改的局部变量,在longjmp跳转返回后的值都是未定义的。这条规则的形成完全符合提到的历史逻辑:K&R C时代的编译器基本不做激进优化,不会把局部变量长期缓存在寄存器、不会乱序执行指令、也不会随意消除看似无用的栈读写,setjmp就是个普通的库函数,直接保存栈上的寄存器上下文就行,不需要编译器特殊照顾。后来优化编译器普及,常规优化策略碰到非局部跳转就会出问题——比如变量被缓存在寄存器里,longjmp恢复的是setjmp调用时的寄存器快照,之后修改的寄存器值直接丢了;再比如编译器把跨setjmp点的指令重排,导致跳转后逻辑顺序错乱。标准委员会最后没有强制要求编译器做全量的上下文回溯修复——毕竟setjmp用得非常少,全量修复要给所有带setjmp调用的函数加大量额外的栈保存逻辑,性能代价太高——索性直接把使用约束写进标准,让开发者用volatile主动告诉编译器哪些变量不能缓存,这套规则就一直沿用到现在。 - 其次是主流编译器的实现逻辑:GCC、Clang、MSVC的优化管线里,都是把
setjmp硬编码成特殊的控制流节点的。碰到setjmp调用,编译器会自动停掉一批可能破坏非局部跳转逻辑的优化:比如不会把setjmp之后修改的局部变量长期放在寄存器里、不会重排跨越setjmp点的内存操作、不会删掉setjmp返回路径上的必要代码。这套特殊处理逻辑从来没有对外做通用化设计,毕竟需求太少。
二、自定义类setjmp/longjmp的实现可行性
如果不给编译器加专门的适配,纯靠汇编写上下文保存/恢复来做自定义的非局部跳转,可靠性会比标准库的setjmp/longjmp差很多,易错性明显更高:
- 编译器根本不知道你写的函数会触发非局部跳转,所有常规优化都会照常跑,除了常见的局部变量值错乱问题,还可能碰到栈帧被优化打乱、尾调用导致跳转点栈帧失效、寄存器值和编译器编译时的预期不匹配等问题,轻则逻辑跑错,重则直接段错误。
- 就算你在自定义实现里额外保存了线程本地上下文、信号掩码等扩展内容,只要编译器不识别你的跳转函数,你就得遵守比标准
setjmp严苛得多的使用规则:比如所有跨跳转点访问的局部变量都必须加volatile、不能开O2及以上的优化、要手动禁用跳转相关函数的尾调用优化等等,实际用起来踩坑概率远高于标准库实现。
真要做到和标准setjmp同等级的可靠性,唯一的办法是修改编译器源码,把你的自定义函数加到内部的特殊函数列表里,让优化管线对它套用和setjmp一样的特殊处理逻辑,这个成本对普通开发者来说基本不可接受。
三、自定义setjmp等价函数的编译器标记方法
目前没有任何稳定、跨编译器的官方方法,可以标记一个自定义函数和setjmp有等价的非局部跳转行为:
- GCC有个未文档化的
returns_twice函数属性,能告诉编译器这个函数可能会返回两次,但是这个属性只处理最基础的控制流识别,不会套用setjmp对应的全套优化限制,实际用的时候还是会碰到很多未定义行为,而且这个属性从来不是对外承诺稳定的用户接口,不同GCC版本的行为差异非常大,生产环境根本没法用。 - Clang基本照搬了GCC的这套逻辑,没有开放专门的稳定接口给用户标记自定义非局部跳转函数。
- MSVC的处理更封闭,所有和非局部跳转相关的特殊优化逻辑,都是硬编码绑定到标准库
setjmp/longjmp的函数签名上的,完全没有给用户留自定义的入口。
补充说明:现在常见的用户态协程、纤程库的上下文切换逻辑,本质也是非局部跳转,但这类实现要么是编译器/标准库内置的(比如C++20协程、Windows纤程API),已经被编译器加入了特殊处理列表;要么就是靠极其严格的编码规范规避优化问题——比如要求跨上下文切换点不能持有需要访问的局部变量、强制在切换点附近禁用特定优化,从来没有达到过标准
setjmp的易用程度。
内容的提问来源于stack exchange,提问作者Petr Skocik
相关产品推荐
相关产品推荐

