跳转至

编译框架学习笔记

Writted on 250217

Author: Jack_hui

从高级代码到机器语言

我们目前在各种ide上写的代码都是高级语言代码,比如PythonCC++Java等各种语言,这些语言机器无法直接识别并运行,因此需要一个媒介来进行转换。不同的语言需要不同的媒介,比如针对PythonJavaScriptshell的解释器,以及针对CC++Java等语言的编译器。并且根据媒介的不同,前者被称为解释型语言,后者被称为编译型语言。对于解释型语言的翻译过程,可类比现实生活中的同声传译,即解释器一行行的将高级语言代码翻译成机器二进制代码,交给机器运行;对于编译器,则是将高级语言代码全部翻译成机器二进制代码以后,再交给机器运行。由此不难看出解释型语言更加灵活,可根据硬件条件变化及时做出响应(JIT);而编译型语言编写的程序由于提前转化完毕,执行速度更快(AOT)。

编译过程

本次主要重点学习了编译相关的内容,接下来简要介绍一下编译过程: 编译器大体可以分为三部分:编译前端编译优化以及编译后端

编译前端

其中编译前端通常包括词法分析、语法分析、语义分析以及生成中间代码,

  • 词法分析:将源代码分解成一个个独立的单元,每个单元都事先人为规定好的,示例如下
    C
    1
    2
    3
    4
    5
    #include<stdio.h>
    // 词法分析将下段代码拆解成int、main、(、)、{、return、0、;、}
    int main(){
        return 0;
    }
    
  • 语法分析:接收词法分析的输出,生成抽象语法树(AST),只关心编写的高级代码形式上是否正确,而不关心逻辑是否存在问题。
  • 语义分析:通过遍历抽象语法树,进一步检查编写的高级代码逻辑上是否存在问题。
  • 中间代码:作为连接硬件与软件的媒介,软件工程师只需掌握前端高级语言,硬件工程师只需掌握相关硬件描述语言,然后两者共同学习中间代码的声明以及编写规则,便可高效完成开发。

编译优化

编译优化即对中间代码的优化,优化的方式有很多种,可根据个人按需进行,其中每种优化对应着一个pass,pass直观理解就是扫描源程序一遍,过程中解决特定的优化目标。整个编译优化过程通常需要多个pass,比如冗余代码删除,可用表达式分析等。

编译后端

编译后端主要将优化后的中间代码转化为目标机器代码,比如基于X86ARMMIPSRISC-V等架构的二进制代码。

目前主流编译框架

  • Windows: Windows本身提供了MSVC编译工具链,集成在Visual Studio中。当然,也提供了MinGW以及MSY32等工具来模拟Unix/Linux操作环境,使用gcc进行编译。
  • MacOs: MacOs早期使用的是GNU系列下的gcc等相关编译工具链,后来由于版本以及开源管理等问题转而开发了现在为人熟知的LLVM,其中clang就发挥着LLVM系列下编译前端的作用。
  • Linux: 使用GNU系列下的GCC作为主要编译工具,其中gcc以及g++发挥着编译前端的作用,通常分别用来处理CC++

GCC

GCCGNU编译工具链下的重要工具,配合Linux内核进行使用。整个工作流程如下所示:

首先进行预编译,生成.i文件,主要引入头文件相关内容以及替换实现定义好的宏,然后将其转化成汇编代码,即.s文件,这里也可以看作是中间代码,紧接着根据中间代码生成可重定位的二进制文件(.o文件),最后根据文件之间的相互依赖关系进行链接,生成最终的可执行文件(.exe)。

LLVM

LLVM是苹果公司后来脱离GNU体系发展的成套编译工具链,其中日常接触比较多的clang就属于LLVM系列,发挥着编译前端的作用。这套体系整个编译流程如下所示:

LLVM下编译过程的模块化更加明显,编译前端,编译优化以及编译后端被很好地划分出来。通过前端生成的.ll文件或者.bc文件(为中间代码的两种表示形式)以及相关pass进行优化,并将优化后产生的DAG图传递给编译后端生成相应的.s汇编文件,并在此基础之上生成二进制.o文件以及可执行文件.exe等。

两者的区别与联系

Example

可以发现GCC没有对编译过程的每个阶段作明显的区分,前后端耦合度过高,也就是前后端之间经常直接进行信息传递。因此添加新语言或新硬件平台需要修改大量代码,扩展性较差。不过由于发展历史悠久,经过长期优化,某些场景下生成代码的执行效率更高(如复杂数学运算、特定架构的深度优化等)。

LLVM发展比GCC晚,有了前车之鉴以及相关经验,因此可以将视角更多地放在模块化上,其严格分离前端、优化器和后端,通过统一的中间表示LLVM IR连接。因此开发者可以独立开发新语言前端(如Clang用于C/C++)或硬件后端,复用优化器模块。

使用GCC以及clang编译源程序示例

这里使用简单的C程序作为待编译的源程序代码,示例代码如下:

C
1
2
3
4
5
#include<stdio.h>
# define HELLOWPRLD "hello world\n"
int main(){
    printf(HELLOWORLD);//
}

GCC编译源程序

Bash
# 对源文件进行预编译,引入头文件、替换宏以及删除注释等
gcc -E hello.c -o hello.i
# 生成汇编文件
gcc -S hello.i -o hello.s
# 生成待链接的二进制文件
gcc -c hello.s -o hello.o
# 生成可执行文件
gcc hello.o -o hello
#执行
./hello

clang编译源程序

Bash
# 对源文件进行预编译,引入头文件、替换宏以及删除注释等
clang -E -c hello.c -o hello.i
# 生成中间表示,"bc"为二进制文件,"ll"更方便人阅读
clang -emit-llvm hello.i -c -o hello.bc
clang -emit-llvm hello.i -S -o hello.ll
# 省略了优化pass,直接生成汇编文件
clang -S hello.ll -o hello.s
# 生成可执行文件
clang hello.s -o hello.exe
#执行
./hello