简单记录一下 rust 的编译过程及相关文档。

编译过程

整个编译过程大致如下:

Rust 源码 —-> Tokens —-> AST —-> HIR —-> MIR —-> LLVM IR —-> 机器码

词法分析(Lexical Analysis)

词法分析是编译的第一阶段,其主要任务是将输入的源代码文本(字符序列)转换成一系列的词法单元(tokens)。这个过程涉及删除空白字符和注释、识别关键字、标识符、文字常量、运算符等。例如,源代码中的字符串 “let a = 5;” 将被分解为以下tokens:

let (关键字)
a (标识符)
= (赋值运算符)
5 (整数常量)
; (分号)

语法分析(Syntax Analysis)

随后的步骤是语法分析,这个阶段的目标是将词法分析阶段生成的tokens序列组织成一种结构化的表示形式,即抽象语法树(AST)。AST是源代码语法结构的树状表示,它把代码组织成一个层次化的树,这棵树的每个节点都代表源代码中的一个构造(如表达式、语句、声明等)。

还是用上面的例子,”let a = 5;” 的 AST 如下(使用 astexplorer):

{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 10,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 5,
            "raw": "5"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

HIR(high-level intermediate representation)

HIR是编译过程中比AST稍微低一级的一种结构,它会对原始的AST进行一些转换和简化。与AST相比,HIR就像是“去语法糖化”的版本。它把诸如for循环、if-let绑定和其他语法糖转换成更简单、更直接的等价形式,如while循环和match表达式。

这种表示形式的目的是让之后的编译步骤更容易处理,因为HIR中移除了很多复杂的语法结构,让编译器易于分析和实现特定的编译优化。HIR的设计是为了保持足够接近原始源码以便于错误报告和诊断,同时又处理了足够多的复杂性(例如,局部变量的名称解析和类型检查),这允许编译器在生成机器代码之前更容易地进行进一步的转换。

还是上面的例子,let a = 5; 生成的 HIR 如下:

#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() { let a = 5; }

另外这里不同版次的 rust 生成的 HIR 也会有所不同。

MIR(medium-level intermediate representation)

MIR 是一种更低级别的中间表示形式,更接近于机器代码的抽象级别。MIR专注于为编译器的后端优化和代码生成阶段提供一个简洁和均一的API。通过这种分层的中间表示法,Rust编译器能够维护高质量的错误报告,同时提供性能优化和代码生成的可能性。

上面的例子生成的 MIR 如下:

fn main() -> () {
    let mut _0: ();
    scope 1 {
        debug a => const 5_i32;
    }

    bb0: {
        return;
    }
}

MIR 里面有两个重要的概念是“scope”和“basic blocks”(通常以bb0, bb1, bb2等命名)。

scope

在MIR中,”scope”指的是源代码中变量或表达式的作用域边界。Scope用于跟踪变量的生命周期,帮助编译器决定何时可以安全地释放或重新利用内存。一个scope通常与源代码中的一对花括号(即代码块)相关联,但它也可以表示控制流结构(例如循环或条件语句)中的生命周期边界。 与生命周期分析相关的信息(例如,哪个scope一个特定的变量被引入,它在栈上的生命周期的起始和结束时刻)都在MIR中记录,并用于后续的静态分析和优化,包括确定何时可以安全地移动或删除数据。

基本块(basic blocks)

MIR由一系列 basic block 构成,这些基本块代表了控制流图中的节点。一个基本块(Basic Block,通常缩写为bb,如bb0, bb1等)是一个直线代码序列,意味着它没有分支(除了在末尾)也没有跳转点(除了开头)。简单来说,控制流只能从基本块的开始进入,并且一旦进入,就会按顺序执行到底。

每个基本块包含一系列的语句(statements),这些语句执行具体操作,如变量赋值、函数调用等,而末尾的终结指令(terminator)表明了控制流之后的去向。这个终结指令常见的形式有:跳转到其他基本块,返回值,以及条件分支等。

LLVM IR

这部分就生成 LLVM IR,然后用 LLVM 来生成机器码。

; ModuleID = 'playground.38c9f0d4595c787f-cgu.0'
source_filename = "playground.38c9f0d4595c787f-cgu.0"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@vtable.0 = private unnamed_addr constant <{ ptr, [16 x i8], ptr, ptr, ptr }> <{ ptr @"_ZN4core3ptr85drop_in_place$LT$std..rt..lang_start$LT$$LP$$RP$$GT$..$u7b$$u7b$closure$u7d$$u7d$$GT$17h2e320739000b13ecE", [16 x i8] c"\08\00\00\00\00\00\00\00\08\00\00\00\00\00\00\00", ptr @"_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h2f23eae2f7c19410E", ptr @"_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h6793cc830e11d105E", ptr @"_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h6793cc830e11d105E" }>, align 8

; std::sys_common::backtrace::__rust_begin_short_backtrace
; Function Attrs: noinline nonlazybind uwtable
define internal fastcc void @_ZN3std10sys_common9backtrace28__rust_begin_short_backtrace17hfcaebe6e09e0e118E(ptr nocapture noundef nonnull readonly %f) unnamed_addr #0 {
start:
  tail call void %f()
  tail call void asm sideeffect "", "~{memory}"() #7, !srcloc !4
  ret void
}

