LLVM IR常用语法
加减乘除指令
1.加法指令(add)
加法指令用于对两个数值进行相加。在 LLVM 中,加法指令的语法如下所示:
%result = add <type> <value1>, <value2>
例如,如果我们想将两个整数相加并得到一个整数结果,可以使用以下指令:
%result = add i32 1, 2
%x = add i32 2, 3
%x = add i32 %a, %b
%z = add i32 %x, %y
这里,<type>
指定为i32
,<value1>
为整数值1
,<value2>
为整数值2
,<result>
为整数类型i32
。各种类型的内存空间大小(以位为单位)如下:
在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 = fadd <type> <value1>, <value2>
2.减法指令(sub)
减法指令用于对两个数值进行相减,语法为:
%result = sub <type> <value1>, <value2>
其中,<type>
表示要进行减法运算的值的数据类型,可以是整数、浮点数等;<value1>
和 <value2>
分别表示相减的两个数,可以是常量、寄存器或者其他指令的结果。
下面是一个减法指令的代码示例,将两个整数相减:
%diff = sub i32 %x, %y
浮点减法
减法指令还有一种形式,可以用于计算两个浮点数之间的差值。语法为:
%result = fsub float <value1>, <value2>
3. 乘法指令(mul)
乘法指令用于对两个数值进行相乘,语法为:
%result = mul <type> <value1>, <value2>
下面是一个乘法指令的代码示例,将两个整数相乘:
%prod = mul i32 %x, %y
浮点乘法
减法指令还有一种形式,可以用于计算两个浮点数之间的差值。语法为:
%result = fmul float <value1>, <value2>
4.除法指令(div)
除法指令用于对两个数值进行相除,语法为:
%result = <u/s>div <type> <value1>, <value2>
其中,表示要执行有符号(sdiv)
还是无符号(udiv)
的除法运算;表示要进行除法运算的值的数据类型,可以是整数、浮点数等;和分别表示相除的两个数,可以是常量、寄存器或者其他指令的结果。
下面是一个除法指令的代码示例,将两个整数相除:
%quot = sdiv i32 %x, %y
如果要进行无符号除法运算,可以使用 udiv
指令:
%quot = udiv i32 %x, %y
浮点除法
<result> = fdiv <ty> <op1>, <op2>
fdiv
指令返回其两个操作数的商。示例:
%result = fdiv float 4.0, %var
模运算指令 (rem)
<result> = <u/s>rem <ty> <op1>, <op2>
urem
指令返回其两个参数的无符号除法的余数。srem
指令返回其两个操作数的有符号除法的余数。使用该指令的示例:
%result = urem i32 4, %var
%result = srem i32 4, %var
位运算指令
IR有多种位运算指令,包括位与(and)、位或(or)、位异或(xor)、位取反(not)等。这些指令可以对整数类型进行按位操作,并将结果存储到一个新的寄存器中。以下是 IR 中常见的位运算指令及其作用:
这些指令都可以用类似的语法进行使用,其中 <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
shl指令
shl
指令是左移运算符,将op1
向左按位移动opt2
位。如果opt2
的位数大于或等于opt1
的位数。
<result> = lshr <ty> <op1>, <op2> ; yields ty:result
<result> = lshr exact <ty> <op1>, <op2> ; yields ty:result
下面是例子:
<result> = shl i32 4, %var ; yields i32: 4 << %var
<result> = shl i32 4, 2 ; yields i32: 16
<result> = shl i32 1, 10 ; yields i32: 1024
<result> = shl i32 1, 32 ; undefined
lshr指令
lshr
指令是右移运算符,将opt2
向右按位移动opt1
位。如果opt2
的位数大于或等于opt1
的位数。
<result> = lshr <ty> <op1>, <op2> ; yields ty:result
<result> = lshr exact <ty> <op1>, <op2> ; yields ty:result
下面是例子:
<result> = lshr i32 4, 1 ; yields i32:result = 2
<result> = lshr i32 4, 2 ; yields i32:result = 1
<result> = lshr i8 4, 3 ; yields i8:result = 0
<result> = lshr i8 -2, 1 ; yields i8:result = 0x7F
<result> = lshr i32 1, 32 ; undefined
ashr指令
ashr
指令是算数右移运算符,将opt2
向右按位移动opt1
位。如果opt2
的位数大于或等于opt1
的位数。
<result> = ashr <ty> <op1>, <op2> ; yields ty:result
<result> = ashr exact <ty> <op1>, <op2> ; yields ty:result
这里注意区分算数右移和逻辑右移的区别,算术右移在最高位要补符号位的,而逻辑右移最高位都是零。下面是例子:
<result> = ashr i32 4, 1 ; yields i32:result = 2
<result> = ashr i32 4, 2 ; yields i32:result = 1
<result> = ashr i8 4, 3 ; yields i8:result = 0
<result> = ashr i8 -2, 1 ; yields i8:result = -1
<result> = ashr i32 1, 32 ; undefined
转换指令
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
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
4.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
5.sitofp
sitofp
指令将一个带符号整数转换为一个浮点数。sitofp
指令的使用格式如下:
%result = sitofp <source type> <value> to <destination type>
例如,下面的代码将一个32位带符号整数转换为单精度浮点数:
%i32 = add i32 1, -2
%float = sitofp i32 %i32 to float
6.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
内存指令
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
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
4.getelementptr
getelementptr
指令用于计算指针的偏移量,以便访问内存中的数据。getelementptr
指令的使用格式如下:
%ptr = getelementptr <type>, <type>* <ptr>, <index type> <idx>, ...
其中,<type>
是指针指向的数据类型,<ptr>
是指向数据的指针,<index type>
是索引的类型,<idx>
是索引的值。getelementptr
指令可以接受多个索引,每个索引都可以是任意类型的。索引类型必须是整数类型,用于计算偏移量。例如,下面的代码计算一个二维数组中的一个元素的指针:g
%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.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
}
控制指令
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
}
2.函数返回指令 (ret)
ret
指令用于从函数中返回一个值。它的语法如下:
ret <type> <value>
其中,<type>
是返回值的类型,<value>
是返回的值。如果函数没有返回值,则<type>
应该是void
。下面是一个示例:
define i32 @test(i32 %a, i32 %b) {
%sum = add i32 %a, %b
ret i32 %sum
}
3.置反指令 (fneg)
<result> = fneg [fast-math flags]* <ty> <op1>
fneg
指令返回其操作数的否定值。例子:
%result = fneg float %val
其他指令
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
,它的功能是比较a
和b
的值,并返回一个结果。在标记%if_true
处,我们使用add
指令计算a+1
的值;在标记%if_false
处,我们使用add
指令计算b+1
的值。然后,在标记%merge
处,我们使用phi
指令选择一个值。具体来说,如果%cmp
的值为true
,我们就选择%result1
的值(即a+1
);否则,我们就选择%result2
的值(即b+1
)。
2.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
}
3.icmp和fcmp
icmp
指令的操作数类型是整型或整型向量、指针或指针向量。对于指针或指针向量,在做比较运算的时候,都会将其指向的地址值作为整型数值去比较,所以归根结底也还是整型。
fcmp
指令要求操作数是浮点值或者浮点向量,这个没有指针类型。
<result> = icmp <cond> <ty> <op1>, <op2> ; yields i1 or <N x i1>:result
<result> = fcmp <cond> <ty> <op1>, <op2> ; yields i1 or <N x i1>:result
<cond>
这是比较规则,icmp
和fcmp
指令的比较规则不一样。
icmp
比较规则:
eq
:相等ne
:不等于ugt
:无符号大于uge
:无符号大于或等于ult
:无符号小于ule
:无符号小于或等于sgt
:有符号大于sge
:有符号大于或等于slt
:有符号小于sle
:有符号小于或等于
fcmp
比较规则:
false
:不比较,总是返回falseoeq
:有序且相等ogt
:有序且大于oge
:有序且大于或等于olt
:有序且小于ole
:有序且小于或等于one
:有序且不相等ord
:有序(无 nans)ueq
:无序或相等ugt
:无序或大于uge
:无序或大于等于ult
:无序或小于ule
:无序或小于等于une
:无序或不相等uno
:无序(nans)true
: 不比较,总是返回 true
有序意味着两个操作数都不是QNAN
,而无序意味着任何一个操作数都可能是QNAN
。
QNAN是一个计算机术语,表示不是一个数字(Not a Number),它是一种特殊的浮点数,用于表示未定义或不可表示的值,例如0除以0,负数的平方根,或者无穷大减去无穷大等。
下面是例子:
icmp:
<result> = icmp eq i32 4, 5 ; yields: result=false
<result> = icmp ne float* %X, %X ; yields: result=false
<result> = icmp ult i16 4, 5 ; yields: result=true
<result> = icmp sgt i16 4, 5 ; yields: result=false
<result> = icmp ule i16 -4, 5 ; yields: result=false
<result> = icmp sge i16 4, 5 ; yields: result=false
fcmp:
<result> = fcmp oeq float 4.0, 5.0 ; yields: result=false
<result> = fcmp one float 4.0, 5.0 ; yields: result=true
<result> = fcmp olt float 4.0, 5.0 ; yields: result=true
<result> = fcmp ueq double 1.0, 2.0 ; yields: result=false
函数语法
函数定义
在LLVM中,一个最基本的函数定义的样子我们之前已经遇到过多次,就是@main
函数的样子:
define <ty> @<name>(<param>, ...) {
...
[ret <op>]
}
在函数名之后可以加上参数列表,如:
define i32 @foo(i32 %a, i64 %b) {
ret i32 0
}
一个函数定义最基本的框架,就是返回值(i32
)+函数名(@foo
)+参数列表((i32 %a, i64 %b)
)+函数体({ ret i32 0 }
)。
除了函数定义之外,还有一种情况十分常见,那就是函数声明。我们在一个编译单元(模块)下,可以使用别的模块的函数,这时候就需要在本模块先声明这个函数,才能保证编译时不出错,从而在链接时正确将声明的函数与别的模块下其定义进行链接。
函数声明也相对比较简单,就是使用declare
关键词替换define
:
declare i32 @printf(i8*, ...)