Rust步伐设计之 第 22 章 不安全代码(1)

打印 上一主题 下一主题

主题 1044|帖子 1044|积分 3132

第 22 章 不安全代码(1)

系统编程的秘密乐趣在于,在每一种安全语言和精心设计的抽象之下,都是非常不安全的机器语言和按位利用的汹涌暗流。你也可以用 Rust 写出这种代码。
迄今为止,通过范例查抄、生命周期查抄、限界查抄等方法,本书中先容的这门语言可以确保你的步伐完全自动地摆脱内存错误和数据竞争的困扰。但是这种自动化推理有其局限性,由于 Rust 中仍然有很多无法辨认为安全的高代价技能。
不安全 1 代码 能让你告诉 Rust:“我选择使用你无法保证安全的特性。”通过将块或函数标志为不安全的,你可以得到调用标准库中的 unsafe 函数、解引用不安全指针以及调用以其他语言(如 C 和 C++)编写的函数等本事。Rust 的其他安全查抄仍然实用:范例查抄、生命周期查抄和索引的边界查抄都会正常举行。不安全代码只会启用一小部分附加特性。
这种跨越 Rust 安全边界的本事使得 Rust 可以实现自身很多最根本的特性,就像 C 和 C++ 被用于实现本身的标准库一样。使用不安全代码, Vec 范例可以更加高效地管理其缓冲区, std::io 模块可以和利用系统对话, std::thread 模块和 std::sync 模块可以提供并发原语。
本章涵盖了使用不安全特性的所有要点。


  • Rust 的 unsafe 块在普通的、安全的 Rust 代码和使用了不安全特性的代码之间创建了边界。
  • 可以将函数标志为 unsafe,提醒调用者这里存在必须服从的额外契约,以避免未定义举动。
  • 裸指针及其方法允许不受限定地访问内存,进而构建 Rust 的范例系统原本会禁止的数据结构。Rust 的引用是安全但受限的,而任何 C 或 C++ 步伐员都知道,裸指针是一个强大而锋利的工具。
  • 理解未定义举动的定义将帮助你理解为什么它会产生比得到禁绝确的结果还要严峻的结果。
  • 不安全特型( unsafe trait)与 unsafe 函数类似,对每个实现而不是每个调用者都强加了必须服从的契约。
   完整版下载地址:我用夸克网盘分享了「1314-Rust步伐设计(第 2 版)(专栏更新完毕)」,点击链接即可生存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速,支持电视投屏。
链接:https://pan.quark.cn/s/ef1ce07cd37a
  失效访问:备份地址:https://cowcowit.com/course/317/317
  22.1 不安全因向来自哪里

在本书的开头,我们展示过一个由于没有服从 C 标准规定中的规则而以令人惊讶的方式瓦解的 C 步伐。在 Rust 中可以做到同样的事情:
  1. $ cat crash.rs
  2. fn main() {
  3.     let mut a: usize = 0;
  4.     let ptr = &mut a as *mut usize;
  5.     unsafe {
  6.         *ptr.offset(3) = 0x7ffff72f484c;
  7.     }
  8. }
  9. $ cargo build
  10.    Compiling unsafe-samples v0.1.0
  11.     Finished debug [unoptimized + debuginfo] target(s) in 0.44s
  12. $ ../../target/debug/crash
  13. crash: Error: .netrc file is readable by others.
  14. crash: Remove password or make file unreadable by others.
  15. Segmentation fault (core dumped)
  16. $
