崩溃发生时的栈展开(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
文档,和同时对”来路不明“链接库多拒绝。
评论区
写评论虽然简短,但是技术细节清晰,而且从只言片语中,比如:直接死给操作系统看,还能看到咱们程序员有趣生动的灵魂!