笔者专注于Android安全范畴, 欢迎重视个人的微信大众号《Android安全工程》(可点击进行扫码重视)。个人微信大众号首要围绕 Android 应用的安全防护和逆向剖析, 分享各种安全攻防手段、Hook 技能、ARM 汇编等 Android 相关的知识,欢迎重视笔者的微信大众号,让咱们一起来见证Android大牛的崛起~

主张:本文内容较多,主张收藏起来,后边有需求的时候能够当备检手册运用即可。一般IR指令只需求知晓有某个指令,不需求花时间专门去背记。

概述

IR 指令是 LLVM 中的一个中心表明方法,用于表明程序的操控流、数据流、内存拜访等等,它是一种依据 SSA 方法(Static Single Assignment)的静态单赋值方法。在 LLVM 中,每个 IR 指令都有一个仅有的操作码(opcode),用于标识该指令的类型,每个操作码对应了一组或许的操作数(operands),这些操作数能够是常量、寄存器或许其他指令的成果。

在 LLVM 的 IR 中,一切的数据类型都是依据 LLVM 类型体系界说的,这些数据类型包括整数、浮点数、指针、数组、结构体等等,每个数据类型都具有自己的属性,例如位宽、对齐方法等等。在 IR 中,每个值都有一个类型,这个类型能够被显式地指定,或许经过指令的操作数推导出来。

LLVM 的 IR 指令非常丰富,包括算术、逻辑、比较、转化、操控流等等,它们能够被用来表达复杂的程序结构,一起 IR 指令还能够经过 LLVM 的优化器进行优化,以生成高效的方针代码。

IR指令类型比较多,以下是一些常见的指令类型:

  1. 加减乘除指令:add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv 等
  2. 位运算指令:and, or, xor, shl, lshr, ashr 等
  3. 转化指令:trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast 等
  4. 内存指令:alloca, load, store, getelementptr 等
  5. 操控流指令:br, switch, ret, indirectbr, invoke, resume, unreachable 等
  6. 其他指令:phi, select, call, va_arg, landingpad 等

加减乘除指令

1.加法指令(add)

加法指令用于对两个数值进行相加。在 LLVM 中,加法指令的语法如下所示:

%result = add <type> <value1>, <value2>

其间,<type> 表明要进行加法运算的值的数据类型,能够是整数、浮点数等;<value1><value2> 别离表明相加的两个数,能够是常量、寄存器或许其他指令的成果。

在LLVM中,add指令的<type>参数指定了<value1><value2>的类型,一起也指定了<result>的类型。支撑的类型包括:

  • 整数类型:i1, i8, i16, i32, i64, i128等;
  • 浮点类型:half, float, double, fp128等;
  • 向量类型:<n x i8>, <n x i16>, <n x i32>等;
  • 指针类型:i8*, i32*, float*等;
  • 标签类型:metadata

例如,假如咱们想将两个整数相加并得到一个整数成果,能够运用以下指令:

%result = add i32 1, 2

这儿,<type>指定为i32<value1>为整数值1<value2>为整数值2<result>为整数类型i32。各种类型的内存空间巨细(以位为单位)如下:

  • 整数类型:i1占1位,i8占8位,i16占16位,i32占32位,i64占64位,i128占128位;
  • 浮点类型:half占16位,float占32位,double占64位,fp128占128位;
  • 向量类型:<n x i8>n * 8位,<n x i16>n * 16位,<n x i32>n * 32位等;
  • 指针类型:指针类型的巨细取决于运行时的操作体系和架构,例如在32位操作体系上,指针类型一般占4个字节(32位),在64位操作体系上,指针类型一般占8个字节(64位);
  • 标签类型:metadata类型一般占有与指针类型相同的空间;

需求留意的是,这儿只是给出了各种类型在LLVM中的默认巨细,实际上在运用LLVM IR时,开发者能够经过在类型后边加上数字来显式指定类型的巨细,例如,i16类型能够经过i16 123来表明一个16位整数值123

下面是一个加法指令的代码示例,将两个整数相加:

%x = add i32 2, 3

这个指令将常量 23 相加,成果保存到寄存器 %x 中。

