“unsafe”是否会破坏 Rust 的保证?

当人们第一次听说 Rust unsafe 时,往往会产生疑问。一个很正常的问题是:”等等,这不是有违初衷吗?虽然这是一个完全合理的问题,但答案既简单明了,又有很多细微差别。因此,让我们来谈谈这个问题。

(顺便说一句,直接的答案是 “不”)

内存安全语言及其实现

思考这类问题的第一种方法是记住编程语言及其实现是两码事。这一点有时很难记住,因为许多编程语言只有一种实现,但即使在这种情况下,这一一般原则也适用。也就是说,你所实现的语言有其所需的语义,然后是让该语言的程序实现这些功能的代码库。

让我们用一种 “真正的 ”编程语言来讨论这个问题: Brainfuck Brainfuck 程序由八个不同的字符组成,每个字符执行一种操作。Brainfuck 程序只能做这八件事,仅此而已。

然而,这种简单性引出了两个关于抽象语言与其实现之间差异的有趣观点。

实现中的不安全性

首先,语言本身的属性与语言实现的属性是不同的。例如,Brainfuck 语言的语义并不提供调用某种可重用函数抽象的机会。然而,当我们编写 Brainfuck 解释器或编译器时,我们可以使用支持函数的编程语言。但这并不能改变Brainfuck程序本身不能定义或调用函数的事实。

事实上,我们经常希望用另一种语言来实现一种语言,而这种语言比我们要实现的语言拥有更多的能力。例如,虽然可以用 Brainfuck 实现 Brainfuck,但用一种可以定义和调用函数的语言来实现要容易得多。另一个典型的例子是与硬件交互。Brainfuck 中的 .指令可以让你产生输出。但要真正实现向终端输出,你需要与平台的 API 进行交互。但在 API 内部,你最终会遇到一个没有抽象层的地方:你需要直接与硬件对话。这样做并不能静态地保证内存安全,因为硬件/软件接口通常归结为 “在这个任意内存位置放置一些信息,然后硬件会从那里获取信息”。我们的 Brainfuck 程序与其他程序一样,都是在一个环境中运行;最终,它必须在某些时候与底层操作系统或硬件进行协调。但我们的实现必须这样做,并不意味着我们的 Brainfuck 程序会直接这样做。用 Brainfuck 编写的程序的语义并不会因为实现可以(而且必须)做这些语义之外的事情而改变。

让我们以 Ruby 为例,看看它是否比 Brainfuck 更现实。Ruby 不允许我们从 Ruby 本身修改任意内存地址。这意味着纯 Ruby 程序不可能产生分段故障。但在现实世界中,Ruby 程序可能会出现分段故障。如果 Ruby 解释器的代码中存在错误,就有可能出现这种情况。当然,是我们的 Ruby 代码调用了导致分段故障的函数,但真正的故障在于 Ruby 权限之外的代码。

这种情况的发生并不意味着 Ruby 在内存操作方面的保证毫无用处,也不意味着 Ruby 会像其他允许你理所当然地操作任意内存的语言程序一样出现分段错误。但这也确实意味着,我们不需要通过查看整个程序来找出问题所在:问题来自我们的非 Ruby 代码。Ruby 的保证帮助我们排除了很多疑点。

扩展语言和不安全性

第二个特性是,某些实现可以任意扩展语言。例如,我可以编写一个 Brainfuck 解释器,然后说我支持第九条指令 @,当调用它时会终止程序。这将是一种不可移植的扩展,用我的变体编写的 Brainfuck 程序将无法在其他解释器中运行,但有时,这种技术是有用的。

许多语言发现这类扩展非常有用,因此提供了一种称为 “外来函数接口 ”的功能,允许程序调用不同语言的代码。这就提供了在语言本身的领域之外做事情的能力,这可能非常有用。Brainfuck 并不提供 FFI,但如果它提供了,你可以想象 @ 可以用它来实现,只要以某种方式(通常是作为某种库)包含扩展功能,使用 @ 的程序就可以再次移植。

就像我们的实现有能力在我们的语言语义之外做一些事情一样,FFI 和类似的扩展机制也给了我们做任意事情的能力。我可以为 Ruby 写一个可以写入任意内存的扩展。而且我还能引发隔行故障。我们知道,如果发生了故障,责任并不在我们的 Ruby 代码,而是在我们的实现或 FFI。

