Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >论文导读 | 使用 Kani 验证 Rust 中的 trait 对象

论文导读 | 使用 Kani 验证 Rust 中的 trait 对象

作者头像
张汉东
发布于 2022-12-08 13:09:41
发布于 2022-12-08 13:09:41
1.2K00
代码可运行
举报
文章被收录于专栏:Rust 编程Rust 编程
运行总次数:0
代码可运行

“原文为2022年5月发布的论文《Verifying Dynamic Trait Objects in Rust》[1] 。 注意,本文不是对该论文的全文翻译,而是论文关键摘要总结,仅供学习使用。

该论文是康奈尔大学和亚马逊工程师合作编写的,本文主要介绍开源的 Kani Rust verifier[2] 验证工具如何使用 MIR 表示的语义trait信息进行验证。该团队在调研 500 个下载次数最多的 Rust 库中发现,有 37% 使用表示动态调用的 dyn 关键字,而动态调度隐式调用达到70%(rustc编译时至少有70%包含一个vtable)。Kani 是第一个用于 Rust 的符号建模检查工具,提供了用于动态 trait 对象的开源验证方法。

Kani 简介

虽然 Rust 语言类型系统可以检查大多数内存安全问题,但仍然有很多执行错误的方法。因此,亚马逊(AWS 推动)和康奈尔大学的合作构建了开源工具 Kani ,用于对 Rust 程序进行健全、位精确的符号分析,主要目标是能无缝集成到 Rust 大型现有项目中。目前 kani 已经被集成到了开源的 Firecraker 虚拟机监视器组件上,Firecraker 为 AWS 的 Lambda 和 Fargate 提供虚拟化

该团队在实现 Kani 的过程中,发现一个意想不到的挑战,就是对动态 trait 对象的方法表进行建模。默认情况下, trait 方法调用是通过泛型限定的方式静态分发,即单态化。但是,开发者也可以使用 dyn 关键字来获得动态表达能力,即使用 trait对象。Rust 的闭包和匿名函数也可以通过 trait 对象动态调度(因为它们都实现了 FnOnce/FnMut/Fn)。

因为 Rust 生态库中大量使用 trait 对象,所以 kani 的目标就包括了对 Rust 标准库和 crate 提供全面支持。Kani 是目前唯一一个针对 Rust MIR 并且可以推理动态 trait 对象和动态闭包符号的模型检查工具。Kani 作为 Rust 编译器后端而实现,该编译器后端使用成熟的工业强度模型检查工具 「C 有界模型检查器(CBMC)[3]」作为验证引擎。Kani 将 Rust MIR 翻译为 GOTO-C(CBMC 类 C 的中间语言)。可以使用 Cargo 对单个rust 文件或 crate 来调用 Kani 。Kani 可以检查用户添加的断言、算术溢出、越界内存访问和无效指针,对于 Unsafe Rust 尤其有用。但默认情况下, Kani 使用断言方式运行。

Rust trait 概述

trait 静态分发(单态化)

泛型限定

trait 通常通过泛型限定进行静态分发,比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
trait Countable { 
 fn count(&self) -> usize; 
}


impl Countable for Vec { 
 fn count(&self) -> usize { 
  self.len() 
 } 
} 

impl Countable for Bucket { 
 fn count(&self) -> usize { 
  self.item_count 
 } 
}

fn print_count(obj: T) { 
 print!("Count = {}", obj.count()); 
}

上面定义的泛型函数 print_count 在 MIR 层面就会被单态化为具体类型的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn print_count_vec_i8(obj: Vec) { 
 print!("Count = {}", obj.count::>()); 
} 

fn print_count_bucket(obj: Bucket) { 
 print!("Count = {}", obj.count::()); 
}
闭包单态化

此外,闭包也可以作为 trait 限定:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn price<T: Fn(f32)->f32>(cost: f32, with_tax: T) 2 -> f32 { 
 with_tax(cost) 
}

fn main(){
    let tax_rate = 1.1; 
 price(5., |a| a * tax_rate); // Price is: 5.5 
 price(5., |a| a + 2.);
}

上面两个调用闭包的代码 Rust 也会对其单态化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fn see_price_closure@main:1(cost: f32) -> f32 { 
 closure@main:1([closure@main:1], cost)  // 这里要存储 cost
} 

fn see_price_closure@main:2(cost: f32) -> f32 ... // 这里不需要存储 cost
单态化成本

单态化可以提升性能,但是会增加编译文件大小和编译时间。

trait 动态分发(trait 对象)

为了权衡运行时效率和改进代码大小和编译时间,开发人员可以使用 trait 对象来进行动态分发(避免单态化)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
trait Countable { 
 fn count(&self) -> usize; 
}