复制代码
这个步伐借用了对局部变量 a 的可变引用,将其转换为 *mut usize 范例的裸指针,然后使用 offset 方法在内存中又天生了 3 个字的指针。这恰好是存储 main 的返回地址的地方。这个步伐用一个常量覆盖了返回地址,这样从 main 返回的举动就会令人非常惊讶。导致这次瓦解的缘故原由是步伐错误地使用了不安全特性——在这个例子中就是解引用裸指针的本事。
不安全特性是强加了某种 契约 的特性:Rust 不能自动实行这些规则,但你必须服从这些规则以避免 未定义举动
这种契约超出了常规范例查抄和生命周期查抄的本事范围,针对该不安全特性强加了更多规则。通常,Rust 本身根本不相识契约,契约只是在该特性的文档中举行相识释。例如,裸指针范例有一个契约,它禁止解引用已超出其原始引用目的末尾的指针。上述例子中的表达式 *ptr.offset(3) = ... 破坏了这个契约。但是,正如前面的记载所示,Rust 毫无怨言地编译了这段步伐,由于它的安全查抄并未检测到这种违规举动。当使用了不安全特性时,作为步伐员,你有责任查抄本身的代码是否服从了它们的契约。
很多特性需要服从某些规则才气准确使用,但这些规则并不是这里所说的契约,除非违背它们的结果包括未定义举动。未定义举动是“Rust 刚强地认为你的代码永远不会出现的举动”。例如,Rust 认为你不会用其他内容覆盖函数调用的返回地址。可以或许通过 Rust 通常的安全查抄并服从其用到的不安全特性的契约的代码不可能做这样的事情。由于前面的步伐违背了裸指针契约,因此其举动是未定义的,它已经偏离了轨道。
假如代码表现出未定义举动,那你就已经违背了与 Rust 告竣的交易,以是 Rust 无法对其结果负责。从系统库深处发掘出不相关的错误消息并导致瓦解是一种可能的结果,将盘算机的控制权交给攻击者是另一种结果。在没有告诫的情况下,从 Rust 的一个版本换到下一个版本可能会产生差别的效果。然而,有时未定义举动并没有明显的结果。假如 main 函数永远不会返回(好比调用了 std::process::exit 来提前停止步伐),那么破坏的返回地址可能无关紧要。
只能在 unsafe 块或 unsafe 函数中使用不安全特性,我们将在接下来的内容中对两者举行解释。这可以避免在不知不觉中使用不安全特性:通过强制编写 unsafe 块或函数,Rust 会确保你已经知道在本身的代码中可能要服从的额外规则。
22.2 不安全块

unsafe 块看起来就像前面加了 unsafe 关键字的普通 Rust 块,差别之处在于可以在块中使用不安全特性:
  1. unsafe {
  2.     String::from_utf8_unchecked(ascii)
  3. }
复制代码
假如块前面没有 unsafe 关键字,那么 Rust 就会反对使用 from_utf8_unchecked,由于这是一个 unsafe 函数。有了它周围的 unsafe 块,就可以在任何地方使用此代码了。
与普通的 Rust 块一样, unsafe 块的值就是其最终表达式的值,假如没有则为 ()。前面展示的对 String::from_utf8_unchecked 的调用提供了该块的值。
unsafe 块解锁了 5 个额外的选项。


  • 可以调用 unsafe 函数。每个 unsafe 函数都必须根据本身的目的指定本身的契约。
  • 可以解引用裸指针。安全代码可以传递裸指针,比力它们,并从引用(以致整数)转换成它们,但只有不安全代码才气真正使用它们来访问内存。22.8 节将详细先容裸指针并解释如何安全地使用它们。
  • 可以访问 union 的各个字段,编译器无法确定这些字段是否包含其各自范例的有效位模式。
  • 可以访问可变的 static 变量。如 19.3.11 节所述,Rust 无法确定线程何时使用可变 static 变量,因此它们的契约要求你确保所有访问都能准确同步。
  • 可以访问通过 Rust 的外部函数接口声明的函数和变量。即使声明为不可变的,这些函数和变量也仍然会被看作 unsafe 的,由于它们对于用其他可能不平从 Rust 安全规则的语言编写的代码仍然是可见的。
将不安全特性限定在 unsafe 块中并不能真正阻止你做任何想做的事。你完全可以只将一个 unsafe 块粘贴到代码中,然后继承我行我素。该规则的紧张目的在于将人们的视线吸引到 Rust 无法保证其安全性的代码上。


  • 你不会偶然中使用不安全特性,然后发现要对连本身都不知道在哪里的契约负责。
  • unsafe 块会引起评审者的更多关注。有些项目以致会通过自动化办法来确保这一点,它们会标志出影响 unsafe 块的代码更改以引起特别关注。
  • 当你考虑编写 unsafe 块时,可以花点儿时间问问本身是否真的需要这样的步伐。假如是为了性能,那是否有丈量结果表明这确实是一个瓶颈呢?大概在安全的 Rust 中有更好的办法来完成同样的事情。