那么,这样的内存安全吗?

如果现实世界中的程序有能力导致内存问题,我们却可以称一种语言为 “内存安全”,这似乎有些矛盾。但问题是,造成问题的并不是该语言中的程序:而是 FFI,它使用的是完全不同的语言;或者是实现问题,由于需要与操作系统或硬件交互,实现必须做内存不安全的事情。因此,“内存安全语言 ”的定义通常被理解为指不存在实现缺陷或 FFI 缺陷的语言及其实现。在实践中,与那些明显不安全的语言相比,这些错误的发生率非常低,因此,即使你可能会对这些 “例外 ”感到不舒服,但这一实用定义还是起到了很好的作用。

但实际上,这些异常之所以可以接受,还有一个更深层次的原因,那就是我们首先是如何理解程序和编程语言的属性的。也就是说,这并不仅仅是 “这些例外看起来很好 ”这样的实际问题,它们在更深层次上其实是可以接受的。

程序、属性和证明

那么,我们是如何知道程序和编程语言是如何工作的呢?

计算机科学与计算机的关系,并不亚于天文学与望远镜的关系。

Edsger Wybe Dijkstra

计算机科学的一个有趣之处在于,它与包括数学在内的其他学科密切相关!因此,使用数学技术来理解计算机及其程序的历史由来已久。

建立知识的一种技术就是证明的思想。如果你获得了计算机科学学位,就会经常接触到证明。作为学位的一部分,我甚至在哲学系选修了一门逻辑课。

我不打算在这篇博文中向你全面介绍证明的概念,但有一些高层次的概念是有用的,以确保我们在继续讨论之前能够达成共识。

亚里士多德的三段论

下面是亚里士多德在公元前 350 年举出的一个非常经典的推理例子,这种推理形式被称为 “三段论”:

  1. 所有人都是凡人。
  2. 苏格拉底是人。
  3. 因此,苏格拉底是凡人。

前两句被称为 “命题”,第三句是结论。我们可以根据命题给我们提供的信息之间的逻辑关系来得出结论。

但我们怎么知道命题是真的呢?所有人都是凡人吗?这里重要的是,为了证明的目的,我们假设命题是真的。我们这样做是因为,在某种程度上,我们不可能知道一切。因此,我们必须从某个地方开始,对我们所知道的做一些假设。诚然,后来我们可能会发现一个事实来推翻我们的命题,这样我们的证明就不再起作用了。但世界就是这样运转的。这并不妨碍这类证明帮助我们获得关于世界的知识,以及世界是如何运作的,就我们目前所能知道的而言。

因此,在某种程度上,这也是为什么 Ruby 是一种内存安全的语言,即使 C 语言的扩展会出现 segfault:总有一些事情是我们必须假定为真的。在内存安全语言中,我们必须假定为内存安全的代码数量很少,而不是通过语义保证内存安全,而且最好在代码本身中指明。换句话说,在内存不安全语言中,我们需要相信内存安全的代码量很大,而在内存安全语言中,我们需要相信内存安全的代码量很小,而不是零。

逻辑和程序

随着时间的推移,我们对逻辑的理解也在不断加深。当然,我们甚至有了相互竞争的逻辑!这就变得有趣了,因为术语的含义可能略有不同。例如,在最新的逻辑学中,我们会把 “人都是凡人 ”这样的词称为公理,而不是命题。同样的意思:这是我们无需证明就能接受的东西。

随着计算机的出现,人们试图将数学逻辑规则应用到计算机中。我们甚至把执行经典逻辑运算的电路称为 “逻辑门”。数论和逻辑是计算机工作的基础。因此,当高级语言出现后,人们也开始对应用这些数学工具来理解程序感兴趣。这门学科被称为 “形式验证”,其总体思路是描述我们希望系统具备的各种属性,然后使用数学中的形式方法来证明这些属性是真实的。

这一研究领域非常深奥,我不打算在这里介绍其中的绝大部分内容。不过,我确实想探讨一下这一领域的一个特殊线索。

胡尔逻辑

“胡尔逻辑 ”是,嗯:

本文试图利用最早应用于几何研究、后来扩展到其他数学分支的技术,探索计算机编程的逻辑基础。这包括阐明可用于证明计算机程序属性的公理和推理规则集。本文举例说明了这些公理和规则,并展示了一个简单定理的正式证明。最后,还论证了研究这些课题可能带来的理论和实践上的重要好处。

