本文深入讲解了 Rust 编译器的各个阶段,从词法分析、语法分析到生成 LLVM 代码。文章详细介绍了词法分析(Lexing)、语法分析(Parsing)、抽象语法树(AST)、高级中间表示(HIR)、类型化高级中间表示(THIR)、中间中间表示(MIR)以及 LLVM 代码生成等关键步骤,并解释了每个阶段的作用和原理
<img width="1500px" height="460px" src="./images/header_rust_compiler2.jpg" />
你可能已经知道,所有编写的程序最终都会转换为二进制指令(那些花哨的 0 和 1)。为什么?因为 CPU 无法理解我们的源代码,它只能理解 0 和 1。由于我们无法通过编写 0 和 1 来编写代码或与内存交互,因此我们必须创建一些抽象,以便以人类可读的方式编写程序。
用于编写代码以与内存交互的最薄的抽象层是 汇编 (Assembly)。它是一种底层语言,可以帮助我们编写人类可读的代码,直接与内存交互,并提供对计算机操作的细粒度控制。 虽然汇编语言具有很高的效率和速度,但由于更容易出现内存问题和编写代码的复杂性,因此不常用。那么我们如何安全地与内存交互呢?
第二层抽象为我们提供了高级编程语言(如 Rust、C 和 Go)。这些语言比汇编语言更具人类可读性,具有语法糖,可隐藏程序员的大部分复杂性和内存管理。有了这个,程序员可以用更少的复杂性和更少的错误编写程序。不同的语言以不同的方式处理内存分配和释放,有些语言有效地管理内存,而有些语言则不然,每种语言都有其自身的权衡。但是等一下……我不是刚刚告诉过你 CPU 只能理解 0 和 1(二进制)吗?
我们无法直接在 CPU 上执行高级编程语言程序,我们必须将这些语法糖程序转换为二进制代码。这种转换由编译器完成。我们将重点关注 Rust 代码如何转换为二进制代码,以及编译期间发生哪些内部检查。我们将逐层剥离并理解每个阶段。考虑到这一点,让我们开始吧 😄。
开发人员编写的代码是人类可读的,允许其他人轻松阅读和理解它。但是,编译器无法理解人类编写的代码。我们需要将源代码转换为二进制代码,这是 CPU 唯一可以理解的格式。
虽然生成二进制代码需要几个步骤,但在高级别上,这些步骤分为三个主要支柱:前端 (Frontend)、中间 (Middle) 和后端 (Backend)。这分解了将源代码转换为机器代码的复杂过程。
<img width="1500px" height="560px" src = "./images/compiler_three_pillers.png" /> <br/>
在前端,你有 Rust 代码。在后端,你拥有由 LLVM (Low Level Virtual Machine,底层虚拟机) 生成的二进制机器代码,该代码直接在目标机器上运行。在中间,会发生所有 Rust 特有的所有权和借用检查。
我们将剥离每一层,了解 Rust 编译的工作原理。如果我们稍微放大 Rust 编译的三个支柱,这就是我们得到的,如下图所示。
<img width="1500px" height="560px" src = "./images/compiler_all_stages.png" /> <br/>
我不希望你了解上图中显示的所有术语,但请不要担心,在本博客结束时,你将了解所有这些术语。让我们逐步进行,剥离每一层,以了解编译代码时会发生什么。
牢记这张大图,让我们开始剥橙子 🍊。
让我们以以下示例作为源代码:
fn main() {
// Let's do some investigation :)
// 让我们做一些调查 :)
let some = String::from("chinna");
println!("Say my name:");
println!("说出我的名字:");
println!("{}", some);
time_pass(&some);
}
fn time_pass(pass: &String) {
println!("Time passing with this guy: {}", pass);
println!("和这家伙一起消磨时间:{}", pass);
}
此步骤是编译开始的第一个步骤。编译器首先将 .rs 文件读取为纯文本,然后将此线性文本分解为 词(Token),如 fn、some、{——这称为 词法分析(Lexing)。
然后,编译器将这些词转换为称为 AST(抽象语法树)的树状结构,并且此 AST 仍然非常类似于源代码,但它位于树状结构中。这被称为 语法分析(Parsing)。你可以在此处查看我们采用的代码示例的 AST 版本。因此,所有宏都会在此层中展开。
<img src="./images/first_step.png" />
AST 将所有句法代码捕获到树状结构中。你可能会问,为什么我们需要这样做?
好吧,编译器无法直接理解此线性源代码。源代码是为人类可读性而设计的语法糖,而不是为编译器设计的。抽象语法树 (AST) 抽象出某些细节,它是一种树数据结构,最能代表源代码的语法结构。你可以在此 Wikipedia 页面上了解有关 AST 的更多信息。
嗯,在解析词并将它们转换为 AST 之后,下一层就开始了。此时,AST 非常类似于源代码,其中仍然包含大量的语法糖,例如 for 和 match。
我们需要剥离这种语法糖以简化 AST。此解糖过程的结果是 AST 的一种形式,称为 HIR(高级中间表示)。HIR 仍然接近用户最初编写的内容,但它会删除语法糖,例如,将 for 循环转换为具有迭代逻辑的 loop。删除所有冗余后,HIR 现在是 AST 的更便于编译器使用的抽象表示形式。
通过删除语法糖来简化或转换 AST 的过程称为 降低(lowering),顺便说一句,你可以通过运行以下命令来检查 AST 的 HIR 表示形式:rustc +nightly -Z unpretty=hir-tree src/main.rs。
通过将 HIR 进一步降低并检查代码的所有类型是否都正确使用,例如,你不能将整数与字符串相加,当然你可以在 JavaScript 中做这样的事情 🤡。
<div align="center"> <img src="./images/javascript_meme.jpg"/> </div>
在完成所有类型检查之后,现在 THIR(类型化高级中间表示) 以 AST 形式表示。顾名思义,THIR 是 HIR 的较低版本,其中所有类型都已填充,这在类型检查完成后是可能的。
在将 HIR 降低到 THIR 并且在 MIR 不安全检查器遍历 THIR 表示形式之前,它会检查每个表达式是否存在不安全操作(如原始指针解引用、对不安全函数的调用、静态 mut 访问、联合字段访问),并验证这些操作是否仅出现在不安全上下文(不安全块或函数)中。
因为 unsafeck 需要类型化表达式,所以它被放置在类型检查和 THIR 构建之后,但在 MIR 构建之前。此位置允许它尽早且精确地执行 Rust 的安全保证,确保所有不安全代码都正确地包含在显式不安全上下文中。
降低之后,HIR 成为 AST 的便于编译器使用的抽象。从这里开始,抽象的下一层开始,这是 Rust 编译器的核心。MIR(中间中间表示)是检测许多经典内存错误(如竞争条件和释放后使用错误)的阶段。如果发现此类错误,编译器将直接抛出错误。
MIR 将你的代码表示为控制流图 (CFG)。将其视为详细的流程图。每个 if、loop 和 match 都会被分解为基本块以及它们之间的显式“go-to”跳转。
为了保证安全,编译器不能只检查“快乐路径”。它必须分析你的代码可能采用的每条路径。如果此 if 为真会发生什么?如果它为假会发生什么?如果此循环运行零次会发生什么?
CFG 使所有这些路径都变得明确。借用检查器可以系统地遍历此流程图,跟踪每个变量的状态(拥有、借用、移动),通过每个可能的分支和循环,确保永远不会违反任何规则。你可以在此处了解有关 CFG 的更多信息,为了查看我们的代码示例的 MIR 版本,你可以此处查看
<div align="center"> <img width=800 height=500 src="./images/CFG.png"/> </div>
简而言之,MIR 为编译器提供了详尽检查每条可能路径并保证满足 Rust 安全不变量的结构。该系统非常强大,是 Rust 安全性的基础。
unsafe 关键字创建了一个重要的例外。这是程序员承诺他们将手动遵守 Rust 的安全规则。如果该承诺被打破,编译器的基本假设将失效。这可能会损害整个程序的行为,无论周围的代码看起来多么“安全”。
最终,如果 Rust 程序没有未定义行为,则该程序被认为是可靠的。通过编译器检查的安全代码会自动实现这种可靠性。
我们正在进入编译的最后阶段。在 MIR 阶段执行所有 Rust 借用和所有权检查后,编译器将在 MIR 级别应用优化,例如删除死代码和简化控制流。
然后,MIR 被转换为 LLVM IR(低级虚拟机中间表示),LLVM 后端使用的一种平台无关的中间表示。LLVM IR 与汇编类似,但它稍微高级且更具人类可读性。
Rust 编译器的代码生成阶段主要由 LLVM(低级虚拟机) 完成。LLVM 是用于构建编译器的一组工具,最著名的是 C/C++ 编译器 Clang 使用。最后,经过所有严格的优化后,LLVM IR 将被转换为目标平台的机器代码(例如,x86_64 或 ARM64)。因此,为一种架构编译的二进制文件无法在另一种架构上运行,而无需模拟或转换,这意味着你无法在 ARM64 处理器上运行 x86_64 二进制代码,反之亦然。
<div align="center"> <img width=800 height=500 src="./images/llvm.png"/> </div>
rust_compiler_video_by_daniel<br/> stack_overflow_discussion<br/> llvm_video<br/> stack_overflow_discussion_of_llvm rust_book_on_undefined_behavior
<img width="1500px" height="460px" src="./images/rust_compiler_footer.png" />
- 原文链接: github.com/baindlapranay...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!