22.3 示例:高效的 ASCII 字符串范例

下面是 Ascii 的定义,它是一种能确保其内容始终为有效 ASCII 的字符串范例。这种范例使用了不安全特性来提供到 String 的零成本转换:
  1. mod my_ascii {
  2.     /// 一个ASCII编码的字符串
  3.     #[derive(Debug, Eq, PartialEq)]
  4.     pub struct Ascii(
  5.         // 必须只持有格式良好的ASCII文本:字节范围从`0`到`0x7f`
  6.         Vec<u8>
  7.     );
  8.     impl Ascii {
  9.         /// 从`bytes`的ASCII文本中创建`Ascii`。如果`bytes`包含
  10.         /// 任何非ASCII字符,则返回`NotAsciiError`错误
  11.         pub fn from_bytes(bytes: Vec<u8>) -> Result<Ascii, NotAsciiError> {
  12.             if bytes.iter().any(|&byte| !byte.is_ascii()) {
  13.                 return Err(NotAsciiError(bytes));
  14.             }
  15.             Ok(Ascii(bytes))
  16.         }
  17.     }
  18.     // 当转换失败时,给出无法转换的向量。这会实现
  19.     // `std::error::Error`,为保持简洁已省略
  20.     #[derive(Debug, Eq, PartialEq)]
  21.     pub struct NotAsciiError(pub Vec<u8>);
  22.     // 使用不安全代码实现的安全、高效的转换
  23.     impl From<Ascii> for String {
  24.         fn from(ascii: Ascii) -> String {
  25.             // 如果此模块没有bug,这就是安全的,因为格式
  26.             // 良好的ASCII文本必然是格式良好的UTF-8
  27.             unsafe { String::from_utf8_unchecked(ascii.0) }
  28.         }
  29.     }
  30.     ...
  31. }
复制代码
这个模块的关键是 Ascii 范例的定义。该范例本身是被标志为 pub 的,以令其在 my_ascii 模块之外可见。但是该范例的 Vec<u8> 元素 不是 公共的,因此只有 my_ascii 模块可以构造 Ascii 值或引用其元素。这使得模块的代码可以完全控制允许出现或不允许出现的内容。只要公共构造函数和方法能确保新创建的 Ascii 值是格式良好的并在其整个生命周期中都是云云,步伐的别的部分就不会违背该规则。究竟上,公共构造函数 Ascii::from_bytes 在同意从给定的向量中构造 Ascii 之前会仔细查抄它。为简便起见,我们没有展示任何方法,但你可以想象有一组文本处置惩罚方法,并确保 Ascii 值始终包含准确的 ASCII 文本,就像 String 的方法会确保其内容始终是格式良好的 UTF-8 一样。
这种安排让我们可以非常高效地为 String 实现 From<Ascii>。不安全函数 String::from_utf8_unchecked 会获取字节向量并从中构建一个 String,而不会查抄其内容是否为格式良好的 UTF-8 文本,该函数的契约要求其调用者对此负责。荣幸的是, Ascii 范例强制实行的规则正是应该满足 from_utf8_unchecked 契约的规则。正如 17.2 节所解释的那样,任何 ASCII 文本块也是格式良好的 UTF-8,因此 Ascii 的底层 Vec<u8> 可以立即用作 String 的缓冲区。
有了这些定义,便可以这样写:
  1. use my_ascii::Ascii;
  2. let bytes: Vec<u8> = b"ASCII and ye shall receive".to_vec();
  3. // 这个调用不需要分配内存或复制文本,只需做扫描
  4. let ascii: Ascii = Ascii::from_bytes(bytes)
  5.     .unwrap(); // 我们知道所选的这些字节肯定是正确的
  6. // 这个调用是零开销的:无须分配内存、复制文本或扫描
  7. let string = String::from(ascii);
  8. assert_eq!(string, "ASCII and ye shall receive");
