LLVM IR 基础介绍

本贴最后更新于 2032 天前,其中的信息可能已经东海扬尘

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

  • 命名值以字母序列开头,例如:%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 存储类
    • 本地线程存储模型
    • 结构类型
    • 全局变量
    • 函数
    • 别名
    • ...
  • 类型系统

  • 常量表示

  • 其他值表示

  • 元数据表示

  • 元数据模型标识

  • 全局变量的本质

  • 不同架构的指令集参考

  • ...

  • LLVM
    20 引用 • 3 回帖 • 1 关注
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    565 引用 • 3532 回帖
  • 软件工程
    29 引用 • 81 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...