顺便提一下,本文作者 C. A. R. Hoare 与 Rust 的创造者 Graydon Hoare 没有任何关系。

胡尔逻辑是如何运作的?我也不打算一一介绍,但总体思路是这样的: 为了弄清程序是否完成了它应该做的事情,我们需要推理程序执行后的状态。因此,我们需要能够描述执行前的状态及其与执行后状态的关系。于是就有了这个符号:

P { Q } R

P 是一个先决条件,Q 是一个程序,R 是程序在这些先决条件下执行的结果。

但大多数程序都有不止一条语句。那么我们该如何建模呢?霍尔给出了 “组合规则”:

如果 P { Q1 } R1R1 { Q2 }P { Q1; Q2 } R。R.

这样,我们就可以通过依次证明每个语句来建立程序。

霍尔逻辑非常简洁,而我在这里只是触及了皮毛。人们做了大量工作来扩展 Hoare 逻辑,使其包括程序的越来越多方面。但是,后来又发生了一些事情。

分离逻辑

2002 年,《分离逻辑》(Separation Logic: 共享可变数据结构逻辑》一书出版。

在与彼得-奥赫恩(Peter O’Hearn)等人的合作中,基于伯斯托尔(Burstall)的早期想法,我们开发了胡尔逻辑的扩展,允许对使用共享可变数据结构的低级命令式程序进行推理。

嗯,共享可变数据结构?我在哪里听说过……

让我们看看他们怎么说:

这些方法所面临的问题是,变异数据结构的程序的正确性通常取决于对这些结构共享的复杂限制。

这是肯定的。那么,我们该怎么办呢?

避免这一困难的关键在于引入一种新颖的逻辑运算 P * Q,即分离连接(有时也称为独立连接或空间连接),这种逻辑运算断言 P 和 Q 在可寻址存储空间的不相邻部分都成立。

我们可能会关心可寻址存储的哪些不相关部分?

我们的目的是捕捉机器语言的低级特征。我们可以把存储看作是描述寄存器的内容,而堆则是描述可寻址存储器的内容。

非常有用!

在我们讨论分离逻辑的工作原理之前,先来看看这一段,为什么要这样命名分离逻辑:

由于这些逻辑基于这样一种思想,即断言的结构可以描述将存储分离成互不相关的部分,因此我们开始使用分离逻辑这个术语,既指使用分离算子的谓词微积分扩展,也指由此产生的霍尔逻辑扩展。更准确的名称可能是存储分离逻辑,因为很明显,其基本思想可以推广到描述其他类型资源的分离。

故事情节越来越精彩。

总之,在分离逻辑中,我们使用的符号与霍尔逻辑略有不同:

{ P } C { Q }

这就是说,我们从前提条件 P 开始,如果程序 C 执行,它不会有未定义的行为,如果程序 C 终止,Q 将成立。

此外,还有一个 “框架规则”,由于我懒得为这篇文章安装能正确渲染数学的东西,所以我将使用 “框架规则 ”的符号:

If { p } c { q }

then { p * r } c { q * r }

where no free variable in r is modified by c.

从某种意义上说,在等式两边添加一些东西为什么很有趣?因为这样我们就能为程序中没有被 c 修改或变异的部分添加谓词。你可能会想到&mut T,甚至只是一般意义上的所有权:我们可以只对程序的这些单独部分进行推理,与程序的其他部分分开。换句话说,我们已经有了一些关于所有权甚至是借用位的基础思想,虽然这篇原始论文并不涉及并发性,但最终并发分离逻辑也会成为一种东西。

我认为这篇论文比我更能解释框架规则为何重要:

每个有效规范 {p} c {q} 都是 “严密 ”的,因为其足迹中的每个单元都必须由 c 分配或由 p 断言为活动的;“局部性 ”则是相反的属性,即所有断言为活动的单元都属于该足迹。框架规则的作用是从命令的局部规范中推断出适合于外层命令的更大足迹的更全局规范。

这就是所谓的 “局部推理”,而局部推理是非常了不起的。在谈这个问题之前,我还想给大家留下一段非常有趣的内容:

由于我们的逻辑允许程序使用不受限制的地址运算,因此构建任何通用垃圾收集器的希望都不大。另一方面,对于地址与整数不相交的旧逻辑来说,情况则更有希望。不过,很明显,这种逻辑允许人们做出断言,比如 “堆包含两个元素”,而垃圾收集器的执行可能会证伪这些断言,尽管在任何现实意义上,这种执行都是不可观测的。

言归正传。让我们来谈谈局部分析与全局分析。

全局分析与局部分析

对程序进行证明并非易事。但有一件事会让它变得更难,那就是对于你想分析的许多程序的许多属性,你都需要进行全局分析。以 Ruby 为例。Ruby 是一种非常动态的编程语言,这使得它对静态分析有相当大的阻力。

全局分析

这是一个 Ruby 程序。你知道这个程序是否成功执行了吗?

class Foo
  def self.bar
    "baz"
  end
end

p Foo.bar

是的,它会打印出 baz。那这个 Ruby 程序呢?

class Foo
  def self.bar
    "baz"
  end
end

require "foo_ext.rb"

p Foo.bar

foo_ext.rb 可能不包含任何相关代码,但也可能包含类似内容:

Foo.class_eval { undef :bar }

在这种情况下,当我们尝试调用 Foo.bar 时,它就不存在了:

<anonymous>': eval:13:in `block in <main>': undefined method `bar' for class `Foo' (NameError)
eval:13:in `class_eval'
eval:13:in `<main>'
-e:in `eval'

哎哟 因此,在这种情况下,我们需要我们程序的全部代码,才能弄清楚这里发生了什么。

顺便提一下,Sorbet 是一个非常酷的项目,旨在为 Ruby 添加类型检查功能。他们确实需要访问整个 Ruby 源代码才能进行分析。不过,他们也做出了一些决定,以帮助提高其可操作性;如果你在网上试用一下 Sorbet,类型检查程序的速度很快。用 Sorbet 试试上面的代码会发生什么?

 

editor.rb:5: Unsupported method: undef https://srb.help/3008
     5 |Foo.class_eval { undef :bar }
                         ^^^^^^^^^^

这是一个非常公平的权衡!在各种形式的分析中,选择某些限制来使他们想做的事情变得可行是很常见的。在我看来,Ruby 有如此多的弊端,不支持一些更晦涩难懂的东西是完全公平的。

局部分析

全局分析的反面就是局部分析。让我们考虑一下 Rust 与 Ruby 的对应关系:

 

struct Foo;

impl Foo {
    fn bar() -> String {
        String::from("baz")
    }
}

fn main() {
    dbg!(Foo::bar());
}

我们能知道这个程序是否有效吗?当然,一切都在这里。现在,如果我们尝试与 Ruby 代码相同的技巧,我们就会知道它是否有效,因为 Rust 无法像 Ruby 那样删除 bar 的定义。所以,让我们试试别的方法:

struct Foo;

impl Foo {
    fn bar() -> String {
        // body hidden
    }
}

fn main() {
    dbg!(Foo::bar());
}

我们能知道这里的 main 类型是否正确吗?可以,尽管我们对主体一无所知。这是因为 dbg! 可以接受任何实现 Debug 特性的值,而我们知道 Foo::bar() 返回 String,它实现了 Debug 特性。对 main 进行类型检查是 main 的本地实现,我们不需要查看它调用的任何函数的主体来确定它是否类型良好。我们只需要知道它们的签名。如果我们不要求 bar 具有类型签名,我们就必须查看它的主体,以确定它的返回值,并以此类推查看 bar 在其主体中调用的任何函数。与这种任意深入的过程不同,我们只需查看签名就可以了。

证明的可组合性

那么,为什么局部分析如此有用呢?第一个原因是速度或可扩展性,这取决于你如何看待它。如果运行全局分析检查,由于需要对整个代码进行检查,因此代码库越大,成本就越高。而本地分析只需要一个本地上下文,当你在项目中添加更多代码时,它不会变得更昂贵,只有当你改变本地上下文时才会。因此,当你试图将检查扩展到更大的项目时,本地分析至关重要。

但我个人也喜欢将其视为一种抽象。也就是说,全局分析是一种有漏洞的抽象:代码库中某一部分的变化会连带影响到其他部分。还记得关于框架规则的这句话吗?

框架规则的作用就是从命令的局部规范中推断出适合外层命令更大范围的全局规范。

如果我们有局部推理,就能确保局部变化不会超出这些变化的边界。只要函数的类型不发生变化,我们就可以随意改动函数体,而且我们知道程序的其他分析部分仍然完好无损。这一点非常有用,就像抽象在构建程序时非常有用一样。

Rustbelt

好了,我们在这里进行了真正的深入探讨。这和不安全的 Rust 有什么关系呢?

七年前,拉尔夫-荣格(Ralf Jung)、雅克-亨利-朱尔丹(Jaques-Henri Jourdan)、罗伯特-克雷伯斯(Robbert Krebbers)和德里克-德雷尔(Derek Dreyer)发表了《RustBelt:Rust 编程语言的安全基础》(RustBelt: Securing the Foundations of the Rust Programming Language)一文

在这篇论文中,我们首次给出了代表 Rust 现实子集的语言的正式(并经过机器校验的)安全证明。

但它比这更令人兴奋:

我们的证明是可扩展的,因为对于每一个使用不安全特性的新 Rust 库,我们都能说出它必须满足哪些验证条件才能被视为对语言的安全扩展。

非常好!

本文首先讨论了为什么验证 Rust 是一项挑战:

因此,为了克服这一限制,Rust 标准库的实现广泛使用了不安全的操作,例如 “原始指针 ”操作,而这种操作的别名是无法跟踪的。这些库的开发者声称,他们对不安全代码的使用已经进行了适当的 “封装”,也就是说,如果程序员使用这些库导出的 API,但在其他方面避免使用不安全操作,那么他们的程序就不会出现任何不安全/未定义的行为。实际上,这些库扩展了 Rust 类型系统的表现力,以模块化、可控的方式放松了对别名可变状态的所有权约束

这基本上就是本篇文章开头提出的问题:如果一个函数的实现中包含了unsafe代码,你能诚实地说这个函数是安全的吗?

他们接着描述了项目面临的第一个挑战:选择正确的逻辑对 Rust 进行建模(为清晰起见,对部分信息进行了删减):

Iris 是一个用于高阶并发分离逻辑的语言通用框架,在过去的一年中,它为在 Coq 中对程序进行机器检查证明提供了战术支持。作为一种分离逻辑,Iris 内置了对所有权进行模块化推理的支持。此外,Iris 的主要卖点还在于它只需使用一小套原始机制,就能为不同领域推导出定制的程序逻辑。就 RustBelt 而言,我们使用 Iris 衍生出了一种新颖的生命周期逻辑,其主要特点是借用命题的概念,与 Rust 中用于跟踪别名的 “借用 ”机制如出一辙。

分离逻辑

那么它是如何工作的呢?证明分为三个部分。如果你想了解具体细节,请去阅读这篇论文,它写得很好。

  1. 验证类型规则在语义解释时是否合理。
  2. 验证如果程序在语义上类型合理,是否会出现不安全行为。
  3. 对于任何使用不安全的库,检查它是否满足其接口所要求的语义。

如果这三点都为真,那么程序的执行就是安全的。这也很令人兴奋,因为有了 3,当有人编写新的不安全代码时,就可以让 RustBelt 告诉他们需要满足哪些属性,以确保他们的不安全代码不会引起问题。

弱点

当然,RustBelt 的后续工作还有很多:

我们没有模拟(1)原子访问的更宽松形式,Rust 在 Arc 等库中使用原子访问来提高效率;(2)Rust 的 trait 对象(类似于 Java 中的接口),由于它们与生命周期的交互,可能会带来安全问题;或者(3)发生恐慌时的堆栈回卷,这会导致类似于 C++ 中异常安全的问题。

不过,这些结果足以让我们发现标准库中的一些缺陷,而且如果我没记错的话,还以合理的方式拓宽了其中一个应用程序接口。RustBelt 的另一个有趣之处在于,它是在 Rust 获得非lexical lifetimes 之前发布的,不过,它对其进行了建模,因为人们知道非lexical lifetimes 即将到来。

理论与实证的对比

因此,我们已经有了一些实际证据,证明 Rust 中的不安全代码不会破坏 Rust 的保证:它所做的就是允许我们扩展 Rust 的语义,就像 FFI 允许我们扩展用支持 FFI 的语言编写的程序一样。只要我们的不安全代码在语义上是正确的,那么我们就没事。

……但如果语义不正确呢?

当事情出错时

也许我会在某个时候写一篇关于具体细节的文章,但这篇文章已经长得令人难以置信了。我就直说了:你可能会得到未定义的行为,这意味着任何事情都可能发生。你没有一个真正的 Rust 程序。这很糟糕。

但至少它在某种程度上是有范围的。然而,很多人都会搞错这个范围。他们会说

你只需要验证不安全代码块。

这是事实,但也有点误导。在讨论不安全代码之前,我想先举例说明。这段 Rust 代码可以吗?

struct Foo {
    bar: usize,
}

impl Foo {
    fn set_bar(&mut self, bar: usize) {
        self.bar = bar;
    }
}

这里没有不安全的诡计。这段代码虽然有点无用,但绝对安全。

让我们来谈谈不安全。不安全代码在不安全本身之外受到影响的典型例子是 Vec<T> 的实现。Vecs 看上去是这样的(实际代码与之不同,原因在这里并不重要):

 

struct Vec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

指针指向一行中的一堆 Ts,长度是当前有效的 Ts 数量,容量是 Ts 的总数。长度和容量是不同的,因此内存分配可以摊销;容量总是大于或等于长度。

这一特性非常重要!如果长度大于容量,当我们尝试索引 Vec 时,就会访问随机存储器。

因此,现在这个与 Foo::set_bar 相同的函数不再适用了:

 

impl<T> Vec<T> {
    fn set_len(&mut self, len: usize) {
        self.len = len;
    }
}

这是因为 Vec<T>的其他方法中的不安全代码需要依赖 len <= capacity 这一事实。因此,你会发现 Rust 中的 Vec<T>::set_len 被标记为不安全,尽管它并不包含不安全代码。

这就是为什么作为隐私边界的模块很重要:在安全的 Rust 代码中直接设置 len 的唯一方法是与 Vec<T> 本身处于同一隐私边界内的代码。因此,这就是同一个模块或其子模块。

这最终还是比整个代码库中的任何一行代码都要好,但并不像你一开始想象的那么小。

如何检查是否出错?

好吧,虽然 RustBelt 可以让你知道你的代码是否正确,但我怀疑你是否会跳进 Coq 写一些证明。你能做什么呢?Rust 提供了一个工具 miri,它可以解释你的 Rust 代码,并告诉你是否违反了某些不安全规则。但它并不完整,也就是说,miri 可以告诉你代码是否错误,但不能告诉你代码是否正确。但它仍然非常有用。

要获得 miri,可以将其与 Rust nightly 一起安装:

$ rustup +nightly component add miri

然后在 miri 下运行测试套件,使用

$ cargo +nightly miri test

有关详细信息,请查阅 miri 的文档

会出错吗?

人们写的不安全代码是好的还是坏的?

对于上述问题,答案显然是 “是的”。说 Rust 中所有不安全代码都是好代码的人显然在撒谎。但这里有趣的不是绝对值,而是灰色阴影。不安全是否普遍存在?Rust 是否与其他 MSL 一样存在内存相关的 bug,还是更接近内存不安全语言?

虽然现在还为时尚早,但我们已经有了一些初步结果,这些结果很有帮助。2022 年,谷歌报告称

迄今为止,在 Android 的 Rust 代码中发现的内存安全漏洞为零。

我很想知道最新的报告对此有何变化,但正如他们所说:”我们不指望这个数字会发生变化:

我们并不指望这个数字永远保持为零,但考虑到两个 Android 版本中新 Rust 代码的数量,以及使用 Rust 代码的安全敏感组件,这个结果意义重大。这表明 Rust 正在实现其预期目标,即防止 Android 最常见的漏洞源。在 Android 的许多 C/C++ 组件(如媒体、蓝牙、NFC 等)中,历史漏洞密度大于 1/kLOC(每千行代码 1 个漏洞)。根据这一历史漏洞密度,使用 Rust 很可能已经阻止了数百个漏洞进入生产环境。

我们将拭目以待,看看这些结果是否会在未来以及在谷歌之外得到复制。但至少看起来,在实践中,大多数不安全代码并没有导致问题。至少到目前为止是这样!

本文文字及图片出自 Does unsafe undermine Rust's guarantees?

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注