; std::rt::lang_start
; Function Attrs: nonlazybind uwtable
define hidden noundef i64 @_ZN3std2rt10lang_start17hed033a10ff81e16dE(ptr noundef nonnull %main, i64 noundef %argc, ptr noundef %argv, i8 noundef %sigpipe) unnamed_addr #1 {
start:
  %_8 = alloca ptr, align 8
  call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %_8)
  store ptr %main, ptr %_8, align 8
; call std::rt::lang_start_internal
  %0 = call noundef i64 @_ZN3std2rt19lang_start_internal17h38afc2c839c7262dE(ptr noundef nonnull align 1 %_8, ptr noalias noundef nonnull readonly align 8 dereferenceable(24) @vtable.0, i64 noundef %argc, ptr noundef %argv, i8 noundef %sigpipe)
  call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %_8)
  ret i64 %0
}

; std::rt::lang_start::
; Function Attrs: inlinehint nonlazybind uwtable
define internal noundef i32 @"_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h6793cc830e11d105E"(ptr noalias nocapture noundef readonly align 8 dereferenceable(8) %_1) unnamed_addr #2 {
start:
  %_4 = load ptr, ptr %_1, align 8, !nonnull !5, !noundef !5
; call std::sys_common::backtrace::__rust_begin_short_backtrace
  tail call fastcc void @_ZN3std10sys_common9backtrace28__rust_begin_short_backtrace17hfcaebe6e09e0e118E(ptr noundef nonnull %_4)
  ret i32 0
}

; core::ops::function::FnOnce::call_once
; Function Attrs: inlinehint nonlazybind uwtable
define internal noundef i32 @"_ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17h2f23eae2f7c19410E"(ptr nocapture noundef readonly %_1) unnamed_addr #2 personality ptr @rust_eh_personality {
start:
  %0 = load ptr, ptr %_1, align 8, !nonnull !5, !noundef !5
; call std::sys_common::backtrace::__rust_begin_short_backtrace
  tail call fastcc void @_ZN3std10sys_common9backtrace28__rust_begin_short_backtrace17hfcaebe6e09e0e118E(ptr noundef nonnull %0), !noalias !6
  ret i32 0
}

; core::ptr::drop_in_place<std::rt::lang_start<()>::>
; Function Attrs: inlinehint mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable
define internal void @"_ZN4core3ptr85drop_in_place$LT$std..rt..lang_start$LT$$LP$$RP$$GT$..$u7b$$u7b$closure$u7d$$u7d$$GT$17h2e320739000b13ecE"(ptr noalias nocapture readnone align 8 %_1) unnamed_addr #3 {
start:
  ret void
}

; playground::main
; Function Attrs: mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable
define internal void @_ZN10playground4main17h0639b84746c9a68bE() unnamed_addr #4 {
start:
  ret void
}

; std::rt::lang_start_internal
; Function Attrs: nonlazybind uwtable
declare noundef i64 @_ZN3std2rt19lang_start_internal17h38afc2c839c7262dE(ptr noundef nonnull align 1, ptr noalias noundef readonly align 8 dereferenceable(24), i64 noundef, ptr noundef, i8 noundef) unnamed_addr #1

; Function Attrs: nonlazybind uwtable
declare noundef i32 @rust_eh_personality(i32 noundef, i32 noundef, i64 noundef, ptr noundef, ptr noundef) unnamed_addr #1

; Function Attrs: nonlazybind
define noundef i32 @main(i32 %0, ptr %1) unnamed_addr #5 {
top:
  %_8.i = alloca ptr, align 8
  %2 = sext i32 %0 to i64
  call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %_8.i)
  store ptr @_ZN10playground4main17h0639b84746c9a68bE, ptr %_8.i, align 8
; call std::rt::lang_start_internal
  %3 = call noundef i64 @_ZN3std2rt19lang_start_internal17h38afc2c839c7262dE(ptr noundef nonnull align 1 %_8.i, ptr noalias noundef nonnull readonly align 8 dereferenceable(24) @vtable.0, i64 noundef %2, ptr noundef %1, i8 noundef 0)
  call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %_8.i)
  %4 = trunc i64 %3 to i32
  ret i32 %4
}

; Function Attrs: mustprogress nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #6

; Function Attrs: mustprogress nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #6

attributes #0 = { noinline nonlazybind uwtable "probe-stack"="inline-asm" "target-cpu"="x86-64" }
attributes #1 = { nonlazybind uwtable "probe-stack"="inline-asm" "target-cpu"="x86-64" }
attributes #2 = { inlinehint nonlazybind uwtable "probe-stack"="inline-asm" "target-cpu"="x86-64" }
attributes #3 = { inlinehint mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable "probe-stack"="inline-asm" "target-cpu"="x86-64" }
attributes #4 = { mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable "probe-stack"="inline-asm" "target-cpu"="x86-64" }
attributes #5 = { nonlazybind "probe-stack"="inline-asm" "target-cpu"="x86-64" }
attributes #6 = { mustprogress nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
attributes #7 = { nounwind }

!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}

!0 = !{i32 8, !"PIC Level", i32 2}
!1 = !{i32 7, !"PIE Level", i32 2}
!2 = !{i32 2, !"RtLibUseGOT", i32 1}
!3 = !{!"rustc version 1.78.0-nightly (bb594538f 2024-02-20)"}
!4 = !{i32 1162431}
!5 = !{}
!6 = !{!7}
!7 = distinct !{!7, !8, !"_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h6793cc830e11d105E: %_1"}
!8 = distinct !{!8, !"_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h6793cc830e11d105E"}

机器码

最终生成机器码,这个二进制的文件就是可以直接运行的程序。

参考