复制代码
使用 Ascii 时不需要 unsafe 块。我们已经使用不安全利用实现了一个安全接口,并预备好仅依赖模块本身的代码而不必靠其用户的举动来满足它们的契约。
Ascii 只不过是 Vec<u8> 的包装器,但隐蔽在对其内容实行额外规则的模块中。这种范例称为 newtype,这是 Rust 中的一种常见模式。Rust 本身的 String 范例以完全雷同的方式定义,不过它的内容被限定为 UTF-8,而不是 ASCII。究竟上,标准库中 String 的定义是这样的:
  1. pub struct String {
  2.     vec: Vec<u8>,
  3. }
复制代码
在机器层面,由于不认识 Rust 的范例,newtype 及其元素在内存中具有雷同的表示,因此构造 newtype 根本不需要任何机器指令。在 Ascii::from_bytes 中,表达式 Ascii(bytes) 被简单地看作 Vec<u8> 的一种表观,只是它现在持有一个 Ascii 值。同理, String::from_utf8_unchecked 在内联时可能也不需要机器指令,由于 Vec<u8> 现在直接作为 String 使用。
22.4 不安全函数

unsafe 函数看起来就像前面加了 unsafe 关键字的普通函数。 unsafe 函数的主体自动被视为 unsafe 块。
只能在 unsafe 块中调用 unsafe 函数。这意味着将函数标志为 unsafe 会告诫其调用者,为避免未定义举动,该函数具有他们必须满足的契约。
例如,下面是前面先容的 Ascii 范例的新构造函数,它会从字节向量构建 Ascii,而不查抄其内容是否为有效的 ASCII:
  1. // 以下代码必须放在`my_ascii`模块内部
  2. impl Ascii {
  3.     /// 从`bytes`构造`Ascii`值,不检查`bytes`中是否真正包含格式良好的ASCII
  4.     ///
  5.     /// 这个构造函数是不会出错的,它会直接返回`Ascii`,而不会像
  6.     /// `from_bytes`那样返回`Result<Ascii, NotAsciiError>`
  7.     ///
  8.     /// # 安全性
  9.     ///
  10.     /// 调用者必须确保`bytes`只包含ASCII字符:各字节
  11.     /// 都不大于0x7f。否则,其行为就是未定义的
  12.     pub unsafe fn from_bytes_unchecked(bytes: Vec<u8>) -> Ascii {
  13.         Ascii(bytes)
  14.     }
  15. }
复制代码
调用 Ascii::from_bytes_unchecked 的代码大概已经以某种方式知道了本身手中的向量只会包含 ASCII 字符,因此 Ascii::from_bytes 坚持要实行的查抄只是浪费时间,调用者也将不得不编写代码来处置惩罚他知道永远不会发生的 Err 结果。 Ascii::from_bytes_unchecked 能让这样的调用者回避查抄和错误处置惩罚。
但早些时候,为了确保 Ascii 值是格式良好的,我们夸大了 Ascii 的公共构造函数和方法的紧张性。 from_bytes_unchecked 难道不能履行这一责任吗?
并非云云,实在 from_bytes_unchecked 通过它的契约将这些义务推脱给了调用者。这个契约的存在使得将这个函数标志为 unsafe 是准确的:虽然函数本身没有实行任何不安全利用,但它的调用者必须服从某些不能靠 Rust 自动实行的规则来避免未定义举动。
真的可以通过破坏 Ascii::from_bytes_unchecked 的契约来导致未定义举动吗?是的。可以构造一个包含格式错误的 UTF-8 的 String,如下所示:
  1. // 将这个向量想象成用来生成ASCII的一些复杂过程的结果。但这里有问题!
  2. let bytes = vec![0xf7, 0xbf, 0xbf, 0xbf];
  3. let ascii = unsafe {
  4.     // 如果`bytes`中存有非ASCII字节,就违反了这个不安全函数的契约
  5.     Ascii::from_bytes_unchecked(bytes)
  6. };
  7. let bogus: String = ascii.into();
  8. // `bogus`现在持有格式错误的UTF-8。解析其第一个字符会生成一个不是有效Unicode
  9. // 码点的`char`。这是未定义行为,所以语言无法说明这个断言应该是什么样的行为
  10. assert_eq!(bogus.chars().next().unwrap() as u32, 0x1fffff);
