LLVM IR指令简介及上手教程

LLVM中间表示(IR)是连接前端和后端的中枢,让LLVM能够解析多种源语言,为多种目标生成代码。前端产生IR,而后端接收IR。IR也是大部分LLVM目标无关的优化发生的地方。

LLVM IR指令——Intermediate Representation

在编译器理论与实践中,IR是非常重要的一环。IR的全称叫做Intermediate Representation,翻译过来叫“中间表示”。对于一个编译器来说,从上层抽象的高级语言到底层的汇编语言,要经历很多个环节(pass),经历不同的表现形式。而编译优化技术有很多种,每种技术作用的编译环节不同。但是IR是一个明显的分水岭。IR以上的编译优化,不需要关心底层硬件的细节,比如硬件的指令集、寄存器文件大小等。IR以下的编译优化,需要和硬件打交道。LLVM最为著名是它的IR的设计。得益于巧妙地IR设计,LLVM向上可以支持不同的语言,向下可以支持不同的硬件,而且不同的语言可以复用IR层的优化算法。
LLVM 框架图
上图展示了LLVM的一个框架图。LLVM把整个编译过程分为三步:

· 前端,把高级语言转换为IR。

· 中端,在IR层做优化。

· 后端,把IR转化为对应的硬件平台的汇编语言。

因此LLVM的扩展性很好。比如你要实现一个名为toyc的语言、希望运行在ARM平台上,你只需要实现一个toyc->LLVM IR的前端,其他部分调LLVM的模块就可以了。或者你要搞一个新的硬件平台,那么只需要搞定LLVM IR->新硬件这一阶段,然后该硬件就可以支持很多种现存的语言。因此,IR是LLVM最有竞争力的地方,同时也是学习使用LLVM Codegen的最核心的地方。

LLVM IR基本知识

LLVM的IR格式非常像汇编,对于学习过汇编语言的同学来说,学会使用LLVM IR进行编程非常容易。对于没学过汇编语言的同学,也不用担心,汇编其实并不难。汇编难的不是学会,而是工程实现。

因为汇编语言的开发难度,会随着工程复杂度的提升呈指数级上升。接下来我们需要了解IR中最重要的三部分,指令格式、Basic Block & CFG,还有SSA。完整的LLVM IR信息请参考LLVM 官方文档

指令格式
LLVM IR提供了一种类似于汇编语言的三地址码式的指令格式。下面的代码片段是一个非常简单的用LLVM IR实现的函数,该函数的输入是5个i32类型(int32)的整数,函数的功能是计算这5个数的和并返回。

define i32 @ir_add(i32, i32, i32, i32, i32){
%6 = add i32 %0, %1
%7 = add i32 %6, %2
%8 = add i32 %7, %3
%9 = add i32 %8, %4
ret i32 %9
}Basic Block & CFG

LLVM IR是支持一些基本的数据类型的,比如i8、i32、浮点数等。LLVM IR中的变量的命名是以 "%"开头,默认%0是函数的第一个参数、%1是第二个参数,依次类推。机器生成的变量一般是以数字进行命名,如果是手写的话,可以根据自己的喜好选择合适的命名方法。

LLVM IR的指令格式包括操作符、类型、输入、返回值。例如 “%6 = add i32 %0, %1"的操作符号是"add”、类型是"i32"、输入是"%0"和“%1”、返回值是"%6"。总的来说,IR支持一些基本的指令,然后编译器通过这些基本指令的来完成一些复杂的运算。

例如,我们在C中写一个形如“A * B + C”的表达式在LLVM IR中是通过一条乘法和一条加法指令来完成的,另外可能也包括一些类型转换指令。

了解了IR的指令格式以后,接下来我们需要了解两个概念:Basic Block(基本块,简称BB)Control Flow Graph(控制流图,CFG)
下图(左)展示了一个简单的C语言函数,下图(中)是使用clang编译出来的对应的LLVM IR,下图(右)是使用graphviz画出来的CFG。结合这张图,我们解释下Basic Block和CFG的概念。
LLVM IR - CFG理解
在我们平时接触到的高级语言中,每种语言都会有很多分支跳转语句,比如C语言中有for, while, if等关键字,这些关键字都代表着分支跳转。开发者通过分支跳转来实现不同的逻辑运算。汇编语言通常通过有条件跳转和无条件跳转两种跳转指令来实现逻辑运算,LLVM IR同理。

比如在LLVM IR中"br label %7"意味着无论如何都跳转到名为%7的label那里,这是一条无条件跳转指令。"br i1 %10, label %11, label %22"是有条件跳转,意味着这如果%10是true则跳转到名为%11的label,否则跳转到名为%22的label。

