第159集:LLVM IR入门
LLVM IR的基本概念
LLVM IR(LLVM Intermediate Representation)是LLVM编译器框架中使用的中间代码表示形式,它是一种低级的、类汇编语言的中间表示,设计目标是独立于具体的硬件平台和源代码语言,同时便于进行各种代码优化。LLVM IR是LLVM编译器框架的核心,它连接了前端和后端,使得不同的前端语言可以共享相同的优化和代码生成基础设施。
LLVM的设计理念
LLVM(Low Level Virtual Machine)最初是一个研究项目,旨在开发一个现代的、基于SSA(静态单赋值)的编译基础设施。LLVM的设计理念包括:
- 模块化:LLVM的各个组件(前端、优化器、后端)都是模块化的,可以独立使用
- 可重用:不同的前端语言可以共享相同的优化和代码生成基础设施
- 可扩展:LLVM的架构设计便于添加新的语言前端、优化通道和目标后端
- 高性能:LLVM的优化器和代码生成器专注于生成高效的代码
- 开放标准:LLVM是开源的,有活跃的社区支持
LLVM IR的特点
- 平台无关:LLVM IR不依赖于具体的硬件平台,可以在不同的平台上运行
- 类型安全:LLVM IR是类型安全的,每个值都有明确的类型
- SSA形式:LLVM IR采用静态单赋值(SSA)形式,便于进行各种优化
- 丰富的指令集:LLVM IR提供了丰富的指令集,支持各种语言特性
- 内存模型:LLVM IR有明确的内存模型,支持不同的内存操作
- 元数据支持:LLVM IR支持元数据,可以存储调试信息、代码覆盖率等额外信息
LLVM IR的语法
LLVM IR有三种表示形式:
- 人类可读的汇编形式:用于调试和手工编写LLVM IR
- 二进制位码形式:用于存储和传输,占用空间小
- 内存中的数据结构形式:用于LLVM内部处理
本集主要介绍人类可读的汇编形式。
基本语法元素
模块(Module)
模块是LLVM IR的顶层容器,对应于一个编译单元(如一个源文件)。一个模块包含函数、全局变量、常量等。
; ModuleID = 'example.ll'
source_filename = "example.c"
; 全局变量
@global_var = global i32 42
; 函数定义
define i32 @add(i32 %a, i32 %b) {
; 函数体
}函数(Function)
函数定义了一个可执行的代码块,包括参数、返回类型和函数体。
define i32 @add(i32 %a, i32 %b) {
entry:
%sum = add i32 %a, %b
ret i32 %sum
}基本块(Basic Block)
基本块是函数中的一个连续的指令序列,只有一个入口点和一个出口点,没有中间的跳转或分支。基本块通常以标签(label)开头。
entry:
%sum = add i32 %a, %b
ret i32 %sum指令(Instruction)
指令是LLVM IR的基本执行单位,每个指令执行一个操作并产生一个结果(除了一些终止指令)。
%sum = add i32 %a, %b
ret i32 %sum操作数(Operand)
操作数是指令的参数,可以是常量、变量、标签等。
%sum = add i32 %a, %b ; %a和%b是操作数类型(Type)
LLVM IR是类型安全的,每个值都有明确的类型。常见的类型包括:
- 基本类型:i1(布尔值)、i8(字节)、i16(短整型)、i32(整型)、i64(长整型)、float(单精度浮点)、double(双精度浮点)
- 指针类型:如 i32*(指向整型的指针)
- 数组类型:如 [10 x i32](10个整型的数组)
- 结构体类型:如 { i32, double }(包含一个整型和一个双精度浮点的结构体)
- 函数类型:如 i32 (i32, i32)(接受两个整型参数并返回整型的函数)
LLVM IR的基本块
基本块是LLVM IR中函数的基本组成部分,每个基本块包含一系列连续的指令,从基本块的标签开始,到一个终止指令(如ret、br、switch等)结束。基本块之间通过控制流指令(如br、switch等)连接。
基本块的结构
一个基本块通常包含以下部分:
- 标签:基本块的名称,以冒号结尾
- 指令序列:一系列连续的指令
- 终止指令:基本块的最后一条指令,决定程序的控制流
基本块的示例
define i32 @max(i32 %a, i32 %b) {
entry:
%cmp = icmp sgt i32 %a, %b
br i1 %cmp, label %if_true, label %if_false
if_true:
ret i32 %a
if_false:
ret i32 %b
}在这个示例中,函数 @max 包含三个基本块:
- entry:比较两个参数的大小,并根据比较结果跳转到不同的基本块
- if_true:如果第一个参数大于第二个参数,返回第一个参数
- if_false:如果第一个参数不大于第二个参数,返回第二个参数
LLVM IR的指令类型
LLVM IR提供了丰富的指令类型,用于执行各种操作。
算术指令
算术指令用于执行基本的算术运算,如加法、减法、乘法、除法等。
| 指令 | 语法 | 描述 |
|---|---|---|
| add | %result = add i32 %a, %b |
加法 |
| sub | %result = sub i32 %a, %b |
减法 |
| mul | %result = mul i32 %a, %b |
乘法 |
| sdiv | %result = sdiv i32 %a, %b |
有符号除法 |
| udiv | %result = udiv i32 %a, %b |
无符号除法 |
| srem | %result = srem i32 %a, %b |
有符号取余 |
| urem | %result = urem i32 %a, %b |
无符号取余 |
逻辑指令
逻辑指令用于执行逻辑运算,如与、或、异或等。
| 指令 | 语法 | 描述 |
|---|---|---|
| and | %result = and i32 %a, %b |
按位与 |
| or | %result = or i32 %a, %b |
按位或 |
| xor | %result = xor i32 %a, %b |
按位异或 |
| not | %result = not i32 %a |
按位取反 |
比较指令
比较指令用于比较两个值的大小或相等性,返回一个布尔值(i1类型)。
| 指令 | 语法 | 描述 |
|---|---|---|
| icmp | %result = icmp sgt i32 %a, %b |
整数比较 |
| fcmp | %result = fcmp ogt double %a, %b |
浮点比较 |
icmp指令的比较操作符:
- eq:等于
- ne:不等于
- sgt:有符号大于
- sge:有符号大于等于
- slt:有符号小于
- sle:有符号小于等于
- ugt:无符号大于
- uge:无符号大于等于
- ult:无符号小于
- ule:无符号小于等于
控制流指令
控制流指令用于控制程序的执行流程,如跳转、分支等。
| 指令 | 语法 | 描述 |
|---|---|---|
| br | br i1 %cond, label %true, label %false |
条件分支 |
| br | br label %dest |
无条件跳转 |
| switch | switch i32 %value, label %default [ i32 0, label %case0, i32 1, label %case1 ] |
switch语句 |
| ret | ret i32 %value |
返回值 |
| ret | ret void |
无返回值 |
| call | %result = call i32 @func(i32 %arg) |
函数调用 |
内存指令
内存指令用于访问内存,如加载、存储、分配内存等。
| 指令 | 语法 | 描述 |
|---|---|---|
| alloca | %ptr = alloca i32 |
在栈上分配内存 |
| load | %value = load i32, i32* %ptr |
从内存加载值 |
| store | store i32 %value, i32* %ptr |
存储值到内存 |
| getelementptr | %ptr = getelementptr inbounds [10 x i32], [10 x i32]* %array, i64 0, i64 %index |
获取元素指针 |
| malloc | %ptr = malloc i32 |
在堆上分配内存 |
| free | free i32* %ptr |
释放内存 |
其他指令
| 指令 | 语法 | 描述 |
|---|---|---|
| phi | %result = phi i32 [ %a, %bb1 ], [ %b, %bb2 ] |
PHI节点,用于SSA形式 |
| select | %result = select i1 %cond, i32 %true, i32 %false |
选择指令 |
| unreachable | unreachable |
标记不可达代码 |
LLVM IR的模块结构
一个完整的LLVM IR模块通常包含以下部分:
- 模块头部:包含模块ID、源文件名等信息
- 目标三元组:指定目标平台,如
target triple = "x86_64-pc-linux-gnu" - 数据布局:指定数据的布局方式,如
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" - 全局变量:定义全局变量
- 函数声明:声明外部函数
- 函数定义:定义函数
- 元数据:存储调试信息、代码覆盖率等额外信息
模块示例
; ModuleID = 'example.ll'
source_filename = "example.c"
target triple = "x86_64-pc-linux-gnu"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
; 全局变量
@global_var = global i32 42
; 函数声明
declare i32 @printf(i8*, ...)
; 函数定义
define i32 @main() {
entry:
%a = alloca i32
%b = alloca i32
store i32 10, i32* %a
store i32 20, i32* %b
%a_val = load i32, i32* %a
%b_val = load i32, i32* %b
%sum = add i32 %a_val, %b_val
%global_val = load i32, i32* @global_var
%total = add i32 %sum, %global_val
%format = getelementptr inbounds [4 x i8], [4 x i8]* @.fmt, i64 0, i64 0
call i32 (i8*, ...) @printf(i8* %format, i32 %total)
ret i32 0
}
; 常量
@.fmt = private constant [4 x i8] c"%d\0A\00"LLVM IR的使用示例
示例1:简单的加法函数
int add(int a, int b) {
return a + b;
}对应的LLVM IR:
define i32 @add(i32 %a, i32 %b) {
entry:
%sum = add i32 %a, %b
ret i32 %sum
}示例2:条件语句
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}对应的LLVM IR:
define i32 @max(i32 %a, i32 %b) {
entry:
%cmp = icmp sgt i32 %a, %b
br i1 %cmp, label %if_true, label %if_false
if_true:
ret i32 %a
if_false:
ret i32 %b
}示例3:循环语句
int sum(int n) {
int s = 0;
for (int i = 0; i < n; i++) {
s += i;
}
return s;
}对应的LLVM IR:
define i32 @sum(i32 %n) {
entry:
%s = alloca i32
%i = alloca i32
store i32 0, i32* %s
store i32 0, i32* %i
br label %loop_cond
loop_cond:
%i_val = load i32, i32* %i
%cmp = icmp slt i32 %i_val, %n
br i1 %cmp, label %loop_body, label %loop_end
loop_body:
%s_val = load i32, i32* %s
%i_val2 = load i32, i32* %i
%add = add i32 %s_val, %i_val2
store i32 %add, i32* %s
%i_val3 = load i32, i32* %i
%inc = add i32 %i_val3, 1
store i32 %inc, i32* %i
br label %loop_cond
loop_end:
%s_val4 = load i32, i32* %s
ret i32 %s_val4
}示例4:函数调用
int foo(int x) {
return x * 2;
}
int bar(int y) {
return foo(y) + 1;
}对应的LLVM IR:
define i32 @foo(i32 %x) {
entry:
%mul = mul i32 %x, 2
ret i32 %mul
}
define i32 @bar(i32 %y) {
entry:
%call = call i32 @foo(i32 %y)
%add = add i32 %call, 1
ret i32 %add
}LLVM IR的优势
- 平台无关:LLVM IR不依赖于具体的硬件平台,可以在不同的平台上运行
- 类型安全:LLVM IR是类型安全的,每个值都有明确的类型
- SSA形式:LLVM IR采用静态单赋值(SSA)形式,便于进行各种优化
- 丰富的指令集:LLVM IR提供了丰富的指令集,支持各种语言特性
- 内存模型:LLVM IR有明确的内存模型,支持不同的内存操作
- 元数据支持:LLVM IR支持元数据,可以存储调试信息、代码覆盖率等额外信息
- 优化友好:LLVM IR的设计便于进行各种优化,如常量折叠、死代码消除、公共子表达式消除等
- 工具链支持:LLVM提供了丰富的工具链,如优化器、代码生成器、汇编器、链接器等
- 活跃的社区:LLVM有活跃的社区支持,不断发展和改进
LLVM IR与其他中间表示的比较
| 特性 | LLVM IR | 抽象语法树 | 四元式 | 三元式 | 字节码 |
|---|---|---|---|---|---|
| 结构 | 线性指令序列(SSA形式) | 树形结构 | 线性结构 | 线性结构 | 线性指令序列 |
| 执行方式 | 需要转换为机器码 | 需要转换为其他表示 | 需要转换为其他表示 | 需要转换为其他表示 | 虚拟机执行 |
| 平台依赖性 | 平台无关 | 平台无关 | 平台无关 | 平台无关 | 平台无关 |
| 执行效率 | 高(编译为机器码) | 低 | 低 | 低 | 中等(解释执行)/ 高(JIT) |
| 空间开销 | 中等 | 大 | 中等 | 小 | 小 |
| 生成难度 | 中等 | 中等 | 简单 | 简单 | 中等 |
| 优化难度 | 低(优化器成熟) | 高 | 高 | 中 | 中等(静态)/ 高(动态) |
| 工具支持 | 丰富 | 有限 | 有限 | 有限 | 中等 |
LLVM IR的应用场景
- 编译器前端:将高级语言编译为LLVM IR
- 代码优化:使用LLVM的优化器对LLVM IR进行优化
- 代码生成:将LLVM IR编译为目标平台的机器码
- 静态分析:使用LLVM的静态分析工具分析LLVM IR
- JIT编译:使用LLVM的JIT编译器在运行时编译LLVM IR
- 跨语言调用:不同语言编译为LLVM IR后可以相互调用
- 代码转换:将一种语言的代码转换为另一种语言的代码
- 教育和研究:作为编译原理的教学和研究工具
实用案例分析
案例:使用LLVM IR实现简单的表达式计算器
问题:使用LLVM IR实现一个简单的表达式计算器,支持基本的算术运算。
分析:
- 定义一个函数,接受两个整数参数和一个操作符参数
- 根据操作符执行相应的算术运算
- 返回运算结果
实现:
define i32 @calculate(i32 %a, i32 %b, i32 %op) {
entry:
switch i32 %op, label %default [
i32 0, label %add
i32 1, label %sub
i32 2, label %mul
i32 3, label %div
]
add:
%sum = add i32 %a, %b
ret i32 %sum
sub:
%diff = sub i32 %a, %b
ret i32 %diff
mul:
%prod = mul i32 %a, %b
ret i32 %prod
div:
%quot = sdiv i32 %a, %b
ret i32 %quot
default:
ret i32 0
}案例:使用LLVM IR实现递归函数
问题:使用LLVM IR实现一个递归函数,计算斐波那契数列的第n项。
分析:
- 定义一个递归函数,接受一个整数参数n
- 基本情况:如果n <= 1,返回n
- 递归情况:返回fib(n-1) + fib(n-2)
实现:
define i32 @fib(i32 %n) {
entry:
%cmp = icmp sle i32 %n, 1
br i1 %cmp, label %base, label %recurse
base:
ret i32 %n
recurse:
%n_minus_1 = sub i32 %n, 1
%call1 = call i32 @fib(i32 %n_minus_1)
%n_minus_2 = sub i32 %n, 2
%call2 = call i32 @fib(i32 %n_minus_2)
%sum = add i32 %call1, %call2
ret i32 %sum
}小结
本集我们学习了LLVM IR作为中间代码表示的相关知识,包括:
- LLVM IR的基本概念:LLVM的设计理念和LLVM IR的特点
- LLVM IR的语法:三种表示形式和基本语法元素
- LLVM IR的基本块:基本块的结构和示例
- LLVM IR的指令类型:算术指令、逻辑指令、比较指令、控制流指令、内存指令等
- LLVM IR的模块结构:模块的组成部分和示例
- LLVM IR的使用示例:简单函数、条件语句、循环语句、函数调用等
- LLVM IR的优势:平台无关、类型安全、SSA形式、丰富的指令集等
- LLVM IR与其他中间表示的比较:结构、执行方式、平台依赖性、执行效率等
- LLVM IR的应用场景:编译器前端、代码优化、代码生成、静态分析等
- 实用案例分析:表达式计算器和递归函数
通过本集的学习,你应该能够理解LLVM IR的结构和使用方法,以及它在编译器中的重要作用。LLVM IR作为一种现代化的中间代码表示形式,已经被广泛应用于各种编译器和工具中,它的设计理念和技术实现对理解现代编译器的工作原理有重要的参考价值。
思考与练习
编写一个LLVM IR模块,实现一个函数,计算两个整数的最大公约数。
编写一个LLVM IR模块,实现一个函数,判断一个数是否为质数。
分析LLVM IR的SSA形式,讨论它对代码优化的影响。
比较LLVM IR和传统的中间表示(如四元式、三元式)的优缺点。
讨论LLVM IR在现代编译器架构中的地位和作用。