复制代码
在某些平台上某些版本的 Rust 中,会观察到此断言失败并显示以下风趣的错误消息:
  1. thread 'main' panicked at 'assertion failed: `(left == right)`
  2.   left: `2097151`,
  3. right: `2097151`', src/main.rs:42:5
复制代码
这两个数值在我们看来明明是相当的——这不是 Rust 的错,而是前一个 unsafe 块所导致的。当我们说未定义举动会导致无法猜测的结果时,就是这个意思。
这个例子说明了关于 bug 和不安全代码的两个关键究竟。


  • 在 unsafe 块之前发生的 bug 可能会破坏契约。 unsafe 块是否会导致未定义举动不光取决于块本身的代码,还取决于为其提供利用目的的代码。 unsafe 代码为满足契约所依赖的一切都与安全有关。仅当模块的别的部分都能准确维护 Ascii 的稳定条件时,基于 String::from_utf8_unchecked 从 Ascii 到 String 的转换才是有明白定义的。
  • 脱离 unsafe 区块后,仍可能出现此处违约的结果。由于没有服从不安全特性的契约而招致的未定义举动通常并不会发生在 unsafe 块内部。如前所述,伪造 String 的举动可能直到步伐实行了很久之后才引发问题。
本质上,Rust 的范例查抄器、借用查抄器和其他静态查抄都是在查抄你的步伐并试图构建出证据,证明它不会表现出未定义举动。假如 Rust 能乐成编译步伐,那么就意味着它乐成地证明了你的代码是准确的。而 unsafe 块是这个证明中的一个缺口,也就是说, unsafe 块就相当于你对 Rust 说:“这段代码很好,请相信我。”你的声明准确与否可能取决于步伐中会影响到此 unsafe 块的任意部分,并且其错误的结果也可能会出现在受此 unsafe 块影响的任意所在。写出 unsafe 关键字,就相当于你在提醒本身没能充分利用该语言的安全查抄。
假如可以选择,你自然更喜欢创建不需要契约的安全接口。这些接口更轻易使用,由于用户可以依靠 Rust 的安全查抄来确保他们的代码没有未定义举动。即使你的实现使用了不安全特性,最好照旧使用 Rust 的范例、生命周期和模块系统来满足它们的契约,同时最好只使用你能自行包管的特性,而不是把责任转嫁给你的调用者。
不过遗憾的是,在现实开辟中遇到不安全函数的情况并不少见,而这些函数的文档并没有认真地解释过它们的契约。因此,你要根据本身的经验和对代码举动方式的相识自行推断出规则。假如你曾焦虑不安地想知道用 C API 或 C++ API 所做的事情是否正常,那么对这种感觉肯定也感同身受。
22.5 不安全块照旧不安全函数

你可能想知道应该使用 unsafe 块照旧将整个函数都标志为 unsafe。我们保举的方法是先对函数做一些判断。


  • 假如能正常编译,但仍可能以导致未定义举动的方式滥用函数,则必须将其标志为不安全。准确使用函数的规则是它的契约,契约的存在意味着函数是不安全的。
  • 否则,函数就是安全的。也就是说,对函数的任何范例良好的调用都不会导致未定义举动。这样的函数不应该标志为 unsafe。
函数是否在函数体中使用了不安全特性无关紧要,紧张的是契约存在与否。之前,我们曾展示过一个没有使用不安全特性的不安全函数,以及一个使用了不安全特性的安全函数。
不要仅仅由于函数体中使用了不安全特性就把安全的函数标志为 unsafe。这会让函数更难使用,并使那些期望在某处找到契约说明的读者感到狐疑(只要是 unsafe 就理当有契约说明)。相反,应该使用 unsafe 块,即便整个函数体只有这一个块。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

勿忘初心做自己

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表