在了解了跳转指令这个概念后,我们介绍Basic Block的概念。一个Basic Block是指一段串行执行的指令流,除了最后一句之外不会有跳转指令,Basic Block入口的第一条指令叫做“Leading instruction”。除了第一个Basic Block之外,每个Basic Block都会有一个名字(label)。第一个Basic Block也可以有,只是有时候没必要。

例如在这段代码当中一共有5个Basic Block。Basic Block的概念,解决了控制逻辑的问题。通过Basic Block, 我们可以把代码划分成不同的代码块,在编译优化中,有的优化是针对单个Basic Block的,有些是针对多个Basic Block的。

CFG(Control Flow Graph,控制流图)其实就是由Basic Block以及Basic Block之间的跳转关系组成的一个图。例如上图所示的代码,一共有5个Basic Block,箭头列出了Basic Block之间的跳转关系,共同组成了一个CFG。如果一个Basic Block只有一个箭头指向别的Block,那么这个跳转就是无条件跳转,否则是有条件跳转。

CFG是编译理论中一个比较简单而且很基础的概念,CFG更进一步是DFG(Data Flow Graph,数据流图),很多进阶的编译优化算法都是基于DFG的。对于使用LLVM进行Codegen开发的同学,理解CFG的概念即可。

SSA
SSA的全称是Static Single Assignment(静态单赋值),这是编译技术中非常基础的一个理念。SSA是学习LLVM IR必须熟悉的概念,同时也是最难理解的一个概念。细心的读者在观察上面列出的IR代码时会发现,每个“变量”只会被赋值一次,这就是SSA的核心思想。因为从编译器的角度来看,编译器不关心“变量”,编译器是以“数据”为中心进行设计的。每个“变量”的每次写入,都生成了一个新的数据版本,编译器的优化是围绕数据版本展开的。接下来我们用如下的C语言代码来解释这一思想。
SSA结构解释说明
上图(左)展示了一段简单的C代码,上图(右)是这段代码的SSA版本,也就是“编译器眼中的代码”。在C语言中,我们知道数据都是用变量来存储的,因此数据操作的核心是变量,开发者需要关心变量的生存时间、何时被赋值、何时被使用。但是编译器只关心数据的流向,因此每次赋值操作都会生成一个新的左值。

例如左边代码只有一个a, 但是在右边的代码有4个变量,因为a里面的数据一共有4个版本。除了每次赋值操作会生成一个新的变量,最后的一个phi节点会生成一个新的变量。在SSA中,每个变量都代表数据的一个版本。

也就是说,高级语言以变量为核心,而SSA格式以数据为核心。SSA中每次赋值操作都会生成一个版本的数据,因此在写IR的时候,时刻牢记IR的变量和高层语言不同,一个IR的变量代表数据的一个版本。Phi节点是SSA中的一个重要概念。在这个例子当中,a_4的取值取决于之前执行了哪个分支,如果执行了第一个分支,那么a_4 = a_1, 依次类推。

Phi节点通过判断这段代码是从哪个Basic Block跳转而来,选择合适的数据版本。LLVM IR自然也是需要开发者写Phi节点的,在循环、条件分支跳转的地方,往往需要手写很多phi节点,这是写LLVM IR时逻辑上比较难处理的地方。

理解LLVM IR的目标依赖

LLVM IR被设计为尽可能地与目标无关,但是它仍然表现出某些目标特定的属性。多数人批评C/C++语言内在的目标依赖的本性。为了理解这个观点,考虑当你在Linux系统上使用标准C头文件时,例如,你的程序隐式地导入一些头文件,从Linux头文件目录bits。这个目录包含目标相关的头文件,其中的一些宏定义约束某些实体使用一个特别的类型,它符合此内核机器的syscalls的期望。随后,举例来说,当前端解析你的代码的时候,也需要为int使用不同的长度,取决于打算在什么目标机器上运行此代码。

因此,程序库头文件和C类型已然都是目标相关的,这使得生成目标无关的IR充满挑战,这种IR可以随后被翻译到不同的目标。如果你只考虑目标相关的C标准库头文件,解析一个给定的编译单元得到的AST已然是目标相关的,甚至在翻译为LLVM IR之前。而且,前端生成的IR代码用了类型长度、调用惯例、特殊库调用,这些都得匹配每个目标的ABI所定义的内容。还有,LLVM IR是相当灵活多面的,能够以一种抽象的方法处理各种独特的目标。

使用基础工具转化LLVM IR格式

考虑下面的sum.c源代码:

int sum(int a, int b) {
  return a+b;
}

为了让Clang生成bitcode,可以用下面的命令:

$ clang sum.c -emit-llvm -c -o sum.bc

为了生成汇编表示,可以用下面的命令:

$ clang sum.c -emit-llvm -S -c -o sum.ll

还可以汇编LLVM IR汇编文本,生成bitcode:

$ llvm-as sum.ll -o sum.bc

为了将bitcode变换为IR汇编,这是反向的,可以使用反汇编器:

$ llvm-dis sum.bc -o sum.ll

llvm-extract工具能提取IR函数、全局变量,还能从IR模块中删除全局变量。例如,用下面的命令从sum.bc中提取函数sum:

$ llvm-extract -func=sum sum.bc -o sum-fn.bc

在这个特别的例子中,从sum.bc到sum-fn.bc没有任何变化,因为sum已然是这个模块中唯一的函数。

注: LLVM IR可以在磁盘上存储为两种格式:bitcode和汇编文本

介绍LLVM IR内存中的模型

驻留内存的表示严密地建模了LLVM语言语法。表述IR的C++类的头文件位于include/llvm/IR。下面列举了其中最重要的类:

Module类聚合了整个翻译单元用到的所有数据,它是LLVM术语中的“module”的同义词。它声明了Module::iterator typedef,作为遍历这个模块中的函数的简便方法。你可以用begin()和end()方法获取这些迭代器。在此处查看它的全部接口: Module类

Function类包含有关函数定义和声明的所有对象。对于声明来说(用isDeclaration()检查它是否为声明),它仅包含函数原型。无论定义或者声明,它都包含函数参数的列表,可通过getArgumentList()方法或者arg_begin()和arg_end()这对方法访问它。你可以通过Function::arg_iterator typedef遍历它们。如果Function对象代表函数定义,你可以通过这样的语句遍历它的内容:for (Function::iterator i = function.begin(), e = function.end(); i != e; ++i),你将遍历它的基本块。可在此处查看它的全部接口: Function类

BasicBlock类封装了LLVM指令序列,可通过begin()/end()访问它们。你可以利用getTerminator()方法直接访问它的最后一条指令,你还可以用一些辅助函数遍历CFG,例如通过getSinglePredecessor()访问前驱基本块,当一个基本块有单一前驱时。然而,如果它有多个前驱基本块,就需要自己遍历前驱列表,这也不难,你只要逐个遍历基本块,查看它们的终结指令的目标基本块。可在此处查看它的全部接口: basicBlock类

Instruction类表示LLVM IR的运算原子,一个单一的指令。利用一些方法可获得高层级的断言,例如isAssociative(),isCommutative(),isIdempotent(),和isTerminator(),但是它的精确的功能可通过getOpcode()获知,它返回llvm::Instruction枚举的一个成员,代表了LLVM IR opcode。可通过op_begin()和op_end()这对方法访问它的操作数,它从User超类继承得到,我们很快将介绍这个超类。可在此处查看它的全部接口: Instruction类

我们还没介绍LLVM最强大的部分(依托SSA形式):Value和User接口;它们让你能够轻松操作use-def和def-use链。在LLVM驻留内存的IR中,一个继承自Value的类意味着,它定义了一个结果,可被其它IR使用。而继承自User的子类意味着,这个实体使用了一个或者多个Value接口。Function和Instruction同时是Value和User的子类,而BasicBlock只是Value的子类。为了理解以上内容,让我们深入地分析这两个类:

Value类定义了use_begin()和use_end()方法,让你能够遍历各个User,为访问它的def-use链提供了轻松的方法。对于每个Value类,你可以通过getName()方法访问它的名字。这个模型决定了任何LLVM值都有一个和它关联的不同的标识。例如,%add1可以标识一个加法指令的结果,BB1可以标识一个基本块,myfunc可以标识一个函数。Value还有一个强大的方法,称为replaceAllUsesWith(Value *),它遍历这个值的所有使用者,用某个其它的值替代它。这是一个好的例子,演示如何替换指令和编写快速的优化。可在此处查看它的全部接口: Value类

User类定义了op_begin()和op_end()方法,让你能够快速访问所有它用到的Value接口。注意这代表了use-def链。你也可以利用一个辅助函数,称为replaceUsesOfWith(Value *From, Value *To),替换所有它用到的值。可在此处查看它的全部接口: User类

注:
高铁等车空闲时间,来不及稍加整理,直接搬运过来了
原文链接:
知乎吴建民的LLVM IR代码生成技术
LLVM工具库文档——中文版

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值