LLVM IR 是什么? 根据编译原理知识,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,我们编译器从业人员称之为“IR”-- 指令集,之后再由中间语言,利用后端程序和设备翻译为目标平台的汇编语言。 无疑,不同编译器的中间语言 IR 是不一样的,而 IR 可以说是集中体现了这款编译器的特征 ---- ..

LLVM IR 基础介绍

LLVM IR 是什么?

根据编译原理知识,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,我们编译器从业人员称之为“IR”-- 指令集,之后再由中间语言,利用后端程序和设备翻译为目标平台的汇编语言。

无疑,不同编译器的中间语言 IR 是不一样的,而 IR 可以说是集中体现了这款编译器的特征 ---- 他的算法,优化方式,汇编流程等等,想要完全掌握某种编译器的工作和运行原理,分析和学习这款编译器的中间语言无疑是重要手段,另外,由于中间语言相当于一款编译器前端和后端的“桥梁”,如果我们想进行基于 LLVM 的后端移植,无疑需要开发出对应目标平台的编译器后端,想要顺利完成这一工作,透彻了解 LLVM 的中间语言无疑是非常必要的工作。

LLVM 相对于 gcc 的一大改进就是大大提高了中间语言的生成效率和可读性,我个人感觉 LLVM 的中间语言是一种介于 c 语言和汇编语言的格式,他既有高级语言的可读性,又能比较全面地反映计算机底层数据的运算和传输的情况,精炼而又高效,相对而言,gcc 的中间代码有如科幻小说一般晦涩难懂。

LLVM IR 三种表示方式

LLVM IR 语言,旨在用于三个不同的形式:内存中的编译中间语言(IR),保存在硬盘上的 bitcode(.bc文件,适合快速地被一个 JIT 编译器加载),一个可读性的汇编语言表示(.ll文件)。如此,LLVM 将为高效编译转换和分析提供一个强大的中间表示。事实上,LLVM 的三种不同的形式都是等价的。以下是三种表示的转化方式。

image.png

LLVM 语言旨在轻量、底层、同时富有表现力,类型化,易于扩展。LLVM IR 语言目标是成为一种 "通用中间语言",通过足够低层次使得高级语言可以清晰的映射到它。通过提供类型信息,LLVM IR 语言可以作为优化的目标:例如,通过指针分析,可以证明,一个 C 自动变量从不当前函数之外访问,允许它被提升到一个简单的 SSA 值,而不是一个堆变量。

LLVM IR 主要组成

LLVM 标识符有 2 个基本类型:全局和局部。全局标识符(函数,全局变量)以“@”字符开头。本地标识符(注册名称,类型)以 '%' 字符开头。此外,有三种不同的格式标识符,用于不同的目的:

LLVM 语言之所以使用特定的前缀,有 2 个理由:编译器不需要担心命名与保留字冲突,以及保留字在未来进行扩展代价更小。此外,非命名标识符允许编译器能够迅速拿出一个临时变量,而不必以避免符号表冲突。

LLVM 语言的保留字和其他语言非常类似。不同的关键字对应不同的 opcode('add', 'bitcast', 'ret'....), 不同的类型名('void', 'i32'...)。他们不会和变量名重复,因为他们不以 '@', '%' 开头。

下面是一个 LLVM 语言例子,int x 乘以 8:

%result = mul i32 %X, 8

简化后的版本:

%result = shl i32 %X, 3

LLVM 语言的一些特征如下:

实例说明

首先编写以下 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 语法主要包含以下几方面:

回帖   
请输入回帖内容...