// `&dyn Countable` trait对象,动态分发
fn print_count(obj: &dyn Countable) { 
 print!("Count = {}", obj.count()); 
}

当调用 print_count 时,编译器不会为每个具体类型创建一个新函数,而是使用一个 print_count 实例和可以表示所有实现 Countable 的对象实例。

trait 对象由一个胖指针表示,这个胖指针包含了一个指向对象本身(数据)的指针和一个指向其实现方法的虚表(Vtable)的指针。

因为 Vtable 需要跳转到动态计算的地址,所以它们可能会在安全攻击中被利用,因此它们的精确实现具有隐患。虽然 Rust 的非正式规范中没有指出 Vtable 的布局,但 MIR 提供了用于构建特定形式 Vtable 的实用函数。Kani 参考了 LLVM 后端中 Vtable 的特定布局。

“在 LLVM 后端中,Vtable 中包含着对象元数据(数据的大小和对齐方式),以及每个方法实现的函数指针。每个 vtable 中都包含一个指向具体类型的 drop(析构函数)方法实现的函数指针。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 上面 print_count 函数等价于
fn print_count(obj: &dyn Countable) {
 print!("Count = {}", 
  *(obj.vtable.count)(obj.vtable.data)
 ); 
}

// 当使用 as 强转具体类型为 trait 对象时
// 该 trait 对象的胖指针就会包含一个指向 Bucket 的指针和指向 Vtable 的指针
print_count(&Bucket::new(1) as &dyn Countable);

Kani 对 trait 对象验证的方式

因为 GOTO-C 没有对 trait 对象 的原生支持,所以 Kani 在实现的时候,只能遵循 LLVM 后端的 Vtable 实现来保持 trait 对象的语义。但 Kani 生成的 Vtable 对象是 GOTO-C 结构。

Kani 在实现 trait 对象验证的过程中遇到了下面的一些问题:

  • 不同trait 但可能存在同名的方法,会造成歧义。 Kani 使用 MIR Api 返回的 vtable_entries 来解决此问题,MIR 保留了大部分 Rust 类型的语义信息,这些丰富的类型信息提供了帮助。
  • Rust 目前不支持 trait upcasting (需要更改底层 vtable 实现,目前这个工作正在进行中),即将 trait 对象向上转换为它的 suptertrait 的 trait对象。但是对于 auto trait 来说可以进行强转,Kani 最初忽略了这一点。
  • Rust 的借用检查器和动态分发之间有一些微妙的联系。闭包解糖之后实际对应三种类型的方法签名(FnOnce(self)/FnMut(&mut self)/Fn(&self)),但是 Kani 当初只围绕 self 进行验证。

与其他语言无关的验证工具相比,Kani 的优势是可以利用 Rust 的语义提高验证的完整性和性能。

AWS EC2 应用案例: Firecracker

在对 Firecracker 进行验证过程中一个巨大挑战是代码中使用了很多 std::io::Error trait 对象(错误处理),这让 CBMC 符号执行引擎无法在四小时内完成任务。Kane 实现了一种「基于 trait 的函数指针限制」模式,将该过程加速了 15 倍。

相关测试代码见:icse22ae-kani[4]

其他类型工具比较

  • CRUST,一个类似于 Rust 的有界模型检查器,也使用 CBMC 工具作为验证后端。但是 CRUST 明确不支持 trait对象,并且不再积极开发。
  • Prusti,一个建立在 Viper 验证基础设施上的 Rust 编译器插件,同 Kani 一样,Prusti 也通过 MIR 类型信息改进验证结果。Prusti 的类型注释语言比 Kani 更具表现力,包括支持循环不变量,允许验证 Kani 目前无法验证的程序。但是 Prusti 对 Unsafe 的代码支持有限,并且不支持 trait 对象。
  • Crux-MIR,使用 Galois 的 Crucible 验证器,同样基于 MIR 。它可以通过 &dyn 指针引用验证动态分发的简单情况,但不支持 Box<dyn T>和动态闭包对象(如 &dyn Fn())。
  • MIRAI (facebook 开源)是一个 MIR 抽象解释器,不提供健全性验证。
  • 其他一些基于 LLVM IR 的验证工具,伴随着无法理解 Rust 类型级别语义的缺陷。
    • SMACK 工具链
    • RVT(来自 Google Research)

小结

Kani 是致力于提供在大型 Rust 项目中部署验证,本论文介绍了 Kani 如何支持 trait 对象的验证,并且展示了如何基于 MIR 中的类型信息将验证速度提升了 15 倍。

