LLVM的IR指令详解
笔者专注于Android安全领域, 欢迎关注个人的微信公众号《 Android安全工程》(可点击进行扫码关注)。个人微信公众号主要围绕 Android 应用的安全防护和逆向分析, 分享各种安全攻防手段、Hook 技术、ARM 汇编等 Android 相关的知识 建议:本文内容较多,建议收藏起来,后面有需要的时候可以当备查手册使用即可。一般IR指令只需要知晓有某个指令,不需要花时间专门去背记。 概述IR 指令是 LLVM 中的一个中间表示形式,用于表示程序的控制流、数据流、内存访问等等,它是一种基于 SSA 形式(Static Single Assignment)的静态单赋值形式。在 LLVM 中,每个 IR 指令都有一个唯一的操作码(opcode),用于标识该指令的类型,每个操作码对应了一组可能的操作数(operands),这些操作数可以是常量、寄存器或者其他指令的结果。
在 LLVM 的 IR 中,所有的数据类型都是基于 LLVM 类型系统定义的,这些数据类型包括整数、浮点数、指针、数组、结构体等等,每个数据类型都具有自己的属性,例如位宽、对齐方式等等。在 IR 中,每个值都有一个类型,这个类型可以被显式地指定,或者通过指令的操作数推导出来。
LLVM 的 IR 指令非常丰富,包括算术、逻辑、比较、转换、控制流等等,它们可以被用来表达复杂的程序结构,同时 IR 指令还可以通过 LLVM 的优化器进行优化,以生成高效的目标代码。
IR指令类型比较多,以下是一些常见的指令类型: - 加减乘除指令:add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv 等
- 位运算指令:and, or, xor, shl, lshr, ashr 等
- 转换指令:trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast 等
- 内存指令:alloca, load, store, getelementptr 等
- 控制流指令:br, switch, ret, indirectbr, invoke, resume, unreachable 等
- 其他指令:phi, select, call, va_arg, landingpad 等
加减乘除指令1.加法指令(add)加法指令用于对两个数值进行相加。在 LLVM 中,加法指令的语法如下所示: 其中, 表示要进行加法运算的值的数据类型,可以是整数、浮点数等; 和 分别表示相加的两个数,可以是常量、寄存器或者其他指令的结果。
在LLVM中,add指令的参数指定了和的类型,同时也指定了的类型。支持的类型包括: - 整数类型:i1, i8, i16, i32, i64, i128等;
- 浮点类型:half, float, double, fp128等;
- 向量类型:, , 等;
- 指针类型:i8*, i32*, float*等;
- 标签类型:metadata;
例如,如果我们想将两个整数相加并得到一个整数结果,可以使用以下指令: 这里,指定为i32,为整数值1,为整数值2,为整数类型i32。各种类型的内存空间大小(以位为单位)如下: - 整数类型:i1占1位,i8占8位,i16占16位,i32占32位,i64占64位,i128占128位;
- 浮点类型:half占16位,float占32位,double占64位,fp128占128位;
- 向量类型:占n * 8位,占n * 16位,占n * 32位等;
- 指针类型:指针类型的大小取决于运行时的操作系统和架构,例如在32位操作系统上,指针类型通常占4个字节(32位),在64位操作系统上,指针类型通常占8个字节(64位);
- 标签类型:metadata类型通常占据与指针类型相同的空间;
需要注意的是,这里只是给出了各种类型在LLVM中的默认大小,实际上在使用LLVM IR时,开发者可以通过在类型后面加上数字来显式指定类型的大小,例如,i16类型可以通过i16 123来表示一个16位整数值123。
下面是一个加法指令的代码示例,将两个整数相加: 这个指令将常量 2 和 3 相加,结果保存到寄存器 %x 中。
除了常量之外,也可以使用寄存器或其他指令的结果作为加法指令的操作数,例如: 1
2
| %x = add i32 %a, %b
%z = add i32 %x, %y
|
第一行代码将寄存器 %a 和 %b 中的值相加,结果保存到寄存器 %x 中;第二行代码将寄存器 %x 和 %y 中的值相加,结果保存到寄存器 %z 中。
在 LLVM 中还支持带进位的加法指令(add with carry)和带溢出的加法指令(add with overflow),这里不再赘述。 2.减法指令(sub)减法指令用于对两个数值进行相减,语法为: 其中, 表示要进行减法运算的值的数据类型,可以是整数、浮点数等; 和 分别表示相减的两个数,可以是常量、寄存器或者其他指令的结果。
下面是一个减法指令的代码示例,将两个整数相减: 这个指令将寄存器 %x 中的值减去 %y 中的值,结果保存到寄存器 %diff 中。
减法指令还有一种形式,可以用于计算两个浮点数之间的差值。语法为: 其中, 表示要进行减法运算的值的数据类型,必须是浮点数类型; 和 分别表示相减的两个数,可以是常量、寄存器或者其他指令的结果。
下面是一个浮点数减法指令的代码示例,将两个单精度浮点数相减: 1
| %diff = fsub float %x, %y
|
这个指令将寄存器 %x 中的单精度浮点数减去 %y 中的单精度浮点数,结果保存到寄存器 %diff 中。 3. 乘法指令(mul)乘法指令用于对两个数值进行相乘,语法为: 其中, 表示要进行乘法运算的值的数据类型,可以是整数、浮点数等; 和 分别表示相乘的两个数,可以是常量、寄存器或者其他指令的结果。
下面是一个乘法指令的代码示例,将两个整数相乘: 这个指令将寄存器 %x 和 %y 中的值相乘,结果保存到寄存器 %prod 中。我们还可以对浮点数进行乘法操作,如下所示: 1
| %result = mul double %value1, %value2
|
这个指令将寄存器 %value1 和 %value2 中的值相乘,结果保存到 %result 中。需要注意的是,对于浮点数的乘法操作,需要使用 double 或 float 等浮点类型。
此外,LLVM 还提供了一些其他类型的乘法指令,例如向量乘法指令、无符号整数乘法指令等。具体的指令使用方法可以参考 LLVM 的官方文档。 4.除法指令(div)除法指令用于对两个数值进行相除,语法为: 其中,表示要执行有符号(`sdiv`)还是无符号(`udiv`)的除法运算; 表示要进行除法运算的值的数据类型,可以是整数、浮点数等;和 分别表示相除的两个数,可以是常量、寄存器或者其他指令的结果。
下面是一个除法指令的代码示例,将两个整数相除: 1
| %quot = sdiv i32 %x, %y
|
这个指令将寄存器 %x 中的值除以 %y 中的值,结果保存到寄存器 %quot 中。由于使用了 sdiv 指令,因此进行的是有符号除法运算。
如果要进行无符号除法运算,可以使用 udiv 指令: 1
| %quot = udiv i32 %x, %y
|
这个指令将寄存器 %x 中的值除以 %y 中的值,结果保存到寄存器 %quot 中。由于使用了 udiv 指令,因此进行的是无符号除法运算。 位运算指令IR有多种位运算指令,包括位与(and)、位或(or)、位异或(xor)、位取反(not)等。这些指令可以对整数类型进行按位操作,并将结果存储到一个新的寄存器中。以下是 IR 中常见的位运算指令及其作用: - 位与(and):将两个整数的二进制表示进行按位与操作。
- 位或(or):将两个整数的二进制表示进行按位或操作。
- 位异或(xor):将两个整数的二进制表示进行按位异或操作。
- 位取反(not):将一个整数的二进制表示进行按位取反操作。
这些指令都可以用类似的语法进行使用,其中 表示要进行位运算的整数的数据类型,可以是 i1、i8、i16、i32、i64 等; 和 分别表示要进行位运算的整数,可以是常量、寄存器或其他指令的结果。例如: 1
2
3
4
| %result = and i32 %x, %y
%result = or i32 %x, %y
%result = xor i32 %x, %y
%result = xor i32 %x, -1
|
第一个指令将 %x 和 %y 进行按位与操作,并将结果保存到 %result 中;第二个指令将 %x 和 %y 进行按位或操作,并将结果保存到 %result 中;第三个指令将 %x 和 %y 进行按位异或操作,并将结果保存到 %result 中;最后一个指令将 %x 和二进制全为 1 的数进行按位异或操作,即将 %x 的每一位取反,结果同样保存到 %result 中。 转换指令- trunc: 将一个整数或浮点数截断为比原来小的位数,即去掉高位的一些二进制位。
- zext: 将一个整数或布尔值的位数增加,新位数的高位都填充为零,即进行零扩展。
- sext: 将一个整数的位数增加,新位数的高位都填充为原有的最高位,即进行符号扩展。
- fptrunc: 将一个浮点数截断为比原来小的位数,即去掉高位的一些二进制位。这是一种舍入操作,可能会丢失一些精度。
- fpext: 将一个浮点数的位数增加,新位数的高位都填充为零,即进行浮点零扩展。
- fptoui: 将一个浮点数转换为一个无符号整数。如果浮点数是负数,则结果为零。
- fptosi: 将一个浮点数转换为一个带符号整数。如果浮点数是负数,则结果为负的最小整数。
- uitofp: 将一个无符号整数转换为一个浮点数。
- sitofp: 将一个带符号整数转换为一个浮点数。
- ptrtoint: 将一个指针类型转换为一个整数类型。该指令通常用于将指针转换为整数进行计算。
- inttoptr: 将一个整数类型转换为一个指针类型。该指令通常用于将整数转换为指针进行内存地址计算。
- bitcast: 将一个值从一种类型转换为另一种类型,但是这些类型必须具有相同的位数。这个指令可以用来实现底层内存操作,例如将浮点数转换为整数以进行位运算。
下面是 IR 转换指令的详细的使用说明和示例: 1.trunctrunc指令将一个整数或浮点数截断为比原来小的位数,即去掉高位的一些二进制位。trunc指令的使用格式如下: 其中,和分别表示源类型和目标类型,表示要转换的值。例如,下面的代码将一个64位整数截断为32位整数: 1
2
| %long = add i64 1, 2
%short = trunc i64 %long to i32
|
在这个例子中,%long是一个64位整数,它的值是3(1+2)。%short是一个32位整数,它的值是3。由于%long被截断为32位整数,因此只有低32位的值保留下来。 2.zextzext指令将一个整数或布尔值的位数增加,新位数的高位都填充为零,即进行零扩展。zext指令的使用格式如下: 例如,下面的代码将一个8位整数扩展为16位整数: 1
2
| %short = add i8 1, 2
%long = zext i8 %short to i16
|
在这个例子中,%short是一个8位整数,它的值是3(1+2)。%long是一个16位整数,它的值是3。由于%short被扩展为16位整数,因此高8位被填充为零。 3.sextsext指令将一个整数的位数增加,新位数的高位都填充为原有的最高位,即进行符号扩展。sext指令的使用格式与zext指令类似: 例如,下面的代码将一个8位整数扩展为16位整数: 1
2
| %short = add i8 -1, 2
%long = sext i8 %short to i16
|
在这个例子中,%short是一个8位整数,它的值是1-2=-1。%long是一个16位整数,它的值是0xffff。由于%short被扩展为16位整数,因此高8位都被填充为1。 4.fptruncfptrunc指令将一个浮点数截断为比原来小的位数,即去掉高位的一些二进制位。fptrunc指令的使用格式如下: 例如,下面的代码将一个双精度浮点数截断为单精度浮点数: 1
2
| %double = fadd double 1.0, 2.0
%float = fptrunc double %double to float
|
在这个例子中,%double是一个双精度浮点数,它的值是3.0(1.0+2.0)。%float是一个单精度浮点数,它的值是3.0。由于%double被截断为单精度浮点数,因此高位的值被截断掉,只有低位的值保留下来。 5.fpextfpext指令将一个浮点数扩展为比原来大的位数,新位数的高位都填充为零。fpext指令的使用格式与fptrunc指令类似: 例如,下面的代码将一个单精度浮点数扩展为双精度浮点数: 1
2
| %float = fadd float 1.0, 2.0
%double = fpext float %float to double
|
在这个例子中,%float是一个单精度浮点数,它的值是3.0(1.0+2.0)。%double是一个双精度浮点数,它的值是3.0。由于%float被扩展为双精度浮点数,新的高位都被填充为零。 6.fptouifptoui指令将一个浮点数转换为一个无符号整数。转换时,如果浮点数的值为负数,则结果为0。fptoui指令的使用格式如下: 例如,下面的代码将一个双精度浮点数转换为32位无符号整数: 1
2
| %double = fadd double 1.0, 2.0
%uint = fptoui double %double to i32
|
在这个例子中,%double是一个双精度浮点数,它的值是3.0(1.0+2.0)。%uint是一个32位无符号整数,它的值是3。由于%double的值为正数,因此可以转换为32位无符号整数。 7.fptosifptosi指令将一个浮点数转换为一个带符号整数。转换时,如果浮点数的值超出了目标类型的表示范围,则结果为该类型的最小值或最大值。fptosi指令的使用格式如下: 例如,下面的代码将一个双精度浮点数转换为32位带符号整数: 1
2
| %double = fadd double 1.0, -2.0
%i32 = fptosi double %double to i32
|
在这个例子中,%double是一个双精度浮点数,它的值是-1.0(1.0-2.0)。%i32是一个32位带符号整数,它的值是-1。由于%double的值为负数,因此可以转换为32位带符号整数。 8.uitofpuitofp指令将一个无符号整数转换为一个浮点数。uitofp指令的使用格式如下: 例如,下面的代码将一个32位无符号整数转换为单精度浮点数: 1
2
| %uint = add i32 1, 2
%float = uitofp i32 %uint to float
|
在这个例子中,%uint是一个32位无符号整数,它的值是3。%float是一个单精度浮点数,它的值是3.0。由于%uint的值为正数,因此可以转换为单精度浮点数。 9.sitofpsitofp指令将一个带符号整数转换为一个浮点数。sitofp指令的使用格式如下: 例如,下面的代码将一个32位带符号整数转换为单精度浮点数: 1
2
| %i32 = add i32 1, -2
%float = sitofp i32 %i32 to float
|
在这个例子中,%i32是一个32位带符号整数,它的值是-1。%float是一个单精度浮点数,它的值是-1.0。由于%i32的值为负数,因此可以转换为单精度浮点数。 10.ptrtointptrtoint指令将一个指针类型转换为一个整数类型。ptrtoint指令的使用格式如下: 例如,下面的代码将一个指针类型转换为64位整数类型: 1
2
| %ptr = alloca i32
%i64 = ptrtoint i32* %ptr to i64
|
在这个例子中,%ptr是一个指向32位整数类型的指针。%i64是一个64位整数类型,它的值是指针%ptr的地址。由于指针类型和整数类型的位宽不同,因此需要使用ptrtoint指令进行类型转换。 11.inttoptrinttoptr指令将一个整数类型转换为一个指针类型。inttoptr指令的使用格式如下: 例如,下面的代码将一个64位整数类型转换为指向32位整数类型的指针: 1
2
| %i64 = add i64 1, 2
%ptr = inttoptr i64 %i64 to i32*
|
在这个例子中,%i64是一个64位整数类型,它的值是3。%ptr是一个指向32位整数类型的指针,它的值是3。由于整数类型和指针类型的位宽不同,因此需要使用inttoptr指令进行类型转换。 12.bitcastbitcast指令将一个值的位表示转换为另一个类型的位表示,但是它不会改变值本身。bitcast指令的使用格式如下: 例如,下面的代码将一个64位双精度浮点数转换为64位整数类型: 1
2
| %double = fadd double 1.0, -2.0
%i64 = bitcast double %double to i64
|
在这个例子中,%double是一个64位双精度浮点数,它的值是-1.0(1.0-2.0)。%i64是一个64位整数类型,它的值是0xbff8000000000000(-4616189618054758400)。由于双精度浮点数和64位整数类型的位宽相同,因此可以使用bitcast指令进行类型转换。 内存指令LLVM IR提供了一些常见的内存指令,包括alloca、load、store、getelementptr、malloc、free、memset、memcpy和memmove等。这些指令可以用于内存分配、初始化和复制操作。下面将对这些指令逐一进行介绍,并提供相应的代码示例。 1.allocaalloca指令用于在栈上分配内存,并返回一个指向新分配的内存的指针。alloca指令的使用格式如下: 其中,是要分配的内存块的类型。例如,下面的代码分配一个包含5个整数的数组: 1
| %array = alloca [5 x i32]
| 2.loadload指令用于从内存中读取数据,并将其加载到寄存器中。load指令的使用格式如下: 其中,是要读取的数据的类型,是指向要读取数据的内存块的指针。例如,下面的代码将一个整数数组的第一个元素加载到寄存器中: 1
2
3
| %array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
%val = load i32, i32* %ptr
|
在这个例子中,%array是一个整数数组,%ptr是指向数组第一个元素的指针,load指令将%ptr指向的内存块中的数据加载到%val寄存器中。 3.storestore指令用于将数据从寄存器中写入内存。store指令的使用格式如下: 其中,是要写入的数据的类型,是要写入的数据的值,是指向要写入数据的内存块的指针。例如,下面的代码将一个整数存储到一个整数数组的第一个元素中: 1
2
3
| %array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
store i32 42, i32* %ptr
|
在这个例子中,%array是一个整数数组,%ptr是指向数组第一个元素的指针,store指令将整数值42存储到%ptr指向的内存块中。 4.getelementptrgetelementptr指令用于计算指针的偏移量,以便访问内存中的数据。getelementptr指令的使用格式如下: 1
| %ptr = getelementptr , * , , ...
|
其中,是指针指向的数据类型,是指向数据的指针,是索引的类型,是索引的值。getelementptr指令可以接受多个索引,每个索引都可以是任意类型的。索引类型必须是整数类型,用于计算偏移量。例如,下面的代码计算一个二维数组中的一个元素的指针: 1
2
| %array = alloca [3 x [4 x i32]]
%ptr = getelementptr [3 x [4 x i32]], [3 x [4 x i32]]* %array, i32 1, i32 2
|
在这个例子中,%array是一个二维数组,%ptr是指向第二行第三列元素的指针。 5.mallocmalloc指令用于在堆上分配内存,并返回一个指向新分配的内存的指针。malloc指令的使用格式如下: 1
| %ptr = call i8* @malloc(i64 )
|
其中,是要分配的内存块的大小。例如,下面的代码分配一个包含10个整数的数组: 1
2
3
| %size = mul i64 10, i64 4
%ptr = call i8* @malloc(i64 %size)
%array = bitcast i8* %ptr to i32*
|
在这个例子中,%size是10个整数占用的字节数,call指令调用malloc函数分配内存,%ptr是指向新分配的内存块的指针,bitcast指令将%ptr指针转换为整数指针类型。 6.freefree指令用于释放之前通过malloc指令分配的内存。free指令的使用格式如下: 其中,是指向要释放的内存块的指针。例如,下面的代码释放之前分配的整数数组: 1
2
| %ptr = bitcast i32* %array to i8*
call void @free(i8* %ptr)
|
在这个例子中,%array是之前通过malloc指令分配的整数数组的指针,bitcast指令将%array指针转换为i8*类型的指针,call指令调用free函数释放内存。 7.memsetmemset指令用于将一段内存区域的内容设置为指定的值。它的基本语法如下: 1
| call void @llvm.memset.p0i8.i64(i8* %dst, i8 %val, i64 %size, i1 0)
|
其中,第一个参数%dst是要设置的内存区域的起始地址,它应该是指针类型。第二个参数%val是要设置的值,它应该是整型。第三个参数%size是内存区域的大小,它应该是64位整型。最后一个参数是一个布尔值,表示对齐方式。如果它为1,表示按照指针类型对齐;如果它为0,表示不按照指针类型对齐。
下面是一个简单的使用示例,将一个整型数组中的所有元素都设置为0: 1
2
3
4
5
6
7
8
| define void @set_to_zero(i32* %array, i32 %size) {
entry:
%zero = alloca i32, align 4
store i32 0, i32* %zero, align 4
%array_end = getelementptr i32, i32* %array, i32 %size
call void @llvm.memset.p0i8.i64(i8* %array, i8 0, i64 sub(i32* %array_end, %array), i1 false)
ret void
}
| 8.memcpymemcpy指令用于将一个内存区域的内容复制到另一个内存区域。它的基本语法如下: 1
| call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 %size, i1 0)
|
其中,第一个参数%dst是目标内存区域的起始地址,它应该是指针类型。第二个参数%src是源内存区域的起始地址,它应该是指针类型。第三个参数%size是内存区域的大小,它应该是64位整型。最后一个参数是一个布尔值,表示对齐方式。如果它为1,表示按照指针类型对齐;如果它为0,表示不按照指针类型对齐。
下面是一个简单的使用示例,将一个整型数组复制到另一个数组中: 1
2
3
4
5
6
| define void @copy_array(i32* %src, i32* %dst, i32 %size) {
entry:
%src_end = getelementptr i32, i32* %src, i32 %size
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 sub(i32* %src_end, %src), i1 false)
ret void
}
| 9.memmovememmove指令用于将源地址指向的内存块中的数据移动到目标地址指向的内存块中,其定义如下: 1
| declare void @llvm.memmove.p0i8.p0i8.i32(i8* nocapture, i8* nocapture, i32, i32, i1)
|
该指令接受五个参数,分别是目标地址、源地址、拷贝的字节数、对齐方式、以及是否进行重叠检查的标志。其中,对齐方式参数表示内存块的对齐方式,如果不需要对齐则设为 1。如果进行重叠检查,需要将标志设为 true,否则设为 false。
下面是一个使用memmove指令将源地址指向的内存块中的数据移动到目标地址指向的内存块中的示例: 1
2
3
4
5
| %src = alloca [10 x i32], align 4
%dst = alloca [10 x i32], align 4
%size = getelementptr [10 x i32], [10 x i32]* %src, i32 0, i32 10
%sizeVal = ptrtoint i32* %size to i32
call void @llvm.memmove.p0i8.p0i8.i32(i8* bitcast ([10 x i32]* %dst to i8*), i8* bitcast ([10 x i32]* %src to i8*), i32 %sizeVal, i32 4, i1 false)
|
该示例中,首先在堆栈上分配了两个 [10 x i32] 类型的内存块,并通过getelementptr指令获取了内存块的大小。然后调用了memmove指令将源地址指向的内存块中的数据移动到目标地址指向的内存块中。需要注意的是,需要使用bitcast指令将源地址和目标地址转换为i8*类型的指针。 控制流指令控制流指令包括以下指令: br:条件分支指令,根据条件跳转到指定的基本块。 switch:多路分支指令,根据输入值跳转到不同的基本块。 ret:函数返回指令,返回到调用函数的地方。 indirectbr:间接分支指令,跳转到存储在指定地址中的基本块。 invoke:调用指令,调用带异常处理的函数,并在异常发生时传递控制权。 resume:异常恢复指令,恢复在调用invoke指令时发生的异常。 unreachable:不可到达指令,表示程序不应该执行到该点,如果执行到该点会导致未定义行为。
这些指令在LLVM IR中用于控制程序的流程,支持高级优化技术,如控制流分析和基于SSA形式的变量重命名。LLVM的控制流指令包括条件分支指令br、多路分支指令switch、函数返回指令ret、间接分支指令indirectbr、调用指令invoke、异常恢复指令resume和不可到达指令unreachable。下面将逐一对这些指令进行说明并给出相应的代码示例。 1.条件分支指令(br)br指令用于执行条件分支,根据条件跳转到不同的基本块。它的语法如下: 其中是条件值,如果其值为真,则跳转到标记为的基本块;否则跳转到标记为的基本块。下面是一个简单的示例: 1
2
3
4
5
6
7
8
9
10
| define i32 @test(i32 %a, i32 %b) {
%cmp = icmp eq i32 %a, %b
br i1 %cmp, label %equal, label %notequal
equal:
ret i32 1
notequal:
ret i32 0
}
|
在这个示例中,我们定义了一个函数test,它接受两个整数参数%a和%b。首先,我们使用icmp指令比较这两个值是否相等,并将结果保存在%cmp中。然后,我们使用br指令根据%cmp的值跳转到不同的基本块,如果它们相等,则返回1;否则返回0。 2.多路分支指令(switch)switch指令用于执行多路分支,根据输入值跳转到不同的基本块。它的语法如下: 1
| switch , label [ , label ... ]
|
其中,是整数类型,是输入值,是默认跳转的基本块。后面的每对, 都表示一个选项,如果等于,则跳转到标记的基本块。下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| define i32 @test(i32 %a) {
switch i32 %a, label %default [
i32 0, label %zero
i32 1, label %one
]
zero:
ret i32 0
one:
ret i32 1
default:
ret i32 -1
}
|
在这个示例中,我们定义了一个函数test,它接受一个整数参数%a。然后,我们使用switch指令根据%a的值跳转到不同的基本块,如果%a等于0,则返回0;如果%a等于1,则返回1;否则返回-1。 3.函数返回指令(ret)ret指令用于从函数中返回一个值。它的语法如下: 其中,是返回值的类型,是返回的值。如果函数没有返回值,则应该是void。下面是一个示例: 1
2
3
4
| define i32 @test(i32 %a, i32 %b) {
%sum = add i32 %a, %b
ret i32 %sum
}
|
在这个示例中,我们定义了一个函数test,它接受两个整数参数%a和%b。首先,我们使用add指令将它们相加并将结果保存在%sum中。然后,我们使用ret指令返回%sum的值。 4.间接分支指令(indirectbr)indirectbr指令用于根据间接地址跳转到不同的基本块。它的语法如下: 1
| indirectbr , [ label , label , ... ]
|
其中,是跳转目标的类型,是指向跳转目标地址的指针。后面的每个表示一个跳转目标基本块的标记。下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| define i32 @test(i32* %ptr) {
%dest1 = label %one
%dest2 = label %two
indirectbr i8* %ptr, [ label %default, label %dest1, label %dest2 ]
one:
ret i32 1
two:
ret i32 2
default:
ret i32 -1
}
|
在这个示例中,我们定义了一个函数test,它接受一个指向整数地址的指针参数%ptr。然后,我们定义了三个标记,分别标记为%one、%two和%default。接着,我们使用indirectbr指令根据%ptr的值跳转到不同的基本块,如果它等于0,则返回1;如果等于1,则返回2;否则返回-1。 5.调用指令(invoke)invoke指令用于调用一个函数,并在发生异常时传递控制权。它的语法与call指令类似,但是多了一个异常处理分支。下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| define void @test() {
%catch = catchswitch within none [label %catch] unwind label %cleanup
invoke void @foo()
to label %normal
unwind label %catch
normal:
catchret from %catch to label %end
end:
ret void
catch:
%excp = catchpad within %catch [i8* null]
call void @handle()
catchret from %excp to label
|
其中,我们定义了一个函数test,它不接受任何参数。首先,我们使用catchswitch指令创建一个异常处理块%catch,然后使用invoke指令调用函数foo。如果函数调用成功,就跳转到标记%normal;否则,就跳转到异常处理块%catch。在标记%normal处,我们使用catchret指令将控制权返回到异常处理块%catch。在异常处理块%catch中,我们使用catchpad指令创建一个异常处理块%excp,并调用handle函数来处理异常。最后,我们使用catchret指令将控制权返回到invoke指令的unwind标记%cleanup。 6.恢复指令(resume)resume指令用于从异常处理块中恢复执行。它的语法如下: 其中,是要恢复的异常类型,是异常值。下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| define void @test() {
%catch = catchswitch within none [label %catch] unwind label %cleanup
invoke void @foo()
to label %normal
unwind label %catch
normal:
catchret from %catch to label %end
end:
ret void
catch:
%excp = catchpad within %catch [i8* null]
%is_error = icmp eq i32 %excp, 1
br i1 %is_error, label %handle, label %next
handle:
call void @handle()
resume void null
next:
catchret from %excp to label %end
}
|
在这个示例中,我们使用resume指令从异常处理块中恢复执行。在标记%catch处,我们使用catchpad指令创建一个异常处理块%excp。然后,我们使用icmp指令将异常值与1进行比较,并根据比较结果跳转到标记%handle或标记%next。在标记%handle处,我们调用handle函数来处理异常,并使用resume指令从异常处理块中恢复执行。在标记%next处,我们使用catchret指令将控制权返回到invoke指令的unwind标记%cleanup。 7.不可达指令(unreachable)unreachable指令用于表示程序不会执行到这里。它的语法如下: 下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
| define i32 @test(i32 %a, i32 %b) {
%is_zero = icmp eq i32 %b, 0
br i1 %is_zero, label %error, label %compute
compute:
%result = sdiv i32 %a, %b
ret i32 %result
error:
unreachable
}
|
在这个示例中,我们定义了一个函数test,它的功能是计算a / b的值。在标记%compute处,我们使用sdiv指令计算a / b的值,并使用ret指令将结果返回。在标记%error处,我们使用unreachable指令表示程序不会执行到这里,因为b的值为0。在这种情况下,我们不需要返回任何值,因为程序已经崩溃了。 其他指令除了上述介绍的七种控制流指令,llvm还有其他常用的指令。在本文中,我们将介绍phi、select、call、va_arg和landingpad这五个指令。 1.phiphi指令用于在基本块之间传递值。它的语法如下: 1
| %result = phi [ , ], [ , ], ...
|
其中,是要传递的值的类型,是要传递的第一个值,是要从中传递第一个值的基本块。其他的和对也类似。下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| define i32 @test(i32 %a, i32 %b) {
%cmp = icmp slt i32 %a, %b
br i1 %cmp, label %if_true, label %if_false
if_true:
%result1 = add i32 %a, 1
br label %merge
if_false:
%result2 = add i32 %b, 1
br label %merge
merge:
%result = phi i32 [ %result1, %if_true ], [ %result2, %if_false ]
ret i32 %result
}
|
在这个示例中,我们定义了一个函数test,它的功能是比较a和b的值,并返回一个结果。在标记%if_true处,我们使用add指令计算a+1的值;在标记%if_false处,我们使用add指令计算b+1的值。然后,在标记%merge处,我们使用phi指令选择一个值。具体来说,如果%cmp的值为true,我们就选择%result1的值(即a+1);否则,我们就选择%result2的值(即b+1)。 2.selectselect指令用于根据条件选择两个值中的一个。它的语法如下: 1
| %result = select i1 , ,
|
其中,是要测试的条件,是条件为真时返回的值,是条件为假时返回的值。下面是一个示例: 1
2
3
4
5
| define i32 @test(i32 %a, i32 %b) {
%cmp = icmp slt i32 %a, %b
%result = select i1 %cmp, i32 %a, i32 %b
ret i32 %result
}
|
在这个示例中,我们定义了一个函数test,它的功能是比较a和b的值,并返回一个结果。我们使用icmp指令将a和b进行比较,并将比较结果存储在%cmp中。然后,我们使用select指令根据` 3.callcall指令用于调用函数。它的语法如下: 其中,是函数返回值的类型,是要调用的函数的名称,是函数参数的列表。下面是一个示例: 1
2
3
4
5
6
7
| declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b) {
%sum = add i32 %a, %b
%format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
call i32 (i8*, ...) @printf(i8* %format_str, i32 %sum)
ret i32 %sum
}
|
在这个示例中,我们首先使用add指令计算a+b的值,然后使用getelementptr指令获取全局字符串@.str的指针,该字符串包含格式化字符串%d。最后,我们使用call指令调用函数printf,将%format_str和%sum作为参数传递给它。 4.va_argva_arg指令用于在变参函数中获取下一个参数。它的语法如下: 其中,是参数类型的指针,是包含参数列表的指针。下面是一个示例: 1
2
3
4
5
6
7
8
9
10
11
12
| declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b, ...) {
%ap = alloca i8*, i32 0
store i8* %0, i8** %ap
%sum = add i32 %a, %b
%format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
%next_arg = va_arg i32*, i8** %ap
%value = load i32, i32* %next_arg
%product = mul i32 %sum, %value
call i32 (i8*, ...) @printf(i8* %format_str, i32 %product)
ret i32 %sum
}
|
在这个示例中,我们定义了一个变参函数test,它接受两个整数参数a和b,以及一个不定数量的其他参数。我们首先使用alloca指令在栈上分配一个指针,用于存储下一个参数的地址。然后,我们使用store指令将第一个参数的地址存储在该指针中。接下来,我们使用va_arg指令获取下一个参数的地址,然后使用load指令将该参数的值加载到%value中。最后,我们使用mul指令计算(%a + %b) * %value的值,并使用printf函数将其输出到标准输出。 5.landingpadlandingpad指令用于实现异常处理。它的语法如下: 其中,是异常处理函数返回值的类型。下面是一个示例: 1
2
3
4
5
6
7
8
| declare void @llvm.landingpad(i8*, i32)
define void @test() personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
%exn = landingpad i8*
%exn_val = extractvalue { i8*, i32 } %exn, 0
%exn_selector = extractvalue { i8*, i32 } %exn, 1
call void @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str, i64 0, i64 0), i8* %exn_val, i32 %exn_selector)
resume { i8*, i32 } %exn
}
|
在这个示例中,我们定义了一个函数test,它实现了异常处理。首先,我们使用personality关键字指定异常处理函数__gxx_personality_v0,它是GCC C++异常处理库的一部分。然后,我们使用landingpad指令获取异常对象和异常类型编号。我们使用extractvalue指令将其拆分为异常对象和异常类型编号,并将它们传递给printf函数。最后,我们使用resume指令重新抛出异常。
这里的异常处理机制是比较复杂的,需要更加深入的了解,这里不再赘述。 参考资料
|