Rust Panic的工作原理
当您致电panic!()
时,会发生什么?
最近,我花了很多时间研究与此相关的标准库的各个部分,结果证明答案很复杂!
我找不到文档来说明Rust中的恐慌状况,因此值得写下。
(无耻的文章:我对此主题感兴趣的原因是@Aaron1011
在Miri中实现了对堆栈展开的支持。
我想从远古时代起就在Miri中看到它,而我却没有时间亲自实现它,因此,很高兴看到有人如何简单地发送PR
来支持它。
经过大量的检查代码后,最近已将其注入。
仍然有一些粗糙的边缘 ,但基本定义明确。)
本文的目的是记录在Rust方面起作用的高级结构和相关接口。
实际的堆栈展开机制是一个完全不同的问题(我无权发言)。
注意:本文介绍了此commit引起的恐慌。
此处描述的许多接口都是libstd的不稳定内部组件,并且可能随时更改。
高层结构
在阅读libstd中的代码时,尝试了解恐慌的工作原理,您很容易迷失在迷宫中。
仅通过链接器可以连接多个间接级别,
有 #[panic_handler]
和一个“运行时紧急恐慌处理程序” (由通过-C panic
设置的紧急恐慌策略控制)和“恐慌陷阱” ,事实证明,在#[no_std]
上下文中的恐慌需要完全不同的代码路径...非常很多事情。
更糟糕的是, 描述恐慌陷阱的RFC将其称为“恐慌处理程序”,但此后此术语已重新定义。
我认为最好的起点是控制两个方向的接口:
libstd使用运行时紧急处理程序来控制将紧急信息打印到stderr之后发生的情况。
这是由恐慌策略决定的:我们中断( -C panic=abort
)还是开始展开堆栈( -C panic=unwind
)。
(处理运行时恐慌还提供了catch_unwind
的实现,但在此我们不再赘述。)
libcore使用panic处理程序来实现(a)通过代码生成插入的panic(例如由算术溢出或超出范围的数组/切片索引导致的panic)和(b) core::panic!
宏(这是libcore本身和#[no_std]
上下文#[no_std]
的panic!
宏)。
这两个接口都是通过extern
块实现的:分别是listed / libcore,只需导入它们委托的某些函数,然后在板条树中的其他地方实现此函数。
仅在绑定期间才允许导入; 从本地查看代码,无法说出相应接口的实际实现所在的位置。
一路上我迷路了几次也就不足为奇了。
将来,这两个接口都将非常有用。 当你搞砸了。 首先要检查的是您是否已将应急处理程序和运行时应急处理程序混淆了 。
(请记住,还有一些紧急拦截器 ,我们会去找他们的。)
它一直在我身上发生。
而且, core::panic!
和std::panic!
不一样 正如我们将看到的,它们使用完全不同的代码路径。
libcore和libstd各自实现了自己的引起恐慌的方式:
core::panic!
的libcore很小:它只是立即将紧急情况委托给处理程序 。
libstd std::panic!
(“正常” panic!
Rust中的panic!
)启动了功能全面的紧急情况引擎,该引擎提供了用户控制的紧急情况拦截 。
默认的挂钩将在stderr中显示紧急消息。
拦截功能完成后,libstd将其委托给运行时紧急处理程序。
libstd还提供了一个调用相同机制的 panic 处理程序 ,因此core::panic!
也到此为止。
现在让我们更详细地研究这些部分。
在程序执行期间处理恐慌
紧急运行时的接口 (由RFC表示)是__rust_start_panic(payload: usize) -> u32
,该__rust_start_panic(payload: usize) -> u32
由libstd导入,然后由链接程序解析。
这里的usize
参数实际上是*mut &mut dyn core::panic::BoxMeUp
这是*mut &mut dyn core::panic::BoxMeUp
“有用数据”的地方(检测到信息时可用)。
BoxMeUp
是一个不稳定的内部实现细节,但查看此类型,我们看到的是它实际上只包装dyn Any + Send
,这是catch_unwind
和thread::spawn
返回的有用的紧急数据的类型 。
BoxMeUp::box_me_up
返回Box<dyn Any + Send>
,但作为原始指针(因为在定义此类型的上下文中Box
不可用); BoxMeUp::get
只是借用内容。
libpanic_unwind
中提供了此接口的两种实现: -C panic=unwind
(在大多数平台上默认)的libpanic_abort
和libpanic_abort
-C panic=abort
libpanic_abort
。
std::panic!
在panic 运行时界面的顶部,libstd在std::panicking
内部模块中实现了默认的Rust panic机制。
rust_panic_with_hook
几乎所有内容都通过的关键函数是rust_panic_with_hook
:
fn rust_panic_with_hook( payload: &mut dyn BoxMeUp, message: Option<&fmt::Arguments<'_>>, file_line_col: &(&str, u32, u32), ) -> !
此函数接受紧急情况来源的位置,可选的未格式化消息(请参阅fmt::Arguments
文档)以及有用的数据。
它的主要任务是触发当前的恐慌拦截器。
紧急情况拦截器具有PanicInfo
参数,因此我们需要紧急情况源的位置,紧急情况消息的格式信息以及有用的数据。
这与参数rust_panic_with_hook
非常匹配!
file_line_col
和message
可以直接用于前两个元素; payload
通过BoxMeUp
接口变成&(dyn Any + Send)
。
有趣的是, 标准的恐慌拦截器完全忽略了message
。 您所看到的是将有效负载转换为&str
或String
(无论如何工作)。
假定,调用者应确保message
格式(如果存在)产生相同的结果。
(下面我们讨论的保证了这一点。)
最后,将rust_panic_with_hook
发送到当前的运行时紧急处理程序。
目前,仅payload
仍然相关-以及重要的一点: message
(有效期为'_
表示可能包含寿命很短的链接,但有用的紧急数据将在堆栈中传播,因此有效期应为'static
)。
'static
约束”已经很好地隐藏了,但是过了一会儿,我意识到Any
意味着'static
(记住dyn BoxMeUp
仅用于获取Box<dyn Any + Send>
)。
Libstd入口点
rust_panic_with_hook
是std::panicking
rust_panic_with_hook
的私有函数; 该模块在此中央功能的顶部提供了三个入口点,一个绕过了它:
支持(如我们将看到的)来自core::panic!
的恐慌的默认恐慌处理程序实现 core::panic!
和内置的恐慌(来自算术溢出或数组/切片索引)。
获取PanicInfo
作为输入,并将其转换为rust_panic_with_hook
参数。
奇怪的是,尽管PanicInfo
组件和rust_panic_with_hook
参数非常相似,而且似乎可以轻松转发,但事实并非如此 。
相反,libstd会完全忽略 PanicInfo中的payload
组件,并设置实际的payload
(传递给rust_panic_with_hook
),以便它包含 message
。
特别是,这意味着运行时紧急处理程序对于no_std
应用程序无关紧要。
它仅在使用libstd中的应急处理程序的实现时才起作用。
(通过-C panic
情况选择的紧急情况策略仍然很重要,因为它还会影响代码生成。
例如,如果使用-C panic=abort
代码可能会变得更简单,因为您不需要支持堆栈展开。
begin_panic_fmt
,支持 std::panic!
的版本 std::panic!
(即,当您将多个参数传递给宏时使用此函数)。
基本上,只是将格式字符串参数包装在PanicInfo
(带有虚拟有效负载 )并调用我们刚刚讨论的默认panic处理程序。
begin_panic
支持 std::panic!
有 std::panic!
。
有趣的是,它使用了与其他两个入口点完全不同的代码路径!
特别是,这是允许您传输任意有用数据的唯一入口点。
该有效负载仅 Box<dyn Any + Send>
以便可以将其传递到rust_panic_with_hook
。
尤其是,从PanicData
看message
字段的恐慌拦截器将无法在std::panic!("do panic")
中看到消息,但可以在std::panic!("panic with data: {}", data)
看到消息std::panic!("panic with data: {}", data)
因为后者begin_panic_fmt
通过begin_panic_fmt
。
看起来很棒。 (但也请注意, PanicData::message()
尚不稳定。)
update_count_then_panic
原来很奇怪:此入口点支持resume_unwind
,实际上并不会引起恐慌拦截。
相反,它会立即发送到紧急处理程序。
例如, begin_panic
允许调用方选择任意有用的数据。
与begin_panic
不同,调用函数负责包装和确定有效负载; update_count_then_panic
函数只是将其参数几乎逐字转发到运行时的紧急处理程序。
紧急处理
std::panic!
该机制确实很有用,但是它需要通过Box
将数据放置在堆上,但并不总是可用的。
为了给libcore提供一种引起恐慌的方法,引入了恐慌处理程序 。
正如我们所看到的,如果libstd可用,它将提供此接口core::panic!
在libstd视图中感到恐慌。
紧急处理程序的接口是fn panic(info: &core::panic::PanicInfo) -> !
函数fn panic(info: &core::panic::PanicInfo) -> !
libcore导入,而链接器稍后将解决此问题。
PanicInfo
类型与恐慌拦截器的类型相同:它包含恐慌源的位置,恐慌消息和有用的数据( dyn Any + Send
)。
紧急消息以fmt::Arguments
的形式表示,即,带有尚未格式化的参数的格式字符串。
core::panic!
除了应急处理器接口外,libcore还提供了一个最小的应急API 。
core::panic!
宏创建fmt::Arguments
,然后传递到紧急处理程序 。
此处不会进行格式化,因为这将需要在堆上分配内存。 这就是PanicInfo
包含一个带有其参数的“未PanicInfo
”格式字符串的原因。
奇怪的是, PanicInfo
的payload
字段传递给了panic处理程序,始终设置为哑数值 。
这就解释了为什么libstd应急处理程序会忽略有效载荷数据(而是从message
创建新的有效载荷数据),但是这使我想知道为什么此字段是应急处理程序API的一部分。
这样做的另一个结果是, core::panic!("message")
和std::panic!("message")
(不带任何格式的选项)实际上导致非常不同的恐慌:第一个变成fmt::Arguments
,通过panic处理程序接口传递,然后libstd通过格式化来创建有用的String
数据。
但是,后者直接将&str
用作有用的数据,并且message
字段保持为None
(如上所述)。
libcore中的panic API的某些元素是语言元素,因为编译器会在代码生成期间插入对这些函数的调用:
结论
我们经历了4个API级别,其中2个通过导入的函数调用重定向并由链接器解析。
真是一段旅程!
但是我们到了尽头。
希望您在此期间不要惊慌 。 ;)
我提到了一些令人惊奇的事情。
事实证明,所有这些都与以下事实有关:恐慌拦截器和恐慌处理器在其接口中共享PanicInfo
结构,该结构包含可选格式的message
和带有擦除类型的payload
:
- 紧急恐慌拦截器始终可以在
payload
找到已格式化的消息,因此该message
对于拦截器似乎毫无意义,实际上,即使payload
包含消息, message
也可能不存在(例如,对于std::panic!("message")
)。 - 紧急处理程序将永远不会实际接收
payload
,因此对于处理程序而言,该字段似乎毫无意义。
从panic处理程序的描述中读取RFC ,似乎该计划是针对core::panic!
也支持任意有用的数据,但到目前为止尚未实现。
尽管如此,即使有这个将来的扩展,我认为我们也有不变性,当message
为Some
, payload == &NoPayload
(因此有用的数据是冗余的)或payload
是格式化的消息(因此该消息是冗余的)。
我想知道是否存在两种情况都有用的情况?
对于当前设计,可能有充分的理由反对该建议。 最好将它们放在文档格式中。 :)
还有很多要说的,但是在这一点上,我邀请您关注上面包含的源代码的链接。
考虑到较高的层次结构,您应该能够遵循此代码。
如果人们认为该评论应该永远存在,我很乐意将这篇文章转变为某种文档的博客-尽管我不确定这是否是一个好地方。
如果您发现我写的内容有任何错误,请告诉我!