目前 Kani 正在积极开发中,Kani 的主要更改记录在 RFC Book[5]中。目前支持相当数量的 Rust 语言特性,但不是全部(比如还不支持并发)。请参阅Rust feature support[6]以获取支持特性的详细列表。Kani 每两周发布一次。作为每个版本的一部分,Kani 将与最近的 Rust Nightly 版本同步。

如需进一步了解 Kani 的应用,可以参考延伸阅读。

延伸阅读

  • https://whileydave.com/2021/10/26/test-driving-the-rust-model-checker-rmc/
  • https://model-checking.github.io/kani/

参考资料

[1]

《Verifying Dynamic Trait Objects in Rust》: https://www.cs.cornell.edu/~avh/dyn-trait-icse-seip-2022-preprint.pdf

[2]

Kani Rust verifier: https://github.com/model-checking/kani

[3]

C 有界模型检查器(CBMC): https://www.cprover.org/cbmc/

[4]

icse22ae-kani: https://github.com/avanhatt/icse22ae-kani

[5]

RFC Book: https://model-checking.github.io/kani/rfc

[6]

Rust feature support: https://model-checking.github.io/kani/rust-feature-support.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 觉学社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Rust 中 Trait 的使用及实现分析
在 Rust 设计目标中,零成本抽象是非常重要的一条,它让 Rust 具备高级语言表达能力的同时,又不会带来性能损耗。零成本的基石是泛型与 trait,它们可以在编译期把高级语法编译成与高效的底层代码,从而实现运行时的高效。这篇文章就来介绍 trait,包括使用方式与三个常见问题的分析,在问题探究的过程中来阐述其实现原理。
MikeLoveRust
2021/05/11
2K0
trait 对象的静态分发与动态分发
A trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait.
charmer
2024/05/15
2490
【Rust投稿】捋捋 Rust 中的 impl Trait 和 dyn Trait
本文来自 PrivateRookie 的知乎投稿:https://zhuanlan.zhihu.com/p/109990547
MikeLoveRust
2020/03/12
2.7K0
【Rust 基础篇】Rust Trait 对象:灵活抽象与动态分发
在 Rust 中,Trait 是一种用于实现共享行为和抽象的重要特性。Trait 对象是 Rust 中的另一个强大概念,允许我们在运行时处理不同类型的对象,实现灵活的抽象和动态分发。本篇博客将深入探讨 Rust 中的 Trait 对象,介绍其定义、使用方法以及与泛型的区别。我们将通过代码示例和详细解释带你一步步了解 Trait 对象的魅力。
繁依Fanyi
2023/10/12
9570
【翻译】200行代码讲透RUST FUTURES (4)
通过使用不与Future执行绑定的唤醒机制,运行时实现者可以提出有趣的新唤醒机制。例如,可以生成一个线程来执行一些工作,这些工作结束时通知Future,这完全独立于当前的运行时。
MikeLoveRust
2020/07/29
7360
Rust学习笔记Day13 怎么用trait实现子类型多态?
今天我们继续学习下子类型多态。一般来说子类型多态,都是出现在面向对象语言里的。说的是对象Child是对象Parent的子类,那么Child实例可以出现在任何期望Parent的实例的上下文中
用户1072003
2023/02/23
6760
Rust学习笔记Day13 怎么用trait实现子类型多态?
【Rust笔记】意译解构 Object Safety for trait
借助【虚表vtable】对被调用成员函数【运行时·内存寻址】的作法允许系统编程语言Rust模仿出OOP高级计算机语言才具备的【专用·多态Ad-hoc Polymorphism】特性。
MikeLoveRust
2023/09/26
2500
【Rust笔记】意译解构 Object Safety for trait
Rust特征对象(Trait Object)
前面学习的泛型,特征。它们都只能实现静态多态。它们和类型的绑定发生在编译期。如何让其实现C++中“父类指针指向子类对象”,从而实现运行时的多态。为了解决这个问题,Rust引入了——特征对象。
zy010101
2023/03/15
1.1K0
透过 Rust 探索系统的本原:泛型
在 Fundamentals of Generic Programming[1] 里,Alexander Stepanov(泛型概念的创立者)用一段优雅的文字描绘了计算机技术不断泛化(generalized)的历史:
tyrchen
2021/05/11
1.2K0
透过 Rust 探索系统的本原:泛型
【Rust 基础篇】Rust Sized Trait:理解Sized Trait与动态大小类型
Rust是一门以安全性和性能著称的系统级编程语言。在Rust中,类型大小的确定在编译期是非常重要的。然而,有些类型的大小在编译期是无法确定的,这就涉及到了Rust中的动态大小类型(DST)。为了保证在编译期可以确定类型的大小,Rust引入了Sized trait。本篇博客将深入探讨Rust中的Sized trait,包括Sized trait的定义、作用、使用方法,以及Sized trait与动态大小类型的关系,以便读者全面了解Rust中的类型大小问题,编写更安全、高效的代码。
繁依Fanyi
2023/10/12
5010
软件开发探索之道:让自己成为知识的所有者
这些让人摸不着头脑的问题,只要你耐心查找,在 stackoverflow 或者各种论坛上,一般能够找到答案。不过,别人给出来的答案很可能是模棱两可的,不好理解的,甚至是错误的。我们需要花时间甄别那些正确的、并且精准的答案,还需要花时间阅读这些答案。有时候,即便是你得到了答案甚至记住了答案,你可能还是没有完全理解别人给出的答案。当你需要把这样的答案讲给别人时,你会发现自己似乎无法讲得清楚。
tyrchen
2021/09/27
5730
【译】为 嵌入式 C 程序员编写的 Rust 指南
这是来自 Google OpenTitan 团队,给嵌入式 C 程序员专门打造的一份 Rust 指南。
张汉东
2021/10/13
5.3K0
impl Trait 的使用
Rust 通过 RFC conservative impl trait 增加了新的语法 impl Trait,它被用在函数返回值的位置上,表示返回的类型将实现这个 Trait。随后的 RFC expanding impl Trait 更进一步,允许 impl Trait 用在函数参数的位置,表示由调用者决定参数的具体类型,其实就等价于函数的泛型参数。
mazhen
2023/11/24
2460
「转自 InfoQ」Rust:一个不再有 C/C++ 的,实现安全实时软件的未来
Rust 作为新兴编程语言深受 Haskell 和 OCaml 等函数式编程语言的影响,使得它在语法上与 C++ 类似,但在语义上则完全不同。Rust 是静态类型语言,同时具有完整类型推断,而不是 C++ 的部分类型推断,它在速度上可与 C++ 媲美的同时,也保证了内存安全。
MikeLoveRust
2019/10/15
1.3K0
「转自 InfoQ」Rust:一个不再有 C/C++ 的,实现安全实时软件的未来
浅聊 Rust 【策略·设计模式】 Strategy / Policy design pattern
【Rust - Strategy / Policy策略·模式】与【OOP - Dependency Inversion依赖倒置·模式】和【Javascript - Callback Functon回调函数·模式】皆同属一类设计模式组合Inversion of Control + Dependency Injection(控制反转 + 依赖注入)。为了描述简洁,后文将该组合记作:IoC + DI。
MikeLoveRust
2022/11/28
1.5K0
浅聊 Rust 【策略·设计模式】 Strategy / Policy design pattern
Rust for Linux | 用 Rust 写 Linux 内核模块
Linux 内核模块在概念和原理层面与动态链接模块(DLL或so)类似。但对于 Linux 来说,内核模块可以在系统运行期间动态扩展系统功能,而无须重新启动系统,更无须重新编译新的系统内核镜像。所以,内核模块这个特性为内核开发者提供了极大的便利,因为对于号称世界上最大软件项目的Linux来说,重启或重新编译的时间耗费肯定是巨大的。
张汉东
2022/12/08
15.4K0
Rust for Linux | 用 Rust 写 Linux 内核模块
听GPT 讲Rust源代码--library/core/src(8)
在Rust源代码中,rust/library/core/src/future/ready.rs文件的作用是定义了一个名为Ready的Future类型。Ready是一个简单的Future实现,它立即返回一个给定的值。
fliter
2023/11/20
2050
听GPT 讲Rust源代码--library/core/src(8)
Rust for Linux 源码导读 | Ref 引用计数容器
2022 年,我们很可能会看到 Linux 内核中的实验性 Rust 编程语言支持成为主流。2021.12.6 早上发出了更新的补丁,介绍了在内核中处理 Rust 的初始支持和基础设施。
张汉东
2022/01/23
1.3K0
听GPT 讲Rust源代码--compiler(46)
文件rust/compiler/rustc_codegen_ssa/src/traits/declare.rs的作用是定义了一个Declare trait,用于声明函数、变量和全局变量等需要使用的实体。
fliter
2024/04/26
1330
听GPT 讲Rust源代码--compiler(46)
听GPT 讲Rust源代码--compiler(27)
在Rust编译器的源代码中,文件rust/compiler/rustc_mir_build/src/build/expr/as_place.rs的作用是用于处理表达式的转换为L-value的过程。L-value是指那些可接受赋值操作的表达式,如变量、数组元素或字段等。
fliter
2024/04/01
1280
听GPT 讲Rust源代码--compiler(27)
相关推荐
Rust 中 Trait 的使用及实现分析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验