A Quick Primer about LLVM

这一章节会提供一些非常 basic 的 LLVM 相关的基础概念,也是学习 LLVM transformation passes 的前提。 如果有兴趣的话,还可以顺着 Writing an LLVM Pass 学习一下怎么安装 LLVM 并写一个非常简单的 pass。 本章的知识主要来源于我之前在 Dr. Guanpeng Li 的 CS:4980@UIowa 写的一个 Tutorial [1],同时也感谢这个文档 [2] 的帮助,写得太棒了。 当然,如果你已经是 LLVM 大佬的话可以直接跳过这一章了。

LLVM Compiler Infrastructure

Low-Level Virtual Machine (LLVM) 是一个 compiler infrastructure,它 targets 成为一个全能的 compiler(事实上我感觉它也做到了)。 LLVM 的架构如下图所示。 简而言之,LLVM 的核心设计在于 language/platform agnostic 的 common optimizer。 传统的 compiler 每支持一种 programming language 和一个 platform 都得把整个 frontend + backend 给重写一遍。 而 common optimizer 的存在使得 LLVM 的编译过程异常灵活:支持一个新的 programming language 只需要写其对应的 frontend;支持一个新的 platform 只需要写其对应的 backend。 除此之外,common optimizer 还可以进行各种各样有趣的 program analysis 和 transformation (没错,也就是本文档专注的 pass),可以帮助用户更好的理解和优化代码。 LLVM 自从2004年被 Chris Lattner 提出之后已经迭代了20年,现在已经是一个很大的 project 了。 我们很多耳熟能详的工具都是 LLVM 的某个 subproject:比如著名的 C/C++ compiler Clang/Clang++,symbolic execution tool KLEE,等等。

LLVM compiler infrastructure

LLVM compiler infrastructure

LLVM IR and Pass

Intermediate Representation (IR) 是 LLVM 使用在 common optimizer(以后都简称 optimizer了)层级上的一种中间表示(也就是一种特定的 programming language)。 LLVM IR 表现为 a set of instructions;虽然它不是真正意义的的 machine-level assembly code,但是其实也差不太多。 与此同时,LLVM IR 不仅保留了一些非常底层的特征(比如 branching,basic blocks),也保留了一些顶层编程语言有的特征(比如 variable name,function),从而确保了其可读性。 所以,LLVM IR 现在已经成为了很多研究领域非常 dominant 的工具,比如 program analysis,code optimization,和 soft error resilience,等等。

LLVM IR 有两种格式,分别是 human-readable .ll 和 binary code .bc。 这两种格式之间是可以相互转换的,我们这里的解释主要 focus 在 .ll 上。 LLVM IR 主要有三层 components,分别是 function,basic block (BB),和 instruction。

  • Function: 一个程序的 LLVM IR 可能由多个 function 组成。在大多数情况下,LLVM IR 层级的 function 都是和 source code 上 function 的一一对应的。

  • Basic block (BB): 一个 function 由一个或者多个 basic block 组成。Basic block 同样也是记录 LLVM IR 中 branching 信息和 control-flow 关系的最小单位。

  • Instruction: 一个 basic block 由一个或多个 instruction 组成。每个 instruction 都有自己对应的 type 和 variables。

下面给个例子来帮助认识一下 LLVM IR。下面有两段代码,分别是一个 C 程序的 source code 和其对应的 LLVM IR。

int variable = 21;

int main()
{
   variable = variable * 2;
   return variable;
}

可以看到,这个程序的 LLVM IR 和 C code 一样都只有一个 main function;这个 function 同样只有一个 basic block;而这个 basic block 里有四个 instructions,他们的类型分别是 loadmulstore, 和 ret

@variable = global i32 21          ; define global variable, in LLVM IR global variable starts with '@'

define i32 @main() {
   %1 = load i32, i32* @variable   ; load the global variable, in LLVM IR local variable starts with '%'
   %2 = mul i32 %1, 2
   store i32 %2, i32* @variable    ; store instruction to write to global variable
   ret i32 %2
}

我们再结合 LLVM compiler infrastructure 理解一下 LLVM IR 是怎么使用的。 如下图所示,给定不同的 high-level programming language,frontend 首先将其编译成 LLVM IR。 LLVM IR 经过 optimizer 的处理之后继续以 LLVM IR 的形式作为 backend 的输入,最后编译成适用于不同 platform 的 machine-level assembly code。 可以看到,在 optimizer 上,input 和 output 都是 LLVM IR,而在这个过程中进行的 program analysis/transformation 就是 LLVM pass。 LLVM 官方提供了很多现成的 transformation/utility/analysis pass,当然用户也可以根据自己的需求写自己的 pass(有兴趣可以读一下 Writing an LLVM Pass)。 LLVM pass 是一个非常强大的基于 LLVM IR 的工具。理论上只要你可以把 pass 写的足够复杂,你甚至可以把一个给定的程序完全 transform 成另一个毫不相关的程序。

LLVM compiler infrastructure and IR

LLVM compiler infrastructure and IR

Useful LLVM Commands

clang: LLVM C compiler

$ clang hello-world.c -o hello-world                             # Compile C code to executable binary
$ clang -S -emit-llvm hello-world.c -o hello-world.ll            # Compile C code to readable IR

clang++: LLVM C++ compiler

$ clang++ hello-world.cpp -o hello-world                         # Compile C++ code to executable binary
$ clang++ -S -emit-llvm hello-world.cpp -o hello-world.ll        # Compile C++ code to readable IR

llvm-as: Assembler

$ llvm-as hello-world.ll -o hello-world.bc                       # Compile readable IR to bitcode format

llvm-dis: Disassembler

$ llvm-dis hello-world.bc -o hello-world.ll                      # Compile bitcode format to readable IR

llvm-link: Linker

$ llvm-link -S hello.ll world.ll -o hello-world.ll               # Link two IRs into a unified one

llc: Static compiler

$ llc hello-world.ll -o hello-world.s                            # Compile IR into assembly code for a specified architecture

lli: Directly execute LLVM IR

$ lli pathfinder.ll 1000 10                                      # Directly execute Rodinia-pathfinder IR with input "1000 10" using a just-in-time compiler

opt: Optimizer,也是 load LLVM pass (官方提供的和自己手写的都可以)的最核心工具。我们本文档提到的所有 transformation pass 都可以用这个 command 来实现。 有兴趣实现下面代码样例中提到的 CallCount LLVM pass的话可以移步这里: Writing an LLVM Pass

$ opt -load ./CallCount.so pathfinder.ll -CallCount -o output.ll # Load LLVM Pass for code transformation and optimization.
$                                                                # CallCount.so is the LLVM Pass we want to load.
$                                                                # -CallCount is the unique flag of this Pass registered in current LLVM project.
$                                                                # The output.ll is bitcode format, which can be disassembler to readable IR via llvm-dis.

Optional

如果你想顺着这个文档的思路尝试一下写一个简单的 LLVM pass 的话,可以看一看下面的文档。

References