LLVM
设计的核心是它的IR
.
在把LLVMIR
翻译特定汇编语言
时,LLVM
首先将程序变换
为(DAG)
有向无环图,以更易选指(SelectionDAG)
容易,然后变换回三地址指令
,来调度指令(MachineFunction)
.
为了看清驱动
编译程序时,调用的后续工具
,用-###
命令行参数:
cpp
$ clang -### hello.c -o hello
部分工具:
1,opt
:IR
层次优化器.输入
必须是LLVM
位码(编码的LLVMIR
)文件,输出
文件也是该类型
.
2,llc
:通过具体后端把LLVM位码
变换为目标机器汇编语言
文件或目标文件
.可通过参数选择优化级别
,开启调试选项
,开关
目标指定优化.
3,llvm-mc
:为多种(如ELF,MachO,PE
)目标格式汇编指令
生成目标文件
.也可反汇编
同样目标文件,输出等价的汇编信息
和内部LLVM
机器指令数据结构.
4,lli
:为LLVMIR
实现了解释器
和JIT
编译器.
5,llvm-link
:连接若干LLVM位码
,产生包含所有输入的单个LLVM位码
.
6,llvm-as
:把人类可读的LLVM
汇编转换为LLVM位码
.
7,llvm-dis
:解码LLVM位码
,生成LLVM
汇编.
考虑简单的来自多个源文件
多个函数组成的C程序.第一个源文件
是main.c
,代码如下:
cpp
#include <stdio.h>
int sum(int x, int y);
int main() {
int r = sum(3, 4);
printf("r = %d\n", r);
return 0;
}
第二个源文件是sum.c
,代码如下:
cpp
int sum(int x, int y) {
return x + y;
}
可用下面命令
编译它:
cpp
$ clang main.c sum.c -o sum
然而,用独立工具
也可实现相同结果.首先,用不同
参数调用clang
,让它为C源文件
生成LLVM位码
,然后不继续编译
,就此停止:
cpp
$ clang -emit-llvm -c main.c -o main.bc
$ clang -emit-llvm -c sum.c -o sum.bc
-emit-llvm
参数,让clang
根据输入参数是-c
还是-S
,生成LLVM位码
或LLVM
汇编文件.
示例中,-emit-llvm
参数和-c
,让clang
生成LLVM位码
格式的目标文件
.
用-flto-c
组合参数同样.如果想生成可读
的LLVM
汇编,用下面这对命令
代替:
cpp
$ clang -emit-llvm -S -c main.c -o main.ll
$ clang -emit-llvm -S -c sum.c -o sum.ll
注意,不用-emit-llvm
或-flto
参数时,-c
参数用目标机器语言
生成目标文件
,而-S
参数生成目标汇编语言文件
.行为与GCC
一样.
这里.bc
和.ll
分别是LLVM位码
和汇编
文件的扩展名
.
为每个LLVM位码
生成目标
指定的目标文件
,用系统链接器
链接它们以生成可执行文件
:
cpp
$ llc -filetype=obj main.bc -o main.o
$ llc -filetype=obj sum.bc -o sum.o
$ clang main.o sum.o -o sum
首先,链接这两个LLVM位码
为一个最终的LLVM位码
.然后,为该最终的位码
生成目标指定目标文件
,调用系统链接器
生成可执行
文件:
cpp
$ llvm-link main.bc sum.bc -o sum.linked.bc
$ llc -filetype=obj sum.linked.bc -o sum.linked.o
$ clang sum.linked.o -o sum
-filetype=obj
参数指定输出目标文件
,而不是输出汇编文件
.
cpp
流程:C==>BC=>llc为.obj文件,再系统链接
流程:C==>BC=>llvm-link为.单个BC文件,再llc,再系统链接
调用(llc)
后端前,链接IR
文件,用opt
工具链接时优化
llc
工具也可生成汇编输出
,利用llvm-mc
进一步汇编
.
LLVM
基础库
1,libLLVMCore
:包含所有LLVMIR
相关的逻辑:IR
构造(数据布局,指令,基本块,函数
)和IR
验证.还提供了趟
管理器.
2,libLLVMAnalysis
:包含若干IR
分析趟
,如别名分析,依赖分析,常量合并,循环信息,内存依赖分析,指令简化
等.
3,libLLVMCodeGen
:实现目标无关
生成代码和机器级(低层版本LLVMIR
)的分析和转换
.
4,libLLVMTarget
:通过抽象通用目标
,访问
目标机器信息.libLLVMCodeGen
实现了通用后端算法
,目标相关
逻辑留给后面的库
,而高层抽象
提供交流通道
.
5,libLLVMX86CodeGen
:包含x86
目标相关的生成代码信息
,转换和分析趟
,由它们构成了x86
后端.注意,每个机器目标
都有自己不同的库,如分别实现了ARM
和MIPS
后端的LLVMARMCodeGen
和LLVMMipsCodeGen
.
6,libLLVMSupport
:包含实用工具.如错误处理,整数和浮点数处理,命令行解析,调试,文件支持,串操作
等,这些是该库
实现算法示例,LLVM
的各个组件
都用它们.
7,libclangDriver
:包含一套C++
类.编译器驱动
用它们理解类似GCC
的命令行参数
,以编译任务,为外部工具
组织参数,以完成编译.根据目标平台
,可用不同编译策略
.
8,libclangAnalysis
:这是一系列Clang
提供的包括构造CFG
和调用图,可达代码,安全格式化串
等的前端架构分析
.
libclang
libclang
:(对比C++
的LLVM
代码)它实现一套C接口
以暴露Clang
的前端功能:诊断报告
,遍历AST
,补全
代码,光标和源码
间映射.
C
接口相当简单,用C设计接口
是为了更稳定
,让其它语言(如Python
),可很容易地使用Clang
的功能.
它仅覆盖内部LLVM
组件所用的C++
接口的子集.
C++实践
在基类
中实现通用生成代码算法
,继承和多态
方法抽象
不同后端
共同任务.这样,每个具体后端
专注实现它的特性
,编写少量
必需函数以覆盖父类
通用操作.
如libLLVMCodeGen
包含常见
算法,libLLVMTarget
包含具体机器
抽象接口.下面的代码片演示了MIPS
目标机器描述类
是如何按LLVMTargetMachine
类的子类声明
的.此代码是LLVMMipsCodeGen
库的一部分:
cpp
class MipsTargetMachine : public LLVMTargetMachine {
MipsSubtarget Subtarget;
const DataLayout DL;
...
另一例,目标无关
的(所有后端公共的)分配寄存器器
要知道哪些寄存器
是保留
而不能用于分配的.
此信息依赖具体目标
,不能通用的父类
中确定.这可调用MachineRegisterInfo::getReservedRegs()
函数来确定,每个目标
必须覆盖
它.
下面演示了SPARC
目标如何覆盖
:
cpp
BitVector SparcRegisterInfo::getReservedRegs(...) const {
BitVector Reserved(getNumRegs());
Reserved.set(SP::G1);
Reserved.set(SP::G2);
...
此代码中,通过位向量
,SPARC
后端说明了哪些寄存器
不能用于通用分配寄存器
.
调试编译器细节
尽量使用libLLVMSupport
实现的断言
.
查看ARM
后端趟
代码,它修改常量池
布局,重新赋值
.
ARM
程序常用该策略
加载大型常量
,因为单个大型的池
距离指令
太远,以致指令
无法访问它,来解决受限的PC
相对寻址机制.如下:
cpp
//lib/Target/ARM/ARMConstantIsland趟.cpp
const DataLayout &TD = *MF->getTarget().getDataLayout();
for (unsigned i = 0, e = CPs.size(); i != e; ++i) {
unsigned Size = TD.getTypeAllocSize(CPs[i].getType());
assert(Size >= 4 && "Too small constant pool entry");
unsigned Align = CPs[i].getAlignment();
assert(isPowerOf2_32(Align) && "Invalid alignment");
//验证所有常量池项是否都是`对齐的倍数`.如果不是,则要`填充`,以便`指令`保持一致.
assert((Size % Align) == 0 && "CP Entry not multiple of 4 bytes!");
此代码遍历ARM
常量池
,期望它的每个字段
遵守约束.注意如何用assert
来控制
数据语义.
插件式趟
接口
趟
是转换分析或优化
.通过LLVMAPI
可在编译生命期
的不同阶段
轻松注册
任意趟
.
趟
管理器用来注册趟
,调度趟
,声明趟
之间的依赖关系.因此,不同编译阶段
都可取得PassManager
类的实例
.
如,目标可自由地在如分配寄存器前后,或输出汇编前
等生成代码
的若干位置
,应用定制
优化.如:
cpp
//lib/Target/X86/X86TargetMachine.cpp
bool X86PassConfig::addPreEmitPass() {
...
if (getOptLevel() != CodeGenOpt::None && getX86Subtarget().hasSSE2()) {
addPass(createExecutionDependencyFixPass(&X86::VR128RegClass));
...
}
if (getOptLevel() != CodeGenOpt::None &&
getX86Subtarget().padShortFunctions()) {
addPass(createX86PadShortFunctions());
...
}
...
注意后端
如何根据具体目标信息
,决定是否添加某个趟
.添加第一个趟
前,X86
目标检查
是否支持SSE2
多媒体扩展.
对第二个趟
,检查是否要求特殊填充
.
编写第一个LLVM
项目
创建一个程序,它读入位码
文件,打印程序
定义的函数名
,函数基本块数量
.
编写Makefile
cpp
//注意制表符.
LLVM_CONFIG =llvm-config
ifndef VERBOSE
QUIET:=@
endif
SRC_DIR =$(PWD)
LDFLAGS+=$(shell $(LLVM_CONFIG) --ldflags)
COMMON_FLAGS=-Wall -Wextra
CXXFLAGS+=$(COMMON_FLAGS) $(shell $(LLVM_CONFIG) --cxxflags)
CPPFLAGS+=$(shell $(LLVM_CONFIG) --cppflags) -I$(SRC_DIR)
第一部分
定义了若干编译选项
的Makefile
变量.第一个
变量决定llvm-config
程序位置.
llvm-config
用来打印构建要链接LLVM
库外部项目的有用信息
.
如,定义C++
编译器的一系列选项
时,注意请求Make
来运行llvm-config-cxxflagsshell
命令,让它打印编译LLVM
项目的一系列选项
.
这样,编译项目源码
和LLVM
源码兼容.最后把变量定义
的一系列选项
传递给编译器预处理器
.
cpp
HELLO=helloworld
HELLO_OBJECTS=hello.o
default: $(HELLO)
%.o : $(SRC_DIR)/%.cpp
@echo Compiling $*.cpp
$(QUIET)$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $<
$(HELLO) : $(HELLO_OBJECTS)
@echo Linking $@
$(QUIET)$(CXX) -o $@ $(CXXFLAGS) $(LDFLAGS) $^ `$(LLVM_CONFIG) --libs bitreader core support`
这里:
cpp
llvm-config --libs bitreader core support
-libs
选项要求llvm-config
提供链接器选项清单
,来链接期望的LLVM
库.这里,要求链接libLLVMBitReader,libLLVMCore,libLLVMSupport
.
代码
完整给出趟
代码.因为创建在LLVM趟
基础设施上,代码
相对较短.
cpp
#include "llvm/Bitcode/ReaderWriter.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/raw_os_ostream.h"
#include "llvm/Support/system_error.h"
#include <iostream>
using namespace llvm;
static cl::opt<std::string> FileName(cl::Positional, cl::desc("位码 file"), cl::Required);
int main(int argc, char** argv) {
cl::ParseCommandLineOptions(argc, argv, "LLVM hello world\n");
LLVMContext context;
std::string error;
OwningPtr<MemoryBuffer> mb;
MemoryBuffer::getFile(FileName, mb);
Module *m = ParseBitcodeFile(mb.get(), context, &error);
if (m==0) {
std::cerr << "读位码错误: " << error << std::end;
return -1;
}
raw_os_ostream O(std::cout);
for (Module::const_iterator i = m->getFunctionList().begin(),
e = m->getFunctionList().end(); i != e; ++i) {
if (!i->isDeclaration()) {
O << i->getName() << " has " << i->size() << " basic block(s).\n";
}
}
return 0;
}
程序利用cl
(cl
代表命令行
)名字空间的llvm
工具来实现命令行接口
.调用ParseCommandLineOptions
函数,并声明一个cl::opt<std::string>
类型的全局变量
,以此说明程序
接收带位码
文件名类型的单个串
参数.
然后,实例化一个保存LLVM
编译的从属数据
的LLVMContext
对象,来让LLVM
线安.MemoryBuffer
类对内存块
定义了个只读
接口.
ParseBitcodeFile
函数用它读取
输入文件内容,并解析文件中的LLVMIR
.错误检查
完成后,遍历
文件中模块
的所有函数
.
LLVM
模块类似翻译单元
,它包含编码
一切内容的位码
文件,作为LLVM
的顶端实体,下面是若干函数
,然后基本块
,最后是指令
.
如果函数
只是个声明,则忽略它.找到函数定义
时,打印名字,及基本块数
.
编译
后,用-help
参数运行,看一看已为你的程序
准备好的LLVM
命令行功能.然后,找个想变换为LLVMIR
的C或C++
文件,变换
,并再用程序
分析.
cpp
$ clang -c -emit-llvm mysource.c -o mysource.bc
$ helloworld mysource.bc