LLVM 学习(一) 初识LLVM
@2022-04-16 10:09:06
@sizaif
LLVM安装&环境搭建
- 官网下载预编译的二进制文件
- 修改环境变量指向
export PATH="$PATH:/usr/local/clang+llvm/bin"
- 检查使用
root@0187031113b5:/home/workhome# env | grep PATH
PATH=/usr/local/clang+llvm/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
root@0187031113b5:/home/workhome# clang --version
clang version 14.0.0
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/clang+llvm/bin
root@0187031113b5:/home/workhome# llvm-as --version
LLVM (http://llvm.org/):
LLVM version 14.0.0
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: skylake
root@0187031113b5:/home/workhome#
LLVM架构
- 使用可重用的组件构建的新的编译器体系结构
- 将现有语言重新设置到JIT或静态编译
其主要组件是前端,优化器和后端。前端解析源代码,检查其错误,并构建特定于语言的抽象语法树(AST)以表示输入代码。AST可选地将其转换为新的优化表示,并且优化器和后端在代码上运行。
优化器负责进行各种各样的转换,以改进代码的运行时间,例如消除冗余计算,并且通常或多或少地独立于语言和目标。
后端(也称为代码生成器)然后将代码映射到目标指令集。除了编写正确的代码外,它还负责生成利用受支持体系结构的特殊特性的好代码。编译器后端的公共部分包括指令选择、寄存器分配和指令调度。
LLVM的设计架构
支持一种新的编程语言只需重新实现一个前端,支持一种新的目标架构只需重新实现一个后端,前端和后端连接枢纽就是LLVM IR。
LLVM的含义
在不同的语义环境下,LLVM具有以下几种不同的含义:
- LLVM基础架构:即一个完整编译器项目的集合,包括但不限于前端、后端、优化器、汇编器、链接器、libcpp标准库、Compiler-RT和JIT引擎
- 基于LLVM构建的编译器:部分或完全使用LLVM构建的编译器
- LLVM库:LLVM基础架构可重用代码部分
- LLVM核心:在LLVM IR上进行的优化和后端算法
- LLVM IR:LLVM中间表示
LLVM基础架构的组成部分
- 前端:将程序源代码转换为LLVM IR的编译器步骤,包括词法分析器、语法分析器、语义分析器、LLVM IR生成器。Clang执行了所有与前端相关的步骤,并提供了一个插件接口和一个单独的静态分析工具来进行更深入的分析
- 中间表示:LLVM IR可以以可读文本代码和二进制代码两种形式呈现。LLVM库中提供了对IR进行构造、组装和拆卸的接口。LLVM优化器也在IR上进行操作,并在IR上进行了大部分优化。
- 后端:负责汇编码或机器码生成的步骤,将LLVM IR转换为特定机器架构的汇编代码或而二进制代码,包括寄存器分配、循环转换、窥视孔优化器、特定机器架构的优化和转换等步骤
下面这张图展示了使用LLVM基础架构时各个组件之间的关系
除此之外,各个组件之间的协作关系也可以以下面这种方式组织
两种方式的主要区别是程序源代码内部的链接是由LLVM或系统链接器完成的还是由LLVM IR链接器完成的,前者是默认方式,后者一般在开启链接优化(Link-Time Optimization)时采用
LLVM GCC 4.2 设计
链接LLVM和GCC编译的代码
- 可以安全地在编译器之间混合和匹配.o文件
- 可以安全地调用到与其他编译器一起构建的库中
链接时间优化
- 使用-O4对文件进行优化(例如内联、固定折叠等)
- 可以跨语言边界进行优化
LLVM中间数据结构
在LLVM中并不只存在LLVM IR一种中间表现形式,LLVM在不同编译阶段采用以下不同的中间数据结构:
- LLVM IR:
- 抽象语法树(AST):将源代码转换为LLVM IR时,Clang前端语法分析器和语义分析器的产出数据结构
- 有向无环图(DAG):将LLVM IR转换为特定机器架构的汇编代码时,LLVM首先将其转换为有向无环图(DAG)的形式,以方便进行指令选择,然后将其转换回三地址码的形式以进行指令调度
- MCModule类:为了实现汇编器和链接器,LLVM使用MCModule类将程序表示保存在对象文件(可重定向文件的一种,通常文件名以.o结尾)的上下文中
不同编译阶段的中间数据结构有以下两种存在方式:
- 内存中:需要编译驱动程序的帮助,将一个阶段的输出数据结构作为下一个阶段的输入数据结构
- 文件中:独立命令之间多数以文件为媒介进行交互,比如汇编器与链接器通过可重定向的
.o
对象文件进行交互
LLVM IR
(Intermediate Representation,IR)
LLVM的中间表示,本质上一种与源编程语言和目标机器架构无关的通用中间表示
LLVM IR是一种类似于RISC的低级虚拟指令集
LLVM是使用简单类型系统的强类型(例如,i32
是一个32位整数,i32**
是指向32位整数的指针)
LLVM IR 不使用一组固定的命名寄存器,它使用一个名为%字符的无限临时集合
LLVM IR代被设计成三种不同的形式:内存编译器IR,磁盘二进制.bc
表示(适合于即时编译器的快速加载),可读的汇编语言.ll
c
代码
unsigned add1(unsigned a, unsigned b) {
return a+b;
}
// Perhaps not the most efficient way to add two numbers.
unsigned add2(unsigned a, unsigned b) {
if (a == 0) return b;
return add2(a-1, b+1);
}
对应 llvmIR .ll文件
define i32 @add1(i32 %a, i32 %b) {
entry:
%tmp1 = add i32 %a, %b
ret i32 %tmp1
}
define i32 @add2(i32 %a, i32 %b) {
entry:
%tmp1 = icmp eq i32 %a, 0
br i1 %tmp1, label %done, label %recurse
recurse:
%tmp2 = sub i32 %a, 1
%tmp3 = add i32 %b, 1
%tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)
ret i32 %tmp4
done:
ret i32 %b
}
LLVM IR结构
- Module(模块)是一份LLVM IR的顶层容器,对应于编译前端的每个翻译单元(TranslationUnit)。每个模块由目标机器信息、全局符号(全局变量和函数)及元信息组成。
- Function(函数)就是编程语言中的函数,包括函数签名和若干个基本块,函数内的第一个基本块叫做入口基本块。
- BasicBlock(基本块)是一组顺序执行的指令集合,只有一个入口和一个出口,非头尾指令执行时不会违背顺序跳转到其他指令上去。每个基本块最后一条指令一般是跳转指令(跳转到其它基本块上去),函数内最后一个基本块的最后条指令是函数返回指令。
- Instruction(指令)是LLVM IR中的最小可执行单位,每一条指令都单占一行
LLVM IR头部是一些Target Information,
; ModuleID = 'add.c'
source_filename = "add.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
每一行分别是,
ModuleID
:编译器用于区分不同模块的IDsource_filename
:源文件名target datalayout
:目标机器架构数据布局target triple
:用于描述目标机器信息的一个元组,一般形式是<architecture>-<vendor>-<system>[-extra-info]
需要关注的是target datalayout
,它由-
分隔的一列规格组成
e
:内存存储模式为小端模式m:o
:目标文件的格式是Mach格式i64:64
:64位整数的对齐方式是64位,即8字节对齐f80:128
:80位扩展精度浮点数的对齐方式是128位,即16字节对齐n8:16:32:64
:整型数据有8位的、16位的、32位的和64位的S128
:128位栈自然对齐