1.3. LLVM 汇编简介

LLVM 是低级虚拟机,其对应的指令可以看做是一种低级跨平台汇编语言(LLVM IR 是一种 SSA 静态单赋值语言)。本节我们简单介绍 LLVM 汇编语言。

最简汇编程序

最小编译器的例子我们见识过最简 LLVM 汇编程序:

; hello.ll
define i32 @main() {
	ret i32 42
}

;开始的是行注释,注明了汇编程序文件为 hello.ll。define 定义一个 @main 函数,函数返回值是 i32 类型。@main 函数的实现只有一个 ret 返回语句,返回 i32 类型的 42。

通过以下命令编译为可执行程序、并执行和查看返回值:

$ clang hello.ll
warning: overriding the module target triple with x86_64-apple-macosx10.15.4 [-Woverride-module]
1 warning generated.
$ ./a.out
$ echo $?
42

其中 clang 命令输出了一个警告信息:表示该 LLVM 程序没有指定目标平台,因此用本地环境覆盖了。警告错误中的 x86_64-apple-macosx10.15.4 为 LLVM 到目标三元组,第一个 x86_64 表示 CPU 类型、第二个 apple 表示操作系统类型、第三个 macosx10.15.4 操作系统版本信息。我们可以通过给 clang 添加 -Wno-override-module 命令行参数关闭该警告信息(在汇编程序中添加 target triple = "x86_64-apple-macosx10.15.4" 也可以,不过汇编程序就绑死了目标平台)。

打印加减法结果

要实现加减法运算,可以通过 LLVM 的 add 和 sub 指令完成:

define i32 @main() {
	%x1 = add i32 1, 3
	%x2 = sub i32 %x1, 2
	ret i32 %x2 ; 1+3-2
}

其中 add 和 sub 分别做减法和减法,指令后面跟着的 3 个参数分别是类型二元操作数。返回的结果依然可以通过 echo $? 方式查看,不过需要注意到是 shell 只支持 0-255 范围内的返回值。

如果要直接输出运算结果,可以借助 C 语言的 @printf 函数完成:

declare i32 @printf(i8*,...)

@format = constant [4 x i8] c"%d\0A\00"

define i32 @main() {
	; 1 + 3 - 2
	%x1 = add i32 1, 3
	%x2 = sub i32 %x1, 2

	; printf("%d\n", x2)
	call i32(i8*,...) @printf(i8* getelementptr([4 x i8], [4 x i8]* @format, i32 0, i32 0), i32 %x2)

	ret i32 0
}

首先通过 declare 指令从外部导入 @printf 函数。然后定义 @format 字符串常量 "%d\n",用于 printf 函数的第一个参数(其中 %d 表示输出一个整数)。call 指令调用@printf打印函数输出%x2的值,其中 getelementptr 是将 @format 转为 i8* 类型的指针传入第一个参数,第二个参数是 i32 类型的 %x2

程序结构

LLVM 汇编程序的结构大同小异,最开始是目标三元组(可省略):

target triple = "x86_64-pc-linux-gnu"

然后是导入的和本地定义的函数、类型、常量、变量等:

; 声明 puts 函数
declare i32 @puts(i8*)

; 定义常量字符串
@msg = constant [14 x i8] c"Hello, world!\00"

最后是全局函数的定义:

define i32 @main() {
entry:
	call i32(i8*) @puts(i8* getelementptr([14 x i8], [14 x i8]* @msg, i32 0, i32 0))
	ret i32 0
}

这里的 main 函数通过外部的 C语言 puts 函数输出一个 Hello, world! 字符串。

小结

LLVM IR 是一种 SSA 静态单赋值语言,因此每个名称在您的程序中只能被分配一次。全局名称以 @ 开头,通常用于全局常量或函数名称,例如 @main。在每个函数中使用局部寄存器名称,这些名称以 % 开头(这些并不是真正的寄存器,因此数量并无限制)。

LLVM的常见类型:

  • i1:1 位整数,可以用于分支判断的条件
  • i8:8 位整数,对应 byte 类型
  • i32:32 位整数
  • i64:64 位整数
  • [N x type]:长度为 N,类似是 type 的数组,比如 @format = constant [4 x i8] c"%d\0A\00" 对应 [4]int8 类型数组。
  • type*:指向 type 类型的指针
  • type(types):函数类型,type 是函数返回值类型,types 是参数的类型。比如 main 函数可能是 i32()i32(i32,i8**) 类型。

每个 LLVM IR 函数内的指令被分组为基本块或普通指令。每个基本块都可以选择以一个标签开头(如果只有一个块则可以省略)。并且每个基本块都必须以特殊的“终止符指令”结尾(比如 br 、ret 等指令)。每个基本块标签也对应一个虚拟寄存器,可以通过%label访问其对应的地址。

LLVM 语言参考在这里:https://llvm.org/docs/LangRef.html

© 2021-2022 | 柴树杉 保留所有权利