llvm学习(七):IR 的基础结构

手头的整个项目差不多完结了,一个月左右的时间,对 llvm 的 pass 理解感觉还行,稍微总结一下从易到难的知识点吧。

一、llvm 的背景知识

简介的话网上搜吧,说下我的理解。llvm 的前端是从 源代码 转化为 IR 的,之后 IR 经过 N 个 Pass 的处理、优化后,生成一份最终的 IR 表示,再生成 llvm 专用的 bitcode ,最后使用后端,将这个bitcode转化为目标架构的可执行文件。

本文的重点是 IR 的结构,属于前端的内容,因为后面要讲 Pass 的编写,所以先把基础知识讲一下。

二、Module

Module 可以被视为一个.c文件的 IR 表示,如果用Java的描述的话,Module相当于Java里的类,是独立存在的一个东西。

每个Module都是相对独立的东西,主要包含了声明或者定义的函数、声明或定义的全局变量、当前Module的基本信息(例如目标架构、数据布局等我不太懂的东西),因为在开发Pass的过程中,基本打交道的就是 FunctionGlobalVariable 这两个东西。

多个Module之间是相互隔离的、无法获取对方的内容,跟Java里类的设定真的非常像。平时为了获取Module 的主要信息,使用它的 M.dump() 方法就会在屏幕上打印出全部信息,非常非常的方便。

三、Function

Function,是Module中以List的方式存放的,如果用Java的描述的话,Function相当于Java里的Method,无法单独存在,必须是在某个Module里的。主要包含了大量BasicBlock 、参数和返回值类型、可变参数列表、函数的attribute和其他函数的基本信息(例如函数名、所在Module等)

Function由无数个BasicBlock组成,使用列表存放,有且仅有一个EntryBlock ,是列表中的第一个BasicBlock,代码真正执行的时候,就从EntryBlock开始执行。因为函数不一定只有一个出口,所以Function是无法知道它退出时的Block的,没有这个API,如果非要知道的话可以手动去判断每个BasicBlock是否有后继。

Function有两个很实用的函数,F.dump() 可以打印出全部信息,F.viewCfg() 可以将ControlFlowGraph按照dot的方式存到文件里,使用第三方工具可以很舒服地观察它,在Mac上叫Graphviz.app,效果如图。

四、BasicBlock

BasicBlock,是Function 中以List方式存放的,和Soot里的BasicBlock概念很像,无法单独存在,必须是在某个Function里的,是真正存放可执行代码的容器。主要包含了大量的Instruction ,前驱、后继的BasicBlock,以及一些基本信息(例如名字什么的),相比Function ,它的校验也更加严格,例如不可以凭空出现、不可以处于游离状态。

BasicBlock由很多Instruction组成,按照是否为TerminatorInst 可以将指令分为两类,一类是普通的指令,一类是可以成为TerminatorInst 的指令;因此BasicBlock一定要以TerminatorInst类的指令结尾,而且除了最后一个指令是TerminatorInst,其他指令都是普通指令。常见的TerminatorInst 有BranchInst 、IndirectBrInst 、SwitchInst 和Return ,C++里还有一些异常处理的懒得列出了。

每个BasicBlock都可以视为一段顺序执行的代码,完全不会有任何的分支,有分支就会通过TerminatorInst进行跳转。

BasicBlock 有两种创建方式,一种是凭空创建,然后插入到之前的CFG里;一种比较方便,使用SplitBasicBlock ,切为相连的两块,可能会遇到PhiNode的问题。

五、Instruction

Instruction ,是BasicBlock 中以List方式存放的,和Soot里的Stmt的概念很像,无法单独存在,必须是在某个BasicBlock 里的,可以视为 IR 层面的汇编语句,或者说指令。类型非常多,一开始觉得很混乱,干什么都得查API,用的多了就记住了,多看官方的那个继承关系图。随便举个例子,例如在栈上申请一个int32,就用AllocaInst ,再加上int32的数据类型。

在开发过程中,一旦遇到什么Instruction不会写,千万千万不要去查文档,应该先写一份c代码,看看自动生成的 IR 长什么样,再朝着这个方向去努力(绝大部分问题都是这么解决的)。

将来会新开一篇,专门讲Instruction的常见用法。

六、Operand

这个其实比较宽泛,表示Instruction里的各个参数(如果有的话)。每个Operand 都是Value ,就是任意的东西,怎么说呢,Function可以作为某些指令的参数、BasicBlock也可以作为某些指令的参数、甚至Instruction本身也可以作为某些指令的参数,所以其实这是一个很开发的概念。大部分指令在构造时候,对参数类型都是有要求的,会直接抛assert 错误,所以也不用太担心用错类型。

七、万物皆为Value(不够严谨)

上文所说的各个类,都有一个共同的父类,那就是Value,我把它理解为了Java里的Object,到处都可以用,平时不知道用什么类型去描述一个变量时,直接上Value *就可以了,将来想知道类型可以使用isa<Instruction>(V) 来确认,再用cast<Instruction>(V) 来进行转换。是一个非常好用的类型,(因为我不喜欢用C++里的auto)

八、总结

本文就是随便写写,把常见概念科普一下。