除了常量之外,也能够运用寄存器或其他指令的成果作为加法指令的操作数,例如:

%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)

减法指令用于对两个数值进行相减,语法为:

%result = sub <type> <value1>, <value2>

其间,<type> 表明要进行减法运算的值的数据类型,能够是整数、浮点数等;<value1><value2> 别离表明相减的两个数,能够是常量、寄存器或许其他指令的成果。

下面是一个减法指令的代码示例,将两个整数相减:

%diff = sub i32 %x, %y

这个指令将寄存器 %x 中的值减去 %y 中的值,成果保存到寄存器 %diff 中。

减法指令还有一种方法,能够用于核算两个浮点数之间的差值。语法为:

%result = fsub <type> <value1>, <value2>

其间,<type> 表明要进行减法运算的值的数据类型,有必要是浮点数类型;<value1><value2> 别离表明相减的两个数,能够是常量、寄存器或许其他指令的成果。

下面是一个浮点数减法指令的代码示例,将两个单精度浮点数相减:

%diff = fsub float %x, %y

这个指令将寄存器 %x 中的单精度浮点数减去 %y 中的单精度浮点数,成果保存到寄存器 %diff 中。

3. 乘法指令(mul)

乘法指令用于对两个数值进行相乘,语法为:

%result = mul <type> <value1>, <value2>

其间,<type> 表明要进行乘法运算的值的数据类型,能够是整数、浮点数等;<value1><value2> 别离表明相乘的两个数,能够是常量、寄存器或许其他指令的成果。

下面是一个乘法指令的代码示例,将两个整数相乘:

%prod = mul i32 %x, %y

这个指令将寄存器 %x%y 中的值相乘,成果保存到寄存器 %prod 中。咱们还能够对浮点数进行乘法操作,如下所示:

%result = mul double %value1, %value2

这个指令将寄存器 %value1%value2 中的值相乘,成果保存到 %result 中。需求留意的是,关于浮点数的乘法操作,需求运用 doublefloat 等浮点类型。

此外,LLVM 还供给了一些其他类型的乘法指令,例如向量乘法指令、无符号整数乘法指令等。具体的指令运用方法能够参考 LLVM 的官方文档。

4.除法指令(div)

除法指令用于对两个数值进行相除,语法为:

%result = <s/u>div <type> <value1>, <value2>

其间, 表明要履行有符号(`sdiv`)还是无符号(`udiv`)的除法运算; 表明要进行除法运算的值的数据类型,能够是整数、浮点数等; 别离表明相除的两个数,能够是常量、寄存器或许其他指令的成果。

下面是一个除法指令的代码示例,将两个整数相除:

%quot = sdiv i32 %x, %y

这个指令将寄存器 %x 中的值除以 %y 中的值,成果保存到寄存器 %quot 中。因为运用了 sdiv 指令,因而进行的是有符号除法运算。

假如要进行无符号除法运算,能够运用 udiv 指令:

%quot = udiv i32 %x, %y

这个指令将寄存器 %x 中的值除以 %y 中的值,成果保存到寄存器 %quot 中。因为运用了 udiv 指令,因而进行的是无符号除法运算。

位运算指令

IR有多种位运算指令,包括位与(and)、位或(or)、位异或(xor)、位取反(not)等。这些指令能够对整数类型进行按位操作,并将成果存储到一个新的寄存器中。以下是 IR 中常见的位运算指令及其作用:

  1. 位与(and):将两个整数的二进制表明进行按位与操作。
  2. 位或(or):将两个整数的二进制表明进行按位或操作。
  3. 位异或(xor):将两个整数的二进制表明进行按位异或操作。
  4. 位取反(not):将一个整数的二进制表明进行按位取反操作。

这些指令都能够用类似的语法进行运用,其间 <type> 表明要进行位运算的整数的数据类型,能够是 i1、i8、i16、i32、i64 等;<value1><value2> 别离表明要进行位运算的整数,能够是常量、寄存器或其他指令的成果。例如:

%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 中。

