LLVM IR 是什么?
根据编译原理知识,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,我们编译器从业人员称之为“IR”--指令集,之后再由中间语言,利用后端程序和设备翻译为目标平台的汇编语言。
无疑,不同编译器的中间语言 IR 是不一样的,而 IR 可以说是集中体现了这款编译器的特征----他的算法,优化方式,汇编流程等等,想要完全掌握某种编译器的工作和运行原理,分析和学习这款编译器的中间语言无疑是重要手段,另外,由于中间语言相当于一款编译器前端和后端的“桥梁”,如果我们想进行基于 LLVM 的后端移植,无疑需要开发出对应目标平台的编译器后端,想要顺利完成这一工作,透彻了解 LLVM 的中间语言无疑是非常必要的工作。
LLVM 相对于 gcc 的一大改进就是大大提高了中间语言的生成效率和可读性,我个人感觉 LLVM 的中间语言是一种介于 c 语言和汇编语言的格式,他既有高级语言的可读性,又能比较全面地反映计算机底层数据的运算和传输的情况,精炼而又高效,相对而言,gcc 的中间代码有如科幻小说一般晦涩难懂。
LLVM IR 三种表示方式
LLVM IR 语言,旨在用于三个不同的形式:内存中的编译中间语言(IR),保存在硬盘上的 bitcode(.bc
文件,适合快速地被一个 JIT 编译器加载),一个可读性的汇编语言表示(.ll
文件)。如此,LLVM 将为高效编译转换和分析提供一个强大的中间表示。事实上,LLVM 的三种不同的形式都是等价的。以下是三种表示的转化方式。
LLVM 语言旨在轻量、底层、同时富有表现力,类型化,易于扩展。LLVM IR 语言目标是成为一种"通用中间语言",通过足够低层次使得高级语言可以清晰的映射到它。通过提供类型信息,LLVM IR 语言可以作为优化的目标:例如,通过指针分析,可以证明,一个 C 自动变量从不当前函数之外访问,允许它被提升到一个简单的 SSA 值,而不是一个堆变量。
LLVM IR 主要组成
LLVM 标识符有 2 个基本类型:全局和局部。全局标识符(函数,全局变量)以“@”字符开头。本地标识符(注册名称,类型)以'%'字符开头。此外,有三种不同的格式标识符,用于不同的目的:
-
命名值以字母序列开头,例如:
%foo
,@DivisionByZero
,%a.really.long.identifier
. 正则表达式为:‘[%@][-a-zA-Z$._][-a-zA-Z$._0-9]*
‘. -
非命名值以数字开头,例如:
%12
,@2
,%44
. -
常量
LLVM 语言之所以使用特定的前缀,有 2 个理由:编译器不需要担心命名与保留字冲突,以及保留字在未来进行扩展代价更小。此外,非命名标识符允许编译器能够迅速拿出一个临时变量,而不必以避免符号表冲突。
LLVM 语言的保留字和其他语言非常类似。不同的关键字对应不同的 opcode('add', 'bitcast', 'ret'....),不同的类型名('void', 'i32'...)。他们不会和变量名重复,因为他们不以'@', '%'开头。
下面是一个 LLVM 语言例子,int x 乘以 8:
%result = mul i32 %X, 8
简化后的版本:
%result = shl i32 %X, 3
LLVM 语言的一些特征如下:
-
注释行以“;”开头
-
如果计算结果没有被赋值给一个命名值,那么一个临时非命名值会创建,并被赋值。
-
临时非命名值,是从‘0’开始,按顺序递增命名的。注意基本块和非命名的函数参数也包含在这里。
实例说明
首先编写以下 C 源码 test.c
#include<stdio.h>
int main(){
int a, b;
return a+b;
}
运行
clang-6.0 -emit-llvm test.c -S -o test.ll
查看 test.ll 内容如下:
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
; Function Attrs: noinline nounwind optnone uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, i32* %1, align 4
%4 = load i32, i32* %2, align 4
%5 = load i32, i32* %3, align 4
%6 = add nsw i32 %4, %5
ret i32 %6
}
attributes \#0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)"}
说明:前面部分为程序的标签属性说明,后面正文部分从 define 开始,即
define i32 @main() \#0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, i32* %1, align 4
%4 = load i32, i32* %2, align 4
%5 = load i32, i32* %3, align 4
%6 = add nsw i32 %4, %5
ret i32 %6
}
主要符号:i32 表示一个 32 位的整型;@ 代表全局变量;% 代表局部变量;alloca 指令用于分配内存堆栈给当前执行的函数,当这个函数返回其调用者退出时自动释放;load 是装载,读出变量中的内容;store 是写入,将值写入变量中;align 表示对齐字节,和程序声明的变量数据类型有关;add 是 IR 中的加法指令;nsw 是“No Signed Wrap”缩写,是一种无符号值运算的标识;ret 表示返回值。
主要工作流程:这段 IR 指令的核心就是把 a 和 b 的值先存入一个临时变量,再将两个临时变量的值读到寄存器中,利用 add 指令将寄存器中的数相加,最后通过 ret 指令返回。
以上是一个简单的 LLVM IR 基本语法介绍,更多的语法可参考 LLVM Language Reference Manual。
其他
LLVM Language Reference Manual 中 LLVM IR 语法主要包含以下几方面:
-
标识符
-
高层次结构
- 模块结构
- 链接类型
- 调用约定
- 符号可见性
- DLL 存储类
- 本地线程存储模型
- 结构类型
- 全局变量
- 函数
- 别名
- ...
-
类型系统
-
常量表示
-
其他值表示
-
元数据表示
-
元数据模型标识
-
全局变量的本质
-
不同架构的指令集参考
-
...
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于