崩溃发生时的栈展开(Stack Unwinding)
在Rust代码编译过程中,前端编译器rustc会向编译后的MIR中间码,注入微量的“运行时上下文”程序,以零开发成本地完成对运行时程序的
- 变量借入规则检查(Borrow Check)
- 程序崩溃“收尾”
等“公共服务类”功能。文章标题中的Stack Unwinding就属于后一项工作范畴(它有时也被简写为Unwind)。即,在程序崩溃发生时,‘程序运行时’会
- 沿着‘函数调用栈’主动地依次释放全部变量的内存占用,
- 执行由元属性#[panic_handler]装饰的 (崩溃)善后处理函数 — 以释放外设资源(比如,打印机,数据库连接、监控摄像头等)
- 向操作系统发送应用程序崩溃信号 — 从系统层面收集应用程序崩溃日志等
但对功能单一的微型IoT设备来讲,软件自身的“栈展开”处理就有些“鸡肋”了。因为‘硬件简陋’与‘功能简单’到任何来自软件的‘自救善后’处理都显得多余,还不如程序包少占点儿硬盘与少费点儿内存来得实惠。于是,一旦程序崩溃,它就直接死给操作系统看。操作系统的轮询扫描服务会
- 发现既被标注又未活跃使用的内存段,并回收之。
- 强制断开外设连接
- 根据注册的脚本程序,自动重启应用程序
在Cargo Package工程的包管理器配置文件中,@Rustacean 可强制要求编译输出的二进制程序分发包不包含Stack Unwinding程序模块和不执行Unwind“收尾”处理,以收获更轻巧的可执行(或链接库)文件。
# 在 Cargo.toml 配置文件中
[profile.release]
panic = 'abort'
# 于是,由“cargo build --release”命令输出的二进制文件就禁用【栈展开】能力了
另外,若来不急精读冗长的Cargo Package工程配置文件,那么直接执行命令rustc --print cfg | grep panic也能立即获悉Cargo Package的栈展开模式。例如,
rustc --print cfg | grep panic
panic="unwind"
但故事可完全没有止步于此,当应用程序涉及FFI时,Stack Unwinding有”大坑“。假设主工程Cargo Package依赖某个C ABI的外部链接库,那么
- 不论对该链接库的链接方式是‘动态’的、还是‘静态’的
- 也不论该链接库是用什么编程语言制作的(比如,
Go,C,C++, 甚至Rust自身)
Cargo Package都必须与此C ABI链接库依赖采用一致的《栈展开》设置。这类跨ABI的《栈展开》也被记作Unwind ABI。
于是,若主工程Rust端对panic!采用了abort处理策略(见前文的Cargo.toml配置)和禁止栈展开,但链接库的C++端却对throw执行栈展开处理,那么当
- 被部署的二进制程序真发生程序崩溃,且
- 函数调用栈包含了来自
C ABI链接库的导出函数
时,真实的程序崩溃原因就会被“ABI不匹配”的次生崩溃所淹没和覆盖掉。然后,“找不到程序崩溃直接原因”的苦恼就会成就 @Rustacean 心态崩溃的瞬间。
综前所述,在给Cargo Package引入跨ABI的外部链接库依赖时,还请 @Rustacean 大哥们一定仔细阅读链接库的README文档,和同时对”来路不明“链接库多拒绝。
评论区
写评论虽然简短,但是技术细节清晰,而且从只言片语中,比如:直接死给操作系统看,还能看到咱们程序员有趣生动的灵魂!