转化指令

  1. trunc: 将一个整数或浮点数切断为比本来小的位数,即去掉高位的一些二进制位。
  2. zext: 将一个整数或布尔值的位数添加,新位数的高位都填充为零,即进行零扩展。
  3. sext: 将一个整数的位数添加,新位数的高位都填充为原有的最高位,即进行符号扩展。
  4. fptrunc: 将一个浮点数切断为比本来小的位数,即去掉高位的一些二进制位。这是一种舍入操作,或许会丢掉一些精度。
  5. fpext: 将一个浮点数的位数添加,新位数的高位都填充为零,即进行浮点零扩展。
  6. fptoui: 将一个浮点数转化为一个无符号整数。假如浮点数是负数,则成果为零。
  7. fptosi: 将一个浮点数转化为一个带符号整数。假如浮点数是负数,则成果为负的最小整数。
  8. uitofp: 将一个无符号整数转化为一个浮点数。
  9. sitofp: 将一个带符号整数转化为一个浮点数。
  10. ptrtoint: 将一个指针类型转化为一个整数类型。该指令一般用于将指针转化为整数进行核算。
  11. inttoptr: 将一个整数类型转化为一个指针类型。该指令一般用于将整数转化为指针进行内存地址核算。
  12. bitcast: 将一个值从一种类型转化为另一种类型,可是这些类型有必要具有相同的位数。这个指令能够用来完成底层内存操作,例如将浮点数转化为整数以进行位运算。

下面是 IR 转化指令的详细的运用阐明和示例:

1.trunc

trunc指令将一个整数或浮点数切断为比本来小的位数,即去掉高位的一些二进制位。trunc指令的运用格局如下:

%result = trunc <source type> <value> to <destination type>

其间,<source type><destination type>别离表明源类型和方针类型,<value>表明要转化的值。例如,下面的代码将一个64位整数切断为32位整数:

%long = add i64 1, 2
%short = trunc i64 %long to i32

在这个比如中,%long是一个64位整数,它的值是3(1+2)。%short是一个32位整数,它的值是3。因为%long被切断为32位整数,因而只要低32位的值保存下来。

2.zext

zext指令将一个整数或布尔值的位数添加,新位数的高位都填充为零,即进行零扩展。zext指令的运用格局如下:

%result = zext <source type> <value> to <destination type>

例如,下面的代码将一个8位整数扩展为16位整数:

%short = add i8 1, 2
%long = zext i8 %short to i16

在这个比如中,%short是一个8位整数,它的值是3(1+2)。%long是一个16位整数,它的值是3。因为%short被扩展为16位整数,因而高8位被填充为零。

3.sext

sext指令将一个整数的位数添加,新位数的高位都填充为原有的最高位,即进行符号扩展。sext指令的运用格局与zext指令类似:

%result = sext <source type> <value> to <destination type>

例如,下面的代码将一个8位整数扩展为16位整数:

%short = add i8 -1, 2
%long = sext i8 %short to i16

在这个比如中,%short是一个8位整数,它的值是1-2=-1。%long是一个16位整数,它的值是0xffff。因为%short被扩展为16位整数,因而高8位都被填充为1。

4.fptrunc

fptrunc指令将一个浮点数切断为比本来小的位数,即去掉高位的一些二进制位。fptrunc指令的运用格局如下:

%result = fptrunc <source type> <value> to <destination type>

例如,下面的代码将一个双精度浮点数切断为单精度浮点数:

%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.fpext

fpext指令将一个浮点数扩展为比本来大的位数,新位数的高位都填充为零。fpext指令的运用格局与fptrunc指令类似:

%result = fpext <source type> <value> to <destination type>

例如,下面的代码将一个单精度浮点数扩展为双精度浮点数:

%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.fptoui

fptoui指令将一个浮点数转化为一个无符号整数。转化时,假如浮点数的值为负数,则成果为0。fptoui指令的运用格局如下:

%result = fptoui <source type> <value> to <destination type>

例如,下面的代码将一个双精度浮点数转化为32位无符号整数:

%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.fptosi

fptosi指令将一个浮点数转化为一个带符号整数。转化时,假如浮点数的值超出了方针类型的表明规模,则成果为该类型的最小值或最大值。fptosi指令的运用格局如下:

