转自:https://github.com/XChainLab/documentation/blob/master/VM/EOS/EOSVMArch.md
LLVM项目是一系列分模块、可重用的编译工具链。它提供了一种代码良好的中间表示(IR),
LLVM实现上可以作为多种语言的后端,还可以提供与语言无关的优化和针对多种CPU的代码生成功能。
LLVM不同于传统的我们熟知的编译器。传统的静态编译器(如gcc)通常将编译分为三个阶段,分别
由三个组件来完成具体工作,分别为前端、优化器和后端,如下图所示。
LLVM项目在整体上也分为三个部分,同传统编译器一致,如下图所示,不同的语言的前端,统一的
优化器,以及针对不同平台的机器码生成。从下图我们也可以得到启发,如果想实现一门自定义的
语言,目前主要的工作可以集中在如何实现一个LLVM的前端上来。
LLVM整体的流程,编译前端将源码编译成LLVM中间格式的文
件,然后使用LLVM Linker进行链接。Linker执行大量的链接时优化,特别是过程间优化。链接得
到的LLVM code最终会被翻译成特定平台的机器码,另外LLVM支持JIT。本地代码生成器会在代码
生成过程中插入一些轻量级的操作指令来收集运行时的一些信息,例如识别hot region。运行时收
集到的信息可以用于离线优化,执行一些更为激进的profile-driven的优化策略,调整native code
以适应特定的架构。
LLVM的优势:
1:持续的程序信息,每个阶段都可以获得程序的信息内容
2:离线代码生成,产生较高的可执行程序
3:便捷的profiling及优化,方便优化的实施
4:透明的运行模式
5:统一,全程编译
LLVM IR
根据编译原理可知,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,即
"IR"。之后再由中间语言,利用后端程序翻译为目标平台的汇编语言。由于中间语言相当于一款编
译器前端和后端的“桥梁”,不同编译器的中间语言IR是不一样的,IR语言的设计直接会影响到编
译器后端的优化工作。LLVM IR官方介绍见:http://llvm.org/docs/LangRef.html
目前LLVM IR提供三种格式,分别是内存里面的IR模型,存储在磁盘上的二进制
格式,存储在磁盘上的文本可读格式。三者本质上没有区别,其中二进制格式以bc为文件扩展名,
文本格式以ll为文件扩展名。除了以上两个格式文件外,和IR相关的文件格式还有s和out文件,这
两种一个是由IR生成汇编的格式文件,一个是生成的可执行文件格式(linux下如ELF格式),
- bc结尾,LLVM IR文件,二进制格式,可以通过lli执行
- ll结尾,LLVM IR文件,文本格式,可以通过lli执行
- s结尾,本地汇编文件
- out, 本地可执行文件
以上几种不同文件的转化图如下所示,整体上我们可以看一下这几种格式的转化关系,同时从中
我们也可以看出工具clang、llvm-dis、llvm-as等工具的作用和使用。
中间语言IR的表示,一般是按照如下的结构进行组织的由外到内分别是:
- 模块(Module)
- 函数(Function)
- 代码块(BasicBlock)
- 指令(Instruction)
LLVM IR指令集
指令集的分类大致可以分为基于栈的,基于运算器的还有基于寄存器的,基于栈的和基于寄存器
的虚拟机目前是比较常见的,两种不同之处主要在运行效率,指令集大小和性能三个方面。LLVM IR采用的是基于寄存器的满足RISC(精简指令集)架构以及load/store模式,也就是说只能通过将load和store 指令来进行CPU和内存间的数据交换。LLVM IR指令集拥有普通CPU一些关键的操作,屏蔽掉了一些和机器相关的一些约束。LLVM提供了足够多的寄存器来存储基本类型值,寄存器是为SSA形式(静态单态赋值),这种形式的UD链(use-define chain, 赋值代表define, 使用变量代表use)便于优化。LLVM指令集仅包含31条操作码。LLVM中的内存地址没有使用SSA形式,因为内存地址有可能会存在别名或指针指向,这样就很难构造出来一个紧凑可靠的SSA表示。在LLVM中一个function就是一组基本块的组合,一个基本块就是一组连续执行的指令并以中止指令结束(包括branch, return, unwind, 或者invoke等),中止指令指明了欲跳转的目的地址。
LLVM IR类型系统
LLVM的类型系统为语言无关。每一个SSA寄存器或者显示的内存对象都有其对应的类型。这些类
型和操作码一起表明这个操作的语义,这些类型信息让LLVM能够在低层次code的基础上进行一
些高层次的分析与转换,LLVM IR包含了一些语言共有的基本类型,并给他们一些预定义的大小,
从8bytes到64bytes不等,基本类型的定义保证了LLVM IR的移植性。同时LLVM又包含了四种复杂
类型,pointer,arrays, structures和functions。这四种类型足够表示现有的所有语言类型。为
了支持类型转换,LLVM提供了一个cast操作来实现类型的转换,同时为了支持地址运算,LLVM
提供了getelementptr的命令。LLVM中的许多优化都是基于地址做的(后续的总结再分析)。
LLVM IR内存模型
LLVM提供特定类型的内存分配,可以使用malloc指令在堆上分配一个或多个同一类型的内存对象,
free指令用来释放malloc分配的内存(和C语言中的内存分配类似)。另外提供了alloca指令用于
在栈上分配内存对象,该内存对象在通常在函数结尾会被释放。统一内存模型,所有能够取地址的
对象都必须显示分配。局部变量也要使用alloca来显示分配,没有隐式地手段来获取内存地址,这就
简化了关于内存的分析。
LLVM IR函数调用
LLVM中对普通函数调用,LLVM提供了call指令来调用附带类型信息的函数指针。这种抽象屏蔽了
机器相关的调用惯例。还有一个不能忽略的就是异常处理,在LLVM中,LLVM提供了invoke和
unwind指令。invoke指令指定在栈展开的过程中必须要执行的代码,例如栈展开的时候需要析构
局部对象等。而unwind指令用于抛出异常并执行栈展开的操作。栈展开的过程会被invoke指令停
下来,执行catch块中的行为或者执行在跳出当前活动记录之前需的操作。执行完成后继续代码执
行或者继续栈展开操作。注意像C++的RTTI(运行时类型识别)则由C++自己的库处理,LLVM并不负责。