likes
comments
collection
share

【Rust精华小册】1.Rust编译过程讲解

作者站长头像
站长
· 阅读数 9

1.了解编译过程

目前主流编译平台有,GNU、MSVC、LLVM。因为rustc调用了llvm,因此我们以LLVM为例,我们从C语言的编译过程聊,再对比Rust,看它们的编译过程有何差异。

【Rust精华小册】1.Rust编译过程讲解 clang下载链接: releases.llvm.org/download.ht…

# 保存编译过程中的临时文件
$ clang -save-temps hello.c 
# 打印编译阶段
$ clang -ccc-print-phases hello.c
            +- 0: input, "hello.c", c -----------> hello.c >
         +- 1: preprocessor, {0}, cpp-output ----> hello.i
      +- 2: compiler, {1}, ir -------------------> hello.bc
   +- 3: backend, {2}, assembler ----------------> hello.s
+- 4: assembler, {3}, object --------------------> hello.o
5: linker, {4}, image ---------------------------> a.exe

第一步:预处理

输入是hello.c,输出是hello.i

#include <stdio.h>
int main() {
    printf("Hello World!\n");
    return 0;
}
# 等价的gcc指令:gcc -E hello.c -o hello.i
$clang -E -c hello.c -o hello.i
# 查看.i文件内容
$cat hello.i
  __attribute__((__format__ (scanf, 2, 0))) __attribute__ ((__nonnull__ (2)))
...
# 1650 "D:/usr/msys2/clang64/include/stdio.h" 2 3
# 2 "hello.c" 2
int main() {
    printf("Hello World!\n");
    return 0;
}

将.i 文件导出为LLVM IR后以备下一步使用

clang -emit-llvm hello.i -c -o hello.bc # 导出二进制的LLVM IR
clang -emit-llvm hello.c -S -o hello.ll # 导出文本类型的LLVM IR

第二部:编译

将预处理完的文件进行一些列的词法分析、语法分析、语义分析和优化后生成的汇编指令代码。 这一步我们就可以使用LLVM的llc命令对上一步的IR文件编译了。

# 等价的gcc指令:gcc -S hello.i -o hello.s
$llc hello.ll -o hello.s
$ cat hello.s
        .text
        .def    @feat.00;
        .scl    3;
        .type   0;
        .endef
        .globl  @feat.00
.set @feat.00, 0
        .file   "hello.c"
        .def    main;
        .scl    2;
        .type   32;
        .endef
        .globl  main                            # -- Begin function main
        .p2align        4, 0x90
main:                                   # @main
.seh_proc main
# %bb.0:
        pushq   %rbp
        .seh_pushreg %rbp
        subq    $48, %rsp
        .seh_stackalloc 48
        leaq    48(%rsp), %rbp
        .seh_setframe %rbp, 48
        .seh_endprologue
        callq   __main
        movl    $0, -4(%rbp)
        leaq    .L.str(%rip), %rcx
        callq   printf
        xorl    %eax, %eax
        addq    $48, %rsp
        popq    %rbp
        retq
        .seh_endproc
                                        # -- End function
        .section        .rdata,"dr"
.L.str:                                 # @.str
        .asciz  "Hello\302\240World!\n"

第三步:汇编

把汇编代码转变成机器可以执行的指令,过程相对编译阶段简单,没有复杂的语法,也不需要优化,只需要对照汇编指令和机器指令对照表一一翻译即可。

#等价的gcc指令:gcc -c add.s -o add.o
clang -fmodules -c hello.s -o hello.o

第四步:链接

目标文件和依赖的库 打包成一个可执行文件

clang hello.o -o hello

链接分为静态链接和动态链接。。

总结

【Rust精华小册】1.Rust编译过程讲解

到现在我们就可以回答一个问题:编译器究竟做了什么呢?

首先就是将源码转换为目标平台可以直接识别的指令文件。分为两类:可执行文件。 在编译最后产生的image,不同操作系统有不同的格式(这里的格式指的是文件的布局结构),在Windows通常是PE,Linux上则是ELF。

ELF格式

现在我们得到了可执行文件,我们在思考可执行文件究竟是什么? 答案就是可执行文件内包含了初始状态的进程数据。

通常可执行文件、目标文件、静态链接库(Linux的.a,Windows的.obj)和动态链接库(Linux的.so,Windows的DLL)都是ELF格式的文件

ELF文件中主要包含程序指令程序数据

ELF的结构:

  • File Header 主要包含了文件是否为可执行文件、目标硬件、目标操作系统、段表等。段表描述了各个段在文件中的偏移等信息。
  • .text section 代码段
  • .data section 数据段
  • .bss section 未初始化的全局变量和局部静态变量,在文件中不占空间。
  • ...

od -x ./hello # 以16进制查看文件 xxd -b ./hello # 以2进制查看文件 hexdump -C ./hello # 以16进制查看文件 file ./add # 查看文件的头信息 ldd ./add # 查看可执行文件依赖的动态库 objdump -h ./add # 打印ELF文件的各个段 size ./add # 查看ELF各个段的长度 readelf -h ./add # 查看ELF文件的信息 clang -ccc-print-phases hello.c # 查看编译过程

Rust中的编译过程

通过前面的介绍,我们知道LLVM有一个好处,就是将前端和后端通过IR中间语言隔离开了。

【Rust精华小册】1.Rust编译过程讲解 这样一来,Rust只需要实现一个前端就可以了。Rust实现的编译器就是rustc.exe,它就包含了rust前端编译器,LLVM和调用连接器。连接器后续极有可能也会使用llvm提供的连接器,目前还是使用mvsc或者GNU的连接器,这也是为什么安装Rust时,需要单独安装vs环境或者gcc环境的原因。

【Rust精华小册】1.Rust编译过程讲解

把这个过程说清楚之后,下一节我们来实践安装Rust。