%result = fptosi <source type> <value> to <destination type>

例如,下面的代码将一个双精度浮点数转化为32位带符号整数:

%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.uitofp

uitofp指令将一个无符号整数转化为一个浮点数。uitofp指令的运用格局如下:

%result = uitofp <source type> <value> to <destination type>

例如,下面的代码将一个32位无符号整数转化为单精度浮点数:

%uint = add i32 1, 2
%float = uitofp i32 %uint to float

在这个比如中,%uint是一个32位无符号整数,它的值是3。%float是一个单精度浮点数,它的值是3.0。因为%uint的值为正数,因而能够转化为单精度浮点数。

9.sitofp

sitofp指令将一个带符号整数转化为一个浮点数。sitofp指令的运用格局如下:

%result = sitofp <source type> <value> to <destination type>

例如,下面的代码将一个32位带符号整数转化为单精度浮点数:

%i32 = add i32 1, -2
%float = sitofp i32 %i32 to float

在这个比如中,%i32是一个32位带符号整数,它的值是-1。%float是一个单精度浮点数,它的值是-1.0。因为%i32的值为负数,因而能够转化为单精度浮点数。

10.ptrtoint

ptrtoint指令将一个指针类型转化为一个整数类型。ptrtoint指令的运用格局如下:

%result = ptrtoint <source type> <value> to <destination type>

例如,下面的代码将一个指针类型转化为64位整数类型:

%ptr = alloca i32
%i64 = ptrtoint i32* %ptr to i64

在这个比如中,%ptr是一个指向32位整数类型的指针。%i64是一个64位整数类型,它的值是指针%ptr的地址。因为指针类型和整数类型的位宽不同,因而需求运用ptrtoint指令进行类型转化。

11.inttoptr

inttoptr指令将一个整数类型转化为一个指针类型。inttoptr指令的运用格局如下:

%result = inttoptr <source type> <value> to <destination type>

例如,下面的代码将一个64位整数类型转化为指向32位整数类型的指针:

%i64 = add i64 1, 2
%ptr = inttoptr i64 %i64 to i32*

在这个比如中,%i64是一个64位整数类型,它的值是3。%ptr是一个指向32位整数类型的指针,它的值是3。因为整数类型和指针类型的位宽不同,因而需求运用inttoptr指令进行类型转化。

12.bitcast

bitcast指令将一个值的位表明转化为另一个类型的位表明,可是它不会改变值自身。bitcast指令的运用格局如下:

%result = bitcast <source type> <value> to <destination type>

例如,下面的代码将一个64位双精度浮点数转化为64位整数类型:

%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供给了一些常见的内存指令,包括allocaloadstoregetelementptrmallocfreememsetmemcpymemmove等。这些指令能够用于内存分配、初始化和复制操作。下面将对这些指令逐一进行介绍,并供给相应的代码示例。

1.alloca

alloca指令用于在栈上分配内存,并回来一个指向新分配的内存的指针。alloca指令的运用格局如下:

%ptr = alloca <type>

其间,<type>是要分配的内存块的类型。例如,下面的代码分配一个包括5个整数的数组:

%array = alloca [5 x i32]

2.load

load指令用于从内存中读取数据,并将其加载到寄存器中。load指令的运用格局如下:

%val = load <type>* <ptr>

其间,<type>是要读取的数据的类型,<ptr>是指向要读取数据的内存块的指针。例如,下面的代码将一个整数数组的第一个元素加载到寄存器中:

%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.store

store指令用于将数据从寄存器中写入内存。store指令的运用格局如下:

store <type> <val>, <type>* <ptr>

其间,<type>是要写入的数据的类型,<val>是要写入的数据的值,<ptr>是指向要写入数据的内存块的指针。例如,下面的代码将一个整数存储到一个整数数组的第一个元素中:

%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.getelementptr

getelementptr指令用于核算指针的偏移量,以便拜访内存中的数据。getelementptr指令的运用格局如下:

%ptr = getelementptr <type>, <type>* <ptr>, <index type> <idx>, ...

其间,<type>是指针指向的数据类型,<ptr>是指向数据的指针,<index type>是索引的类型,<idx>是索引的值。getelementptr指令能够承受多个索引,每个索引都能够是任意类型的。索引类型有必要是整数类型,用于核算偏移量。例如,下面的代码核算一个二维数组中的一个元素的指针:

%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.malloc

malloc指令用于在堆上分配内存,并回来一个指向新分配的内存的指针。malloc指令的运用格局如下:

%ptr = call i8* @malloc(i64 <size>)

其间,<size>是要分配的内存块的巨细。例如,下面的代码分配一个包括10个整数的数组:

%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.free

free指令用于开释之前经过malloc指令分配的内存。free指令的运用格局如下:

call void @free(i8* <ptr>)

其间,<ptr>是指向要开释的内存块的指针。例如,下面的代码开释之前分配的整数数组:

%ptr = bitcast i32* %array to i8*
call void @free(i8* %ptr)

在这个比如中,%array是之前经过malloc指令分配的整数数组的指针,bitcast指令将%array指针转化为i8*类型的指针,call指令调用free函数开释内存。

7.memset

memset指令用于将一段内存区域的内容设置为指定的值。它的根本语法如下:

call void @llvm.memset.p0i8.i64(i8* %dst, i8 %val, i64 %size, i1 0)

其间,第一个参数%dst是要设置的内存区域的开始地址,它应该是指针类型。第二个参数%val是要设置的值,它应该是整型。第三个参数%size是内存区域的巨细,它应该是64位整型。最终一个参数是一个布尔值,表明对齐方法。假如它为1,表明依照指针类型对齐;假如它为0,表明不依照指针类型对齐。

下面是一个简略的运用示例,将一个整型数组中的一切元素都设置为0:

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.memcpy

memcpy指令用于将一个内存区域的内容复制到另一个内存区域。它的根本语法如下:

call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 %size, i1 0)

其间,第一个参数%dst是方针内存区域的开始地址,它应该是指针类型。第二个参数%src是源内存区域的开始地址,它应该是指针类型。第三个参数%size是内存区域的巨细,它应该是64位整型。最终一个参数是一个布尔值,表明对齐方法。假如它为1,表明依照指针类型对齐;假如它为0,表明不依照指针类型对齐。

下面是一个简略的运用示例,将一个整型数组复制到另一个数组中:

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.memmove

memmove指令用于将源地址指向的内存块中的数据移动到方针地址指向的内存块中,其界说如下:

declare void @llvm.memmove.p0i8.p0i8.i32(i8* nocapture, i8* nocapture, i32, i32, i1)

该指令承受五个参数,别离是方针地址、源地址、拷贝的字节数、对齐方法、以及是否进行堆叠查看的标志。其间,对齐方法参数表明内存块的对齐方法,假如不需求对齐则设为 1。假如进行堆叠查看,需求将标志设为 true,不然设为 false。

下面是一个运用memmove指令将源地址指向的内存块中的数据移动到方针地址指向的内存块中的示例:

%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*类型的指针。

操控流指令

操控流指令包括以下指令:

  1. br:条件分支指令,依据条件跳转到指定的根本块。

  2. switch:多路分支指令,依据输入值跳转到不同的根本块。

  3. ret:函数回来指令,回来到调用函数的地方。

  4. indirectbr:直接分支指令,跳转到存储在指定地址中的根本块。

  5. invoke:调用指令,调用带反常处理的函数,并在反常产生时传递操控权。

  6. resume:反常康复指令,康复在调用invoke指令时产生的反常。

  7. unreachable:不行抵达指令,表明程序不应该履行到该点,假如履行到该点会导致未界说行为。

这些指令在LLVM IR中用于操控程序的流程,支撑高级优化技能,如操控流剖析和依据SSA方法的变量重命名。LLVM的操控流指令包括条件分支指令br、多路分支指令switch、函数回来指令ret、直接分支指令indirectbr、调用指令invoke、反常康复指令resume和不行抵达指令unreachable。下面将逐一对这些指令进行阐明并给出相应的代码示例。

1.条件分支指令(br)

br指令用于履行条件分支,依据条件跳转到不同的根本块。它的语法如下:

br i1 <cond>, label <iftrue>, label <iffalse>

其间<cond>是条件值,假如其值为真,则跳转到符号为<iftrue>的根本块;不然跳转到符号为<iffalse>的根本块。下面是一个简略的示例:

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指令用于履行多路分支,依据输入值跳转到不同的根本块。它的语法如下:

switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]

其间,<intty>是整数类型,<value>是输入值,<defaultdest>是默认跳转的根本块。后边的每对<val>, <dest>都表明一个选项,假如<value>等于<val>,则跳转到<dest>符号的根本块。下面是一个示例:

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指令用于从函数中回来一个值。它的语法如下:

ret <type> <value>

其间,<type>是回来值的类型,<value>是回来的值。假如函数没有回来值,则<type>应该是void。下面是一个示例:

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指令用于依据直接地址跳转到不同的根本块。它的语法如下:

indirectbr <type> <address>, [ label <dest1>, label <dest2>, ... ]

其间,<type>是跳转方针的类型,<address>是指向跳转方针地址的指针。后边的每个<dest>表明一个跳转方针根本块的符号。下面是一个示例:

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指令类似,可是多了一个反常处理分支。下面是一个示例:

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指令用于从反常处理块中康复履行。它的语法如下:

resume <type> <value>

其间,<type>是要康复的反常类型,<value>是反常值。下面是一个示例:

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指令用于表明程序不会履行到这儿。它的语法如下:

unreachable

下面是一个示例:

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.phi

phi指令用于在根本块之间传递值。它的语法如下:

%result = phi <type> [ <value1>, <label1> ], [ <value2>, <label2> ], ...

其间,<type>是要传递的值的类型,<value1>是要传递的第一个值,<label1>是要从中传递第一个值的根本块。其他的<value><label>对也类似。下面是一个示例:

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,它的功能是比较ab的值,并回来一个成果。在符号%if_true处,咱们运用add指令核算a+1的值;在符号%if_false处,咱们运用add指令核算b+1的值。然后,在符号%merge处,咱们运用phi指令挑选一个值。具体来说,假如%cmp的值为true,咱们就挑选%result1的值(即a+1);不然,咱们就挑选%result2的值(即b+1)。

2.select

select指令用于依据条件挑选两个值中的一个。它的语法如下:

%result = select i1 <cond>, <type> <iftrue>, <type> <iffalse>

其间,<cond>是要测试的条件,<iftrue>是条件为真时回来的值,<iffalse>是条件为假时回来的值。下面是一个示例:

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,它的功能是比较ab的值,并回来一个成果。咱们运用icmp指令将ab进行比较,并将比较成果存储在%cmp中。然后,咱们运用select指令依据`

3.call

call指令用于调用函数。它的语法如下:

%result = call <type> <function>(<argument list>)

其间,<type>是函数回来值的类型,<function>是要调用的函数的称号,<argument list>是函数参数的列表。下面是一个示例:

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\n。最终,咱们运用call指令调用函数printf,将%format_str%sum作为参数传递给它。

4.va_arg

va_arg指令用于在变参函数中获取下一个参数。它的语法如下:

%result = va_arg <type*> <ap>

其间,<type*>是参数类型的指针,<ap>是包括参数列表的指针。下面是一个示例:

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,它承受两个整数参数ab,以及一个不定数量的其他参数。咱们首要运用alloca指令在栈上分配一个指针,用于存储下一个参数的地址。然后,咱们运用store指令将第一个参数的地址存储在该指针中。接下来,咱们运用va_arg指令获取下一个参数的地址,然后运用load指令将该参数的值加载到%value中。最终,咱们运用mul指令核算(%a + %b) * %value的值,并运用printf函数将其输出到标准输出。

5.landingpad

landingpad指令用于完成反常处理。它的语法如下:

%result = landingpad <type>

其间,<type>是反常处理函数回来值的类型。下面是一个示例:

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指令重新抛出反常。

这儿的反常处理机制是比较复杂的,需求愈加深化的了解,这儿不再赘述。

参考资料

  1. LLVM Language Reference Manual — LLVM 17.0.0git documentation