0x1. LLVM 架构简介
经典的三段式设计
LLVM,GCC, JIT(Java, Python) 等编译器都遵循经典的三段式设计
-
前端 (Frontend) - 词法分析,语法分析, 生成抽象语法树,生成中间语言 (例如 java 的字节码,llvm 的 IR,GCC 的 GIMPLE Tuples)
-
优化器 (Optimizer) - 分析中间语言,避免多余的计算,提高性能;
-
后端 (Backend) - 根据中间语言,生成对应的 CPU 架构指令 例如 X86,ARM;
通过这种设计,增加新的语言,只需要实现新的 Frontend,Optimizer 和 Backend 可以重用;同理新增新的 CPU 架构时,也只需要实现新的 Backend。
LLVM 的优势
LLVM,GCC,JIT 都采用三段式设计,LLVM 的优势在哪里 ?
GCC,JIT 存在的问题:
-
GCC 的问题
- 古老,模块化不够 (GCC 是个整体,无法独立使用某块功能)。
-
JIT 的问题
- 强制 JIT 编译,垃圾回收以及使用非常特殊的对象模型;在编译与该模型不完全匹配的语言(例如C)时,性能欠佳。(我也不懂 😊)
LLVM 的优势:
-
良好的模块化
-
严格定义语义的中间语言 IR
0x2: 编译 LLVM
-
前置要求
cmake, python, 可以通过 homebrew 来安装
-
检出 LLVM 工程
git clone --depth=1 https://github.com/llvm/llvm-project.git
-
编译 LLVM 和 Clang
cd llvm-project mkdir build_with_ninja cd build_with_ninja cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;compiler-rt" -G Ninja ../llvm ninja
- -DCMAKE_BUILD_TYPE=Release
- 默认 Debug,设置为 Release 可以减少硬盘空间的占用
- -DLLVM_ENABLE_ASSERTIONS=ON
- Debug 下默认 YES,其他 NO
- -DCMAKE_INSTALL_PREFIX=directory
- 不指定,默认安装在 build_with_ninja 的 bin 目录下
- -G 参数说明:
- Ninja - 推荐,编译速度更快
- 安装方式- brew install ninja
- Unix Makefiles - 通过 make 来编译
- Visual Studio,Xcode - 方便调试
- Ninja - 推荐,编译速度更快
- -DCMAKE_BUILD_TYPE=Release
-
测试是否成功
cd bin ./clang --help
0x3. Clang 命令
-
首先创建一个名为 "hello.c" 的 C 文件
#include <stdio.h> int main() { printf("hello world\n"); return 0; }
-
预编译
clang hello.c -E
-
词法分析,导出 token
clang -fsyntax-only -Xclang -dump-tokens hello.c
-
语法分析, 导出抽象语法树
clang -fsyntax-only -Xclang -ast-dump hello.c
-
编译为可执行文件
clang hello.c -o hello
-
编译为 bitcode
clang -o3 -emit-llvm hello.c -c -o hello.bc
-emit-llvm 搭配 -S 或者 -c 选项可以生成 LLVM .ll 或者 .bc 文件,.ll, .bc 都是 LLVM IR 格式,它们的区别是 .ll 是可读的,而 .bc 不可读。
-
运行程序, 输出 hello world
-
./hello
运行 bitcode
lli hello.bc
-
使用 llvm-dis 查看 .bc 文件
llvm-dis < hello.bc | less
-
将 LLVM 中间文件 (.ll 或者 .bc) 编译为汇编文件
llc hello.ll -o hello.s
或者
llc hello.bc -o hello.s
也可以使用 clang
clang hello.bc -S hello.s
-
生成可执行文件
clang hello.s -o hello
-
常见错误
- 找不到头文件
fatal error: 'stdio.h' file not found #include <stdio.h> ^~~~~~~~~ 1 error generated.
通过 -I 指定头文件路径
clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/include ...
0x4. 上手 Pass
pass 是什么?
一系列优化 和 转换 LLVM IR 的 C++ 代码, 主要流程如下:
几个概念:
Module, Function, BasicBlock, Instruction, Value
-
Module: 包含 Function, 全局变量等
可以遍历 Module 得到 Function
for (Module::iterator iter = M.begin(); iter != M.end(); iter++) { Function *F = &(*iter);
-
Function:包含若干 BasicBlock
-
BasicBlock:包含若干 Instruction
-
Instruction: 指令,包含操作,Value
add 是操作,其他为 Value;
%1 = add i32 %a, %b
-
Value:大部分对象都可以看成 Value,包括常量,参数,指令,函数
在 LLVM 源码目录外开发 Pass
为方便表述,简称 “外部 pass”,外部 pass 比较灵活,不用修改 LLVM 源码配置;
尝试制作一个打印函数名的简单 Pass,步骤如下:
-
新建 outpasses
# llvm-project 同级目录 cd ../.. mkdir outpasses
-
创建如下目录
outpasses/ | CMakeLists.txt PrintFunctions/ | CMakeLists.txt PrintFunctions.cpp ...
-
outpasses / CMakeLists.txt 内容如下:
配置 LLVM_DIR - LLVM_DIR 为 LLVM 编译时的安装目录
cmake_minimum_required(VERSION 3.4) set(ENV{LLVM_DIR} ~/llvm/llvm-project/build_with_ninja/lib/cmake/llvm) find_package(LLVM REQUIRED CONFIG) add_definitions(${LLVM_DEFINITIONS}) include_directories(${LLVM_INCLUDE_DIRS}) link_directories(${LLVM_LIBRARY_DIRS}) # add c++ 14 to solve "error: unknown type name 'constexpr'" add_compile_options(-std=c++14) add_subdirectory(PrintFunctions) # Use your pass name here.
-
PrintFunctions/CMakeLists.txt 内容如下:
add_library(PrintFunctions MODULE # List your source files here. printFunctions.cpp ) # LLVM is (typically) built with no C++ RTTI. We need to match that; # otherwise, we'll get linker errors about missing RTTI data. set_target_properties(PrintFunctions PROPERTIES COMPILE_FLAGS "-fno-rtti" ) # Get proper shared-library behavior (where symbols are not necessarily # resolved when the shared library is linked) on OS X. if(APPLE) set_target_properties(PrintFunctions PROPERTIES LINK_FLAGS "-undefined dynamic_lookup" ) endif(APPLE)
-
PrintFunctions/printFunctions.cpp 内容如下:
#include "llvm/Pass.h" #include "llvm/IR/Function.h" #include "llvm/Support/raw_ostream.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/Transforms/IPO/PassManagerBuilder.h" using namespace llvm; namespace { struct Hello : public FunctionPass { static char ID; Hello() : FunctionPass(ID) {} virtual bool runOnFunction(Function &F) { errs() << "I saw a function called " << F.getName() << "!\n"; return false; } }; } // Automatically enable the pass. char Hello::ID = 0; static RegisterPass<Hello> X("hello", "Hello World Pass", false /* Only looks at CFG */, false /* Analysis Pass */); static RegisterStandardPasses Y( PassManagerBuilder::EP_EarlyAsPossible, [](const PassManagerBuilder &Builder, legacy::PassManagerBase &PM) { PM.add(new Hello()); });
-
编译 pass 得到 libPrintFunctions.so
cd outpasses cmake . make
libPrintFunctions.so 在 PrintFunctions 文件夹中
-
通过 clang 加载 pass
cd llvm-project ./build_with_ninja/bin/clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/include -Xclang -load -Xclang ../outpasses/PrintFunctions/libPrintFunctions.so ../llvmtest/test.c
test.c 内容如下:
#include <stdio.h> int main() { int a = 0; if ( a = 1) { printf("123"); } return 0; }
输出:
I saw a function called main!
-
也可以通过 opt 来加载 pass
生成 bitcode
./build_with_ninja/bin/clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/include -o3 -emit-llvm test.c -c -o test.bc
opt -hello 开启 pass
./build_with_ninja/bin/opt -load ../outpasses/PrintFunctions/libPrintFunctions.so -hello < ../llvmtest/test.bc
Pass 和 PassManager 的关系
PassManager 管理 Pass,解决多个 Pass 依赖,传值等问题,例如 PassA 需要等待 PassB 执行完成后才执行;
Pass 需要注册到 PassManager 中:
-
注册到 opt 中, hello 命令行可选参数,“Hello World Pass” 帮助说明
# 注册到 opt 中, static RegisterPass<Hello> X("hello", "Hello World Pass", false /* Only looks at CFG */, false /* Analysis Pass */);
通过 opt -hello 来使用 HelloPass
opt -load lib/LLVMHello.so -hello < hello.bc > /dev/null
-
注册到标准编译流程中,默认会执行 HelloPass,例如通过 clang 调用
static llvm::RegisterStandardPasses Y( llvm::PassManagerBuilder::EP_EarlyAsPossible, [](const llvm::PassManagerBuilder &Builder, llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });
移植到 LLVM 源码目录
-
拷贝 PrintFunctions 目录到 LLVM 源码目录的 lib/Transform 目录下
-
在 lib/Transform/CMakeLists.txt 中新增 add_subdirectory(PrintFunctions)
-
重新编译,可以得到 libPrintFunctions.so
通过 Xcode 调试 Pass
0x5. 参考
- 了解 LLVM 是什么,基本架构模块,IR 是什么,pass 的作用
- 阅读 LLVM Overview 了解 LLVM 的模块划分以及各模块基本功能。
- 阅读 The Architecture of Open Source Applications 了解 LLVM 的架构
- 编译
- 阅读 get_started 了解如何编译 LLVM
- 阅读 llvm cmake 参数 了解编译时 cmake 参数的含义
- 阅读 CMake 简介 了解 CMake 语法
- 上手 pass,IR
- 根据 writing an LLVM pass 上手尝试,了解 pass 的使用流程
- ProgrammersManual 阅读开发手册了解编写 pass 可能涉及到的 API
- IR 手册 略读 IR 手册,了解 IR 的基本特征
- 其他参考资料
- 日常查阅
- llvm cmake 参数
- IR 手册
- LLVM command guide llvm-as, llvm-dis, opt 等命令的使用说明