背景图

LLVM语言参考手册(类型系统到其他值)

类型系统

LLVM类型系统是中间表示的最重要的特征之一。类型化使得可以直接对中间表示执行许多优化,而不必在转换之前在侧面进行额外的分析。强大的类型系统使读取生成的代码变得更加容易,并且支持新的分析和转换,这些分析和转换在普通的三个地址代码表示上不可行。

void类型

概述:void类型不代表任何值并且没有大小。

句法:

1
void

函数类型

概述: 函数类型可以被认为是函数签名。它由一个返回类型和一个形式参数类型列表组成。函数类型的返回类型是void类型或第一类类型 - 标签元数据类型除外。

句法:

1
<returntype> (<parameter list>)

…其中<parameter list>是逗号分隔的类型说明符列表。可选地,参数列表可以包括类型…,该类型指示该函数采用可变数目的参数。变量参数函数可以通过处理内部函数的变量参数来访问它们的参数。<returntype>是除标签元数据之外的任何类型。

例子:
i32 (i32) 函数接受i32,返回一个i32
float (i16, i32 *) * 指针,以接受一个函数i16和一个i32指针来返回float。
i32 (i8*, ...) 可变参数函数,它有至少一个指针到i8(在C中是char),它返回一个整数。LLVM中这是printf的签名。
{i32, i32} (i32) 一个函数i32,返回一个包含两个i32值的结构

第一类类型(First Class Types)

第一类的类型可能是最重要的。这些类型的值是唯一可以通过指令产生的类型。

单值类型

这些是从CodeGen的角度来看在寄存器中有效的类型。

整数类型

概述: 整数类型是一个非常简单的类型,它简单地为所需的整数类型指定一个任意的位宽。可以指定从1位到223-1(约8百万)的任何位宽。

句法:

1
iN

整数将占据的位数由该N值指定。

例子:
i1 一个单位整数。
i32 一个32位整数。
i1942652 一个超过100万位的大整数。

浮点类型

类型 描述
half 16位浮点值
float 32位浮点值
double 64位浮点值
fp128 128位浮点值(112位尾数)
x86_fp80 80位浮点值(X87)
ppc_fp128 128位浮点值(两个64位)

half,float,double和fp128的二进制格式分别对应于binary16,binary32,binary64和binary128的IEEE-754-2008规范。

X86_mmx类型

概述: x86_mmx类型表示在x86机器上的MMX寄存器中保存的值。允许的操作相当有限:参数和返回值,load和store以及bitcast。用户指定的MMX指令表示为具有参数and/or此类型结果的内部调用或asm调用。没有这种类型的数组、向量或常量。

句法:

1
x86_mmx

指针类型

概述: 指针类型用于指定内存位置。指针通常用于引用内存中的对象。

指针类型可能有一个可选的地址空间属性,用于定义指向对象所在的编号地址空间。默认地址空间是数字零。非零地址空间的语义是特定于目标的。

请注意,LLVM不允许指向void(void*)的指针,也不允许指向标签(label*)的指针。改为使用i8*

句法:

1
<type> *

例子:
[4 x i32]* 4个的i32值数组的指针。
i32 (i32*) * 函数指针,它接受一个i32*,并返回i32。
i32 addrspace(5)* i32值的指针,驻留在地址空间#5中的值。

矢量类型

概述: 矢量类型是表示元素矢量的简单派生类型。当使用单个指令(SIMD)并行操作多个原始数据时,使用矢量类型。矢量类型需要大小(元素数量)和基础原始数据类型。矢量类型被认为是第一类

句法:

1
< <# elements> x <elementtype> >

元素的数量是一个大于0的常数整数值; elementtype可以是任何整数、浮点或指针类型。大小为零的矢量是不允许的。

例子:
<4 x i32> 4个32位整数值的向量。
<8 x float> 8个32位浮点值的向量。
<2 x i64> 2个64位整数值的向量。
<4 x i64*> 4个64位整数值指针的向量。

标签类型

概述: 标签类型代表代码标签。
句法:

1
label

令牌(token)类型

概述: 当值与指令相关联时使用token类型,但该值的所有用法不得试图反思或模糊它。因此,具有phiselect类型令牌是不合适的。

句法:

1
token

元数据类型

概述: 元数据类型表示嵌入的元数据。除函数参数外,不得从元数据创建派生类型。

句法:

1
metadata

聚合类型

聚合类型是派生类型的一个子集,可以包含多个成员类型。数组结构是聚合类型。向量不被视为聚合类型。

数组类型

概述: 数组类型是一种非常简单的派生类型,它将元素按顺序排列在内存中。数组类型需要大小(元素数量)和基础数据类型。

句法:

1
[<# elements> x <elementtype>]

元素的数量是一个常数整数值; elementtype可以是任何尺寸的类型。

例子:
[40 x i32] 包含40个32位整数值的数组。
[41 x i32] 41个32位整数值的数组。
[4 x i8] 包含4个8位整数值的数组。
以下是多维数组的一些示例:
[3 x [4 x i32]] 3x4 32位整数值数组。
[12 x [10 x float]] 单精度浮点值的12×10数组。
[2 x [3 x [4 x i16]]] 2x3x4的16位整数值数组。

除了静态类型隐含的数组末尾之外,没有对索引的限制(尽管在某些情况下索引超出了分配对象的范围)。这意味着可以在零长度数组类型的LLVM中实现单维“可变大小数组”。例如,在LLVM中实现“pascal样式数组”可以使用类型“{ i32, [0 x float]}”。

结构类型

概述: 结构类型用于在内存中一起表示数据成员的集合。结构的元素可以是任何具有大小的类型。

使用’load‘和’store‘通过使用’getelementptr‘指令获取指向字段的指针来访问内存中的结构。使用’extractvalue‘和’insertvalue‘指令访问寄存器中的结构。

结构可以选择是“压缩”结构,其指示结构的对齐是一个字节,并且元素之间没有填充。在非压缩结构中,字段类型之间的填充按照DataLayout字符串在模块中定义的方式插入,该模块需要与基础代码生成器的预期匹配。

结构可以是“文字”或“识别符”。字面结构与其他类型(例如{i32, i32}*)内联定义,而标识类型始终在顶层使用名称定义。文字类型被其内容所独占,因为没有办法编写它们,所以永远不会递归或不透明。识别符的类型可以是递归的,可以是不透明的,并且永远不会被分离。

句法:

1
2
%T1 = type { <type list> }     ; Identified normal struct type
%T2 = type <{ <type list> }> ; Identified packed struct type

例子:
{ i32, i32, i32 } 三个i32值的结构
{ float, i32 (i32) * } 一对,其中第一个元素是a float,第二个元素是一个指向函数的指针,该函数接受一个i32返回值i32。
<{ i8, i32 }> 一个已知为5字节大小的打包结构。

不透明结构类型

概述: 不透明结构类型用于表示没有指定主体的命名结构类型。这符合(例如)正向声明结构的C概念。

句法:

1
2
%X = type opaque
%52 = type opaque

例子:
opaque 一种不透明的类型。

常量

LLVM有几种不同的基本类型的常量。本节介绍它们的全部和它们的语法。

简单常量

  • 布尔常量: 两个字符串’ true’和’ false’都是该i1类型的有效常量。
  • 整型常量: 标准整数(如’4’)是整数类型的常量 。负数可能与整数类型一起使用。
  • 浮点常量: 浮点常量使用标准十进制表示法(例如123.421),指数表示法(例如1.23421e + 2)或更精确的十六进制表示法(请参见下文)。汇编器需要浮点常量的精确十进制值。例如,汇编程序接受1.25,但拒绝1.3,因为1.3是二进制中的重复小数。浮点常量必须具有 浮点类型。
  • 空指针常量: 标识符’null‘被识别为空指针常量,并且必须是指针类型。
  • 令牌(Token)常量: 标识符’none’被识别为空的标记常量,并且必须是标记类型。

常量的一个非直观符号是浮点常量的十六进制形式。例如,形式为’double 0x432ff973cafa8000‘等同于(但难以阅读)’double 4.5e+15‘。唯一需要十六进制浮点常量(以及它们由反汇编程序生成的唯一时间)是必须发出浮点常量但不能用合理数目的十进制浮点数表示的浮点常量数字。例如,NaN'sinfinities和其他特殊值以IEEE十六进制格式表示,因此汇编和反汇编不会导致常量中的任何位发生更改。

当使用十六进制形式时,half,float和double类型的常量使用上面显示的16位数字形式表示(与IEEE754表示符合double); 然而,half和float值必须分别精确表示为IEEE 754的半精度和单精度。十六进制格式总是用于长双,并有三种形式的长双。x86使用的80位格式表示0xK后跟20个十六进制数字。PowerPC使用的128位格式(两个相邻的双精度) 0xM由32个十六进制数字表示。IEEE 128位格式0xL由32个十六进制数字表示。长双打只有在你的目标上的长双重格式匹配时才有效。IEEE 16位格式(半精度)由表示0xH 后跟4个十六进制数字。所有的十六进制格式都是big-endian(左边的符号位)。

没有x86_mmx类型的常量。

复杂(Complex)常量

复杂常量是简单常量和较小复常量的(可能递归)组合。

结构常数

结构常量用类似于结构类型定义的符号表示(逗号分隔的元素列表,用大括号({})括起来)。例如:“{ i32 4, float 17.0, i32* @G }”,其中“@G”被声明为“@G = external global i32”。结构常量必须具有结构类型,并且元素的数量和类型必须与该类型指定的类型匹配。

数组常量

数组常量用类似于数组类型定义的符号表示(逗号分隔的元素列表,用方括号([])括起来)。例如:“[ i32 42, i32 11, i32 74 ]”。数组常量必须具有数组类型,并且元素的数量和类型必须与该类型指定的数量和类型相匹配。作为一种特殊情况,字符数组常量也可以用 前缀表示为双引号字符串。例如:“c"Hello World\0A\00"”。

矢量(Vector)常量

向量常量用类似于向量类型定义的符号表示(逗号分隔的元素列表,由小于/大于((<>))围绕)。例如:“< i32 42, i32 11, i32 74, i32 100 >”。向量常量必须具有向量类型,并且元素的数量和类型必须与该类型指定的类型匹配。

零初始化

字符串“zeroinitializer‘可用于将零值初始化为任何类型的零,包括标量和聚合类型。这通常用于避免必须打印大型零初始化器(例如,用于大型数组),并且始终完全等同于使用显式零初始化器。

元数据节点

元数据节点是一个没有类型的常量元组。例如:“!{!0, !{!2, !0}, !"test"}”。元数据可以引用常量值,例如:“!{!0, i32 0, i8* @global, i64 (i64)* @function, !"str"}”。与其他类型化的常量不同,它们被解释为指令流的一部分,元数据是附加附加信息的地方,例如调试信息。

全局变量和函数地址

全局变量函数总是隐式有效(链接时间)常量。当使用全局标识符并且总是有指针类型时,这些常量被明确引用。例如,以下是合法的LLVM文件:

1
2
3
@X = global i32 17
@Y = global i32 42
@Z = global [2 x i32*] [ i32* @X, i32* @Y ]

未定义的值

字符串’undef‘可以用于任何需要常量的地方,并且表示该值的用户可能会收到未指定的位模式。未定义的值可以是任何类型(除了’label‘或’void‘),并且可以在任何允许常量的地方使用。

未定义的值非常有用,因为它们向编译器指出,无论使用什么值,该程序都已定义良好。这为编译器提供了更多的优化自由度。下面是一些有效的(可能令人惊讶的)转换的例子(在伪IR中):

1
2
3
4
5
6
7
  %A = add %X, undef
%B = sub %X, undef
%C = xor %X, undef
Safe:
%A = undef
%B = undef
%C = undef

这是安全的,因为所有的输出位都受undef位的影响。任何输出位都可以有一个零或一个依赖的输入位。

1
2
3
4
5
6
7
8
9
10
11
  %A = or %X, undef
%B = and %X, undef
Safe:
%A = -1
%B = 0
Safe:
%A = %X ;; By choosing undef as 0
%B = %X ;; By choosing undef as -1
Unsafe:
%A = undef
%B = undef

这些逻辑操作的位不总是受输入的影响。例如,如果%X有一个零位,那么“and‘操作的输出将始终为该位的零,而不管”undef‘ 的相应位是什么。因此,优化或假设and的结果是undef是不安全的。但是,假设undef的所有位都可以是0,并且将’and‘优化为0是安全的。同样,假设可以设置undefor操作的所有位是安全的,允许’or‘被折叠为-1。

1
2
3
4
5
6
7
8
9
10
11
  %A = select undef, %X, %Y
%B = select undef, 42, %Y
%C = select %X, %Y, undef
Safe:
%A = %X (or %Y)
%B = 42 (or %Y)
%C = %Y
Unsafe:
%A = undef
%B = undef
%C = undef

这组例子表明,未定义的’select‘(和条件分支)条件可以采取任何方式,但它们必须来自两个操作数中的一个。在%A例子中,如果%X%Y是两个已知具有明显的低位,那么%A就必须有一个清除低位。然而,在这个%C例子中,优化器被允许假设’undef‘操作可以是和%Y相同的,允许整个’select‘被消除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  %A = xor undef, undef

%B = undef
%C = xor %B, %B

%D = undef
%E = icmp slt %D, 4
%F = icmp gte %D, 4

Safe:
%A = undef
%B = undef
%C = undef
%D = undef
%E = undef
%F = undef

这个例子指出两个undef操作不一定相同。这对于人们来说可能是令人惊讶的(并且也匹配C语义),他们认为“X^X”总是零,即使 X未定义也是如此。由于多种原因,这是不正确的,但简单的答案是,一个undef“变量”可以在其“生存范围”内随意改变它的值。这是真的,因为这个变量实际上并没有生存范围。相反,该值是从任意寄存器中逻辑读取的,这些寄存器恰好在需要时发生变化,因此该值不一定随时间变化。事实上,%A%C需要有相同的语法或核心LLVM“全部替换与使用”的概念将不成立。

1
2
3
4
5
  %A = sdiv undef, %X
%B = sdiv %X, undef
Safe:
%A = 0
b: unreachable

这些示例显示了未定义的值 和未定义的行为之间的关键区别。一个未定义的值(如’undef‘)允许有一个任意的位模式。这意味着%A操作可以不断折叠为’0’,因为’undef‘可能为零,并且零除以任何值为零。但是,在第二个例子中,我们可以做一个更积极的假设:因为undef允许它是一个任意值,我们可以假设它可能为零。由于被零除以具有未定义的行为,我们被允许假设该操作根本不执行。这允许我们删除分割和所有代码。由于未定义的操作“不可能发生”,因此优化器可以假定它发生在死代码中。

1
2
3
4
5
a:  store undef -> %X
b: store %X -> undef
Safe:
a: <deleted>
b: unreachable

存储的未定义的值可以被假设为不具有任何影响; 我们可以假设这个值被恰好与已经存在的相匹配的位覆盖。然而,一个存储到一个未定义的位置可能破坏任意的内存,因此,它具有未定义行为。

毒药(Poison)值

毒性(Poison)值与undef值相似,但它们也表示这样的事实,即不能引起副作用的指令或常量表达式已经检测到导致未定义行为的条件。

目前在IR中无法表示毒物值; 它们只存在于某些操作的调用,如带有nsw标志的add操作。

毒药值行为是根据值依赖来定义的:

  • phi节点以外的值取决于它们的操作数。
  • Phi节点取决于对应于其动态前驱基本块的操作数。
  • 函数参数取决于其函数的动态调用者中相应的实际参数值。
  • 调用(call)指令取决于将控制动态传回给它们的ret指令。
  • 调用(invoke)指令取决于ret, resume或异常抛出调用指令,动态地将控制权交还给它们。
  • 非易失性加载和存储取决于所有引用的内存地址的最新存储,遵循IR中的命令(包括由`@llvm.memcpy`等内在函数隐含的加载和存储)。
  • 具有外部可见副作用的指令取决于最近的先前的指令,其具有外部可见的副作用,遵循IR中的顺序。(这包括易失性操作。)
  • 指令控制依赖于一个终止指令,如果终止子指令有多个后继者和指令总是被执行时控制转移到后继的一个,并且当控制被转移到另一个可以不执行。
  • 此外,指令也是控制 - 取决于终止指令,如果终止指令已将控制权转移给不同的后继者,则其所依赖的指令集将会不​​同。
  • 依赖性是传递性的。

Poison值具有与undef值相同的行为,另外的效果是任何依赖poison值的指令都具有未定义的行为。

这里有些例子:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
entry:
%poison = sub nuw i32 0, 1 ; Results in a poison value.
%still_poison = and i32 %poison, 0 ; 0, but also poison.
%poison_yet_again = getelementptr i32, i32* @h, i32 %still_poison
store i32 0, i32* %poison_yet_again ; memory at @h[0] is poisoned

store i32 %poison, i32* @g ; Poison value stored to memory.
%poison2 = load i32, i32* @g ; Poison value loaded back from memory.

store volatile i32 %poison, i32* @g ; External observation; undefined behavior.

%narrowaddr = bitcast i32* @g to i16*
%wideaddr = bitcast i32* @g to i64*
%poison3 = load i16, i16* %narrowaddr ; Returns a poison value.
%poison4 = load i64, i64* %wideaddr ; Returns a poison value.

%cmp = icmp slt i32 %poison, 0 ; Returns a poison value.
br i1 %cmp, label %true, label %end ; Branch to either destination.

true:
store volatile i32 0, i32* @g ; This is control-dependent on %cmp, so
; it has undefined behavior.
br label %end

end:
%p = phi i32 [ 0, %entry ], [ 1, %true ]
; Both edges into this PHI are
; control-dependent on %cmp, so this
; always results in a poison value.

store volatile i32 0, i32* @g ; This would depend on the store in %true
; if %cmp is true, or the store in %entry
; otherwise, so this is undefined behavior.

br i1 %cmp, label %second_true, label %second_end
; The same branch again, but this time the
; true block doesn't have side effects.

second_true:
; No side effects!
ret void

second_end:
store volatile i32 0, i32* @g ; This time, the instruction always depends
; on the store in %end. Also, it is
; control-equivalent to %end, so this is
; well-defined (ignoring earlier undefined
; behavior in this example).

基本块的地址

blockaddress(@function, %block)

在“blockaddress”常数计算在指定函数指定的基本块的地址,并总是有一个i8*类型。取出输入块的地址是非法的。

当用作’indirectbr‘指令的操作指令时,或者用于与空值进行比较时,该值仅具有已定义的行为。标签地址之间的指针相等测试会导致未定义的行为 - 但是,再次,与null进行比较是可以的,并且没有标签等于空指针。只要这些位未被检查,这可以作为不透明的指针大小值传递。ptrtoint只要原始值在indirectbr 指令之前重新构成,就允许和计算这些值。

最后,有些目标可能会在使用该值作为内联程序集的操作数时提供定义的语义,但这是目标特定的。

常量表达式

常量表达式用于允许涉及其他常量的表达式用作常量。常量表达式可以是任何第一类类型,并且可能涉及没有副作用的任何LLVM操作(例如,不支持加载和调用)。以下是常量表达式的语法:

  • trunc (CST to TYPE): 对常量执行trunc操作
  • zext (CST to TYPE): 对常量执行zext操作
  • sext (CST to TYPE): 对常量执行sext操作
  • fptrunc (CST to TYPE): 将浮点常量截断为另一个浮点类型。CST的大小必须大于TYPE的大小。这两种类型都必须是浮点型。
  • fpext (CST to TYPE): 浮点将常量扩展为另一种类型。CST的大小必须小于或等于TYPE的大小。这两种类型都必须是浮点型。
  • fptoui (CST to TYPE): 将浮点常量转换为相应的无符号整数常量。TYPE必须是标量或向量整数类型。CST必须是标量或向量浮点类型。CST和TYPE都必须是标量,或者具有相同数量元素的向量。如果该值不适合整数类型,则结果未定义。
  • fptosi (CST to TYPE): 将浮点常量转换为相应的有符号整数常量。TYPE必须是标量或向量整数类型。CST必须是标量或向量浮点类型。CST和TYPE都必须是标量,或者具有相同数量元素的向量。如果该值不适合整数类型,则结果未定义。
  • uitofp (CST to TYPE): 将无符号整数常量转换为相应的浮点常量。TYPE必须是标量或向量浮点类型。CST必须是标量或向量整数类型。CST和TYPE都必须是标量,或者具有相同数量元素的向量。如果该值不适合浮点类型,则结果未定义。
  • sitofp (CST to TYPE): 将有符号整数常量转换为相应的浮点常量。TYPE必须是标量或向量浮点类型。CST必须是标量或向量整数类型。CST和TYPE都必须是标量,或者具有相同数量元素的向量。如果该值不适合浮点类型,则结果未定义。
  • ptrtoint (CST to TYPE): 对常量执行ptrtoint操作
  • inttoptr (CST to TYPE): 对常量执行inttoptr操作。这个真的很危险!
  • bitcast (CST to TYPE): 将常数CST转换为另一个TYPE。操作数的限制与bitcast指令的限制相同 。
  • addrspacecast (CST to TYPE): 将指针CST的常量指针或常量向量转换为另一个地址空间中的另一个TYPE。操作数的约束与addrspacecast指令的约束相同。
  • getelementptr (TY, CSTPTR, IDX0, IDX1, ...), getelementptr inbounds (TY, CSTPTR, IDX0, IDX1, ...): 对常量执行getelementptr操作。与getelementptr 指令一样,索引列表可能有一个或多个索引,这些索引对于“指向TY的指针”类型是有意义的。
  • select (COND, VAL1, VAL2): 对常量执行选择操作
  • icmp COND (VAL1, VAL2): 对常量执行icmp操作
  • fcmp COND (VAL1, VAL2): 对常量执行fcmp操作
  • extractelement (VAL, IDX): 对常量执行extractelement操作
  • insertelement (VAL, ELT, IDX): 对常量执行insertelement操作
  • shufflevector (VEC1, VEC2, IDXMASK): 对常量执行shufflevector操作
  • extractvalue (VAL, IDX0, IDX1, ...): 对常量执行extractvalue操作。索引列表的解释方式与“getelementptr”操作中的索引类似。至少必须指定一个索引值。
  • insertvalue (VAL, ELT, IDX0, IDX1, ...): 对常量执行insertvalue操作。索引列表的解释方式与“getelementptr”操作中的索引类似。至少必须指定一个索引值。
  • OPCODE (LHS, RHS): 执行LHS和RHS常量的指定操作。操作码可以是任何二进制按位二进制操作。操作数的限制与相应指令的限制相同(例如,不允许对浮点值进行按位操作)。

其他值

内嵌汇编表达式

LLVM 通过使用特殊值支持内联汇编表达式(与模块级内联汇编相反)。该值表示内联汇编程序作为模板字符串(包含要发出的指令),操作数约束列表(存储为字符串),指示内联asm表达式是否具有副作用的标志以及指示是否包含asm的函数需要保守地调整堆栈。

模板字符串支持使用$后跟一个数字的参数替换,以指示由约束字符串指定的给定寄存器/内存位置的替换。“${NUM:MODIFIER}”也可以使用,其中MODIFIER是如何打印操作数的特定于目标的注释(请参阅Asm模板参数修饰符)。

字符$可以在模板中使用“$$”。要在输出中包含其他特殊字符,可以使用通常的“\XX”转义符,就像在其他字符串中一样。请注意,在模板替换之后,生成的汇编字符串将由LLVM的集成汇编器进行分析,除非它被禁用 - 即使发出.s文件 - 也必须包含LLVM已知的汇编语法。

LLVM还支持一些有用的内联汇编代码:

  • ${:uid}:扩展为这个内联汇编blob唯一的十进制整数。在声明本地标签时,这种替换很有用。许多标准的编译器优化(如内联)可能会复制内联asm blob。添加blob唯一标识符可确保这两个标签在装配过程中不会发生冲突。这用于实现GCC的%=特殊格式字符串
  • ${:comment}:扩展为当前目标的汇编方言的注释字符。这通常是#,但很多指标使用其他字符串,例如;//!
  • ${:private}:扩展为汇编器专用标签前缀。带有此前缀的标签不会出现在组装对象的符号表中。通常前缀是L,但目标可能使用其他字符串。.L是比较受欢迎的。

LLVM对inline asm的支持与Clang的GCC兼容的inline-asm支持的要求紧密相关。因此,这里列出的特征集以及约束和修饰符代码与GCC内联asm支持中的代码类似或相同。然而,要清楚的是,这里描述的模板和约束字符串的语法与GCC和Clang所接受的语法并不相同,并且尽管大多数约束字母是通过Clang原样传递的,但当从C源代码转换为LLVM程序集时,有些字符会被转换为其他代码。

一个内联汇编表达式的例子是:

1
i32 (i32) asm "bswap $0", "=r,r"

内联汇编程序表达式只能用作调用(call)或调用(invoke)指令的被调用者操作数。因此,通常我们有:

1
%X = call i32 asm "bswap $0", "=r,r"(i32 %Y)

带有在约束列表中不可见的副作用的内联asms必须标记为具有副作用。这是通过使用’sideeffect’关键字完成的,如下所示:

1
call void asm sideeffect "eieio", ""()

在某些情况下,内联asms将包含无法工作的代码,除非堆栈以某种方式对齐,例如x86上的调用或SSE指令,但不会包含在asm中执行对齐的代码。编译器应该对asm可能包含的内容做出保守的假设,并且如果’alignstack’关键字存在,应该在序言中生成其通常的堆栈对齐代码:

1
call void asm alignstack "eieio", ""()

内联asms也支持使用非标准汇编方言。假定的方言是ATT。当’inteldialect‘关键字存在时,内联asm使用英特尔方言。目前,ATT和Intel是唯一支持的方言。一个例子是:

1
call void asm inteldialect "eieio", ""()

如果出现多个关键字,则’sideeffect‘关键字必须首先出现,’alignstack‘关键字第二关键字和’inteldialect‘关键字最后出现。

内联汇编约束字符串

约束列表是逗号分隔的字符串,每个元素包含一个或多个约束代码。

对于约束列表中的每个元素,将选择一个适当的寄存器或内存操作数,并且将对$0列表中的第一个约束,$1第二个等将使其可用于组件模板字符串扩展。

有三种不同类型的约束,它们通过约束代码前面的前缀符号进行区分:输出,输入和Clobber。必须始终按照以下顺序给出约束:先输出,然后输入,然后是clobbers。他们不能混在一起。

还有三种不同类型的约束代码:

  • 注册约束。这是一个寄存器类,或者是一个固定的物理寄存器。这种约束将分配一个寄存器,并且如果必要的话,将该参数或结果进行bitcast到适当的类型。
  • 内存约束。这种约束用于获取内存操作数的指令。不同的约束允许目标使用不同的寻址模式。
  • 立即值限制。这种约束是针对整数或其他立即值的,它可以直接渲染到指令中。各种特定于目标的约束条件允许为您希望使用的指令选择合适范围内的值。

输出约束

输出约束由“=”前缀(例如“=r”)指定。这表示程序集将写入此操作数,然后操作数将作为asm表达式的返回值提供。输出约束不会消耗调用指令中的参数。(除了下面关于间接输出的内容)。

通常,在读取所有输入之前,预计没有输出位置被汇编表达式写入。因此,LLVM可以将相同的寄存器分配给输出和输入。如果这不安全(例如,如果程序集包含两条指令,其中第一条写入一个输出,第二条读取输入并写入第二条输出),则必须使用“&”修饰符(例如“=&r”)来指定输出是“早期破坏”输出。将输出标记为“early-clobber”可确保LLVM不会对任何输入(除了与此输出关联的输入)使用相同的寄存器。

输入约束

输入约束没有前缀 - 只是约束代码。每个输入约束将从调用指令中消耗一个参数。asm不允许写入任何输入寄存器或存储单元(除非该输入连接到输出)。还要注意,如果LLVM可以确定它们必然都包含相同的值,则可以将多个输入全部分配给相同的寄存器。

通过提供一个整数作为约束字符串,输入约束可以将它们自己绑定到输出约束,而不是提供约束代码。被绑定的输入仍然会从调用指令中消耗一个参数,并且按照通常的方式在asm模板编号中占据一个位置 - 它们将被简单地限制为始终使用与其绑定的输出相同的寄存器。例如,一个约束字符串“=r,0”表示为输出分配一个寄存器,并将该寄存器用作输入(它是第0个约束)。

允许将输入连接到“早期破坏(early-clobber)”输出。在这种情况下,没有 其他输入可能与连接到早期触发器的输入共享相同的寄存器(即使其他输入具有相同的值)。

您只能将输入绑定到具有寄存器约束但不受内存约束的输出。只有一个输入可能与输出相关联。

还有一个“有趣”的特性,值得一点解释:如果寄存器类约束分配的寄存器对于作为输入提供的值类型操作数来说太小,则输入值将被分成多个寄存器,并且所有寄存器传递给内联asm。

但是,此功能通常不如您想象的那么有用。

首先,寄存器不保证连续。因此,在那些具有多条连续指令操作指令的体系结构上,这不是支持它们的适当方式。(例如,32位SparcV8具有64位加载,该指令只需要一个32位寄存器,然后硬件将加载到指定的寄存器和下一个寄存器中。内联asm的此功能对于此的支持将不会有用。)

几个目标提供了一个模板字符串修改,允许两寄存器操作数的第二个寄存器明确的访问(例如MIPS L,M和 D)。在这样的体系结构中,您实际上可以访问第二个已分配的寄存器(但是,仍然没有任何后续的寄存器)。但是,在这种情况下,为了清晰起见,将这个值简化为两个独立的操作数仍然可能更好。(例如,请参阅AX86 上的约束描述,尽管该特性仅用于此功能,但使用并不是一个好主意)

间接投入和产出

间接输出或输入约束可以由“*”修饰符(在输出的情况下在“=”之后)指定。这表明asm将写入或读取作为输入参数提供的地址的内容。(注意,在这种方式,间接输出更像一个输入而不是输出:只是像输入,它们消耗的调用表达式的参数,而不是产生一个返回值。间接输出约束是“输出”仅是希望在asm可以写入输入内存位置的内容,而不是从中读取)。

这通常用于内存约束,例如“=*m”,以将变量的地址作为值传递。

也可以使用间接寄存器约束,但仅限于输出(例如“=*r”)。这会导致LLVM正常地为输出值分配一个寄存器,然后在提供的内联asm之后,单独发送一个存储到作为输入提供的地址。(与在asm语句后明确写入store相比,此功能提供了什么值尚不清楚,而且它只能生成更糟糕的代码,因为它绕过了许多优化过程,我建议不要使用它。)

Clobber约束

一个clobber约束由一个“~”前缀表示。clobber不会消耗输入操作数,也不会生成输出。Clobbers不能使用任何一般的约束代码字母 - 它们可能只使用明确的寄存器约束,例如“~{eax}”。一个例外是,“~{memory}” 的clobber字符串表示程序集写入任意未声明的内存位置 - 不仅是由声明的间接输出指向的内存。

请注意,输出约束中也存在的clobbering命名寄存器是不合法的。

约束代码

潜在的前缀来了约束代码或代码之后。

约束代码可以是单个字母(例如“r”),“^”字符后跟两个字母(例如“^wc”)或“{”寄存器名称“ }”(例如“{eax}”)。

通常选择单字母和双字母约束代码与GCC的约束代码相同。

一个约束可能包含一个或多个约束代码,而让LLVM选择使用哪一个约束代码。这主要包括与来自clang的GCC inline asm的翻译兼容。

有两种方式可以指定替代方案,并且可以在内联asm约束列表中使用其中之一或两者。

  1. 相互追加代码,制作约束代码集。例如“im”或“ {eax}m”。这意味着“选择集合中的任何选项”。对约束列表中的每个约束独立进行约束的选择。
  2. 在约束代码集之间使用“|”,创建替代方案。约束列表中的每个约束都必须具有相同数量的备选集。使用这种语法,约束列表中所有项目中的相同备选项将一起选择。

把它们放在一起,你可能会有两个操作数约束字符串,像”rm|r,ri|rm“。这表明如果操作数0是r或m,则操作数1可以是r或i。如果操作数0是r,则操作数1可以是r或m。但是,操作数0和1不能都是m类型。

但是,不推荐使用其中任何一种替代功能,因为LLVM无法对使用哪种替代功能做出明智选择。(在当前需要选择的时候,没有足够的信息可以用聪明的方式来实现。)因此,它只是试图做出最有可能编译的选择,而不是最优性能的选择。(例如,给定“rm”,它总是选择使用内存,而不是寄存器)。而且,如果给定多个寄存器或多个寄存器类,它将简单地选择第一个。(实际上,目前它甚至不确保明确指定的物理寄存器是唯一的,因此指定多个物理寄存器作为替代,例如 {r11}{r12},{r11}{r12},将r11分配给两个操作数,而不是所有打算的。)

支持的约束代码列表

一般来说,约束代码的行为与GCC中的一样。LLVM的支持通常是在’按需’的基础上实现的,以支持GCC支持的C inline代码。LLVM和GCC之间的行为不匹配可能表明LLVM存在错误。

所有目标通常都支持一些约束代码:

  • r:目标通用寄存器类中的寄存器。
  • m:存储器地址操作数。它支持哪些寻址模式,典型的例子是寄存器,寄存器+寄存器偏移量,或寄存器+直接偏移量(某些目标特定的大小)。
  • i:一个整数常量(目标特定宽度)。允许简单的即时或可重定位的值。
  • n:一个整数常量 - 不包括可重定位值。
  • s:一个整数常量,但只允许重定位值。
  • X:允许任何类型的操作数,不受任何限制。通常用于为asm分支或call传递标签。
  • {register-name}:需要完整的指定物理寄存器。

其他约束是针对具体目标的:

AArch64:

  • z:一个立即数整数0.输出WZR或者XZR视情况而定。
  • I:对一个ADD或SUB指令有效的立即整数,即0到4095,可选的移位12。
  • J:一个立即数,取反时对一个ADD或 SUB指令有效,即-1到-4095,可选左移12。
  • K:一个直接整数,它是有效的“位掩码即时32”的逻辑指令等AND,EOR或ORR与32位寄存器。
  • L:一个直接整数,它是有效的“位掩码即时64”的逻辑指令等AND,EOR或ORR与64位寄存器。
  • M:与MOV32位寄存器上的程序集别名一起使用的立即整数。这是一个超集K:除了bitmask立即数,还允许立即可以装载单个MOVZ或MOVL指令的整数 。
  • N:用于MOV64位寄存器上的程序集别名的立即整数。这是一个超集L。
  • Q:存储器地址操作数必须位于单个寄存器中(无偏移量)。(但是,LLVM目前也为m约束做了这个。)
  • r:32位或64位整数寄存器(W 或X )。
  • w:一个32,64或128位浮点/ SIMD寄存器。
  • x:较低的128位浮点/ SIMD寄存器(V0至V15)。

AMDGPU:

  • r:32位或64位整数寄存器。
  • [0-9]v:32位VGPR寄存器,编号0-9。
  • [0-9]s:32位SGPR寄存器,编号0-9。

所有ARM模式:

  • Q,Um,Un,Uq,Us,Ut,Uv,Uy:内存地址的操作数。目前处理方式与操作数相同m。

ARM和ARM的Thumb2模式:

  • j:0到65535之间的一个立即数(有效MOVW)
  • I:对数据处理指令有效的立即整数。
  • J:一个介于-4095和4095之间的直接整数。
  • K:一个立即数,它的位反转对数据处理指令有效。(可以与模板修饰符“ B”一起使用以打印反转的值)。
  • L:一个立即整数,其否定对数据处理指令有效。(可以与模板修饰符“ n”一起使用以打印否定值)。
  • M:2的幂或0到32之间的整数。
  • N:无效的即时约束。
  • O:无效的即时约束。
  • r:一个通用的32位整数寄存器(r0-r15)。
  • l:在Thumb2模式下,低32位GPR寄存器(r0-r7)。在ARM模式下,与r。
  • h:在Thumb2模式下,一个高32位的GPR寄存器(r8-r15)。在ARM模式下,无效。
  • w:一个32,64或128位浮点/ SIMD寄存器:s0-s31, d0-d31,或q0-q15。
  • x:一个32,64或128位浮点/ SIMD寄存器:s0-s15, d0-d7,或q0-q3。
  • t:一个低浮点/ SIMD寄存器:s0-s31,d0-d16,或 q0-q8。

ARM的Thumb1模式:

  • I:0到255之间的立即数。
  • J:-255和-1之间的立即数。
  • K:0到255之间的直接整数,可选左移一定数量。
  • L:-7和7之间的立即数。
  • M:0到1020之间的整数,是4的倍数。
  • N:0到31之间的立即数。
  • O:在-508和508之间的立即数,是4的倍数。
  • r:一个低32位的GPR寄存器(r0-r7)。
  • l:一个低32位的GPR寄存器(r0-r7)。
  • h:高GPR寄存器(r0-r7)。
  • w:一个32,64或128位浮点/ SIMD寄存器:s0-s31, d0-d31,或q0-q15。
  • x:一个32,64或128位浮点/ SIMD寄存器:s0-s15, d0-d7,或q0-q3。
  • t:一个低浮点/ SIMD寄存器:s0-s31,d0-d16,或 q0-q8。

Hexagon:

  • o,v:此时存储器地址操作数,与约束一样对待m。
  • r:一个32位或64位寄存器。

MSP430:

  • r:一个8位或16位寄存器。

MIPS:

  • I:一个直接带符号的16位整数。
  • J:一个立即整数零。
  • K:一个直接无符号的16位整数。
  • L:一个直接的32位整数,其中低16位是0。
  • N:-65535和-1之间的立即数。
  • O:一个立即有符号的15位整数。
  • P:1到65535之间的立即数。
  • m:存储器地址操作数。在MIPS-SE模式下,允许一个基址寄存器加上16位立即数偏移量。在MIPS模式下,只需一个基址寄存器。
  • R:存储器地址操作数。在MIPS-SE模式下,允许一个基地址寄存器加上一个9位有符号偏移量。在MIPS模式下,与约束相同 m。
  • ZC:一个存储器地址操作数,适用于使用pref,ll或 sc在给定的子目标指令(细节有所不同)。
  • r,d, y:一个32位或64位GPR寄存器。
  • f:一个32位或64位FPU寄存器(F0-F31)或一个128位MSA寄存器(W0-W31)。在MSA寄存器的情况下,建议使用w 参数修饰符与GCC兼容。
  • c:适用于间接跳转(始终25)的32位或64位GPR寄存器 。
  • l:lo寄存器,32或64位。
  • x:无效。

NVPTX:

  • b:1位整数寄存器。
  • c或者h:一个16位整数寄存器。
  • r:一个32位整数寄存器。
  • l或者N:一个64位整数寄存器。
  • f:一个32位浮点寄存器。
  • d:一个64位的浮点寄存器。

PowerPC的:

  • I:一个直接带符号的16位整数。
  • J:直接无符号的16位整数,左移16位。
  • K:一个直接无符号的16位整数。
  • L:立即带符号的16位整数,左移16位。
  • M:大于31的立即数。
  • N:是2的精确幂的立即数。
  • O:立即整数常量0。
  • P:一个立即整型常量,其否定是一个有符号的16位常量。
  • es,o,Q,Z,Zy:一个存储器地址操作数,目前一样对待m。
  • r:32位或64位整数寄存器。
  • b:32位或64位整数寄存器,不包括R0(即 :)R1-R31。
  • f:32位或64位浮点寄存器(F0-F31)或QPX使能时,128位或256位QPX寄存器(Q0-Q31;用于别名F寄存器)。
  • v:对于或类型,当启用QPX时,为128或256位QPX寄存器(),否则为128位altivec向量寄存器()。4 x f324 x f64Q0-Q31V0-V31
  • y:条件寄存器(CR0-CR7)。
  • wc:CR寄存器中的单独CR位。
  • wa,wd,wf:任何128位VSX向量寄存器,从全VSX寄存器组(重叠两个浮点和向量寄存器文件)。
  • ws:来自完整的VSX寄存器组的32位或64位浮点寄存器。

SPARC:

  • I:一个立即的13位有符号整数。
  • r:一个32位整数寄存器。
  • f:SparcV8上的任何浮点寄存器或SparcV9上“低”一半寄存器中的浮点寄存器。
  • e:任何浮点寄存器。(与fSparcV8 相同。)

SystemZ:

  • I:直接无符号的8位整数。
  • J:直接无符号的12位整数。
  • K:一个直接带符号的16位整数。
  • L:一个直接签名的20位整数。
  • M:立即整数0x7fffffff。
  • Q:具有基地址和12位立即无符号位移的存储器地址操作数。
  • R:一个带有基地址的内存地址操作数,一个12位立即无符号位移和一个索引寄存器。
  • S:一个内存地址操作数,带有一个基址和一个20位立即带符号的位移。
  • T:一个带有基地址的内存地址操作数,一个20位立即带符号位移和一个索引寄存器。
  • r或者d:一个32位,64位或128位整数寄存器。
  • a:32,64或128位整数地址寄存器(不包括地址上下文中评估为0的R0)。
  • h:64位数据寄存器高位部分的32位值(LLVM专用)
  • f:一个32,64或128位浮点寄存器。

X86:

  • I:0到31之间的立即数。
  • J:0到64之间的立即数。
  • K:一个立即有符号的8位整数。
  • L:立即整数,0xff或0xffff或(仅在64位模式下)0xffffffff。
  • M:0到3之间的立即数。
  • N:直接无符号的8位整数。
  • O:0到127之间的立即数。
  • e:一个立即的32位有符号整数。
  • Z:一个立即的32位无符号整数。
  • o,v:目前处理方式与此相同m。
  • q:一个8,16,32或64位寄存器,可以作为8位 l整数寄存器访问。在X86-32,这是a,b,c,和d 寄存器,以及X86-64,它是所有的整数寄存器。
  • Q:一个8,16,32或64位寄存器,可以作为8位 h整数寄存器访问。这是a,b,c,和d寄存器。
  • r或者l:8,16,32或64位整数寄存器。
  • R:8,16,32或64位“传统”整数寄存器 - 自i386以来一直存在,并且可以在没有REX前缀的情况下访问。
  • f:一个32,64或80位’387 FPU堆栈伪寄存器。
  • y:如果启用MMX,则为64位MMX寄存器。
  • x:如果启用SSE:SSE寄存器中的32位或64位标量操作数或128位向量操作数。如果AVX也被使能,也可以是AVX寄存器中的256位向量操作数。如果AVX-512也被使能,也可以是AVX512寄存器中的512位向量操作数,否则会出错。
  • Y:同x,如果SSE2被启用,否则会出现错误。
  • A:特殊情况:首先为EAX分配EAX,然后再为EDX分配单个操作数(在32位模式下,64位整数操作数将分成两个寄存器)。不建议使用此约束,因为在64位模式下,64位操作数只会分配给RAX - 如果需要两个32位操作数,则最好在将其分配给它之前自行分割asm声明。

XCore:

  • r:一个32位整数寄存器。

Asm模板参数修饰符

在asm模板字符串中,可以在操作数引用上使用修饰符,如“${0:n}”。

一般来说,修饰符的行为与GCC中的相同。LLVM的支持通常是在’按需’的基础上实现的,以支持GCC支持的C inline代码。LLVM和GCC之间的行为不匹配可能表明LLVM存在错误。

目标无关的:

  • c:不带目标特定的直接标点符号(例如无$前缀)打印一个立即的整数常量。
  • n:取消并打印立即数整数常量,不带目标特定的直接标点符号(例如无$前缀)。
  • l:打印为无标签的标签,没有特定于目标的标签标点(例如无$前缀)。

AArch64:

  • w:用w名称而不是x名称打印GPR寄存器。例如,而不是x30打印w30。
  • x:用x*名称打印GPR寄存器。(无论如何,这是默认的)。
  • b,h,s,d,q:打印浮点/ SIMD寄存器有 b,h,s,d,或q名称,而不是默认的 v

AMDGPU:

  • r: 没有效果。

ARM:

  • a:打印操作数作为一个地址([和]周围的寄存器)。
  • P: 没有效果。
  • q: 没有效果。
  • y:将VFP单精度寄存器作为索引双精度打印(例如打印d4[1]而不是s9)
  • B:按位反转并打印不带# 前缀的立即整数常量。
  • L:打印立即整数常量的低16位。
  • M:打印为适合ldm / stm的寄存器组。同时打印 指定的一个(!)后面的所有寄存器操作数,请谨慎使用。
  • Q:打印寄存器对的低位寄存器或双寄存器操作数的低位寄存器。
  • R:打印寄存器对的高位寄存器或双寄存器操作数的高位寄存器。
  • H:打印寄存器对的第二个寄存器。(在大端系统上, H相当于Q小端系统,H相当于R。)
  • e:打印NEON四路寄存器的低双字寄存器。
  • f:打印NEON四路寄存器的高位双字寄存器。
  • m:打印没有[和] 装饰的内存操作数的基址寄存器。

Hexagon:

  • L:打印双寄存器操作数的第二个寄存器。要求它已被连续分配到第一个。
  • I:如果操作数是一个整数常量,则打印字母’i’,否则不打印。用于打印’addi’和’add’指令。

MSP430:

  • 没有额外的修饰符。

MIPS:

  • X:以十六进制形式打印一个立即数
  • x:以十六进制形式打印立即数的低16位。
  • d:以小数形式打印一个立即数。
  • m:减去一个并以十进制形式打印一个立即数。
  • z:如果立即为零,则打印$ 0,否则正常打印。
  • L:打印双寄存器操作数的低位寄存器,或打印双字存储器操作数的低位字的地址。
  • M:打印双寄存器操作数的高位寄存器,或者打印双字存储器操作数的高位字的地址。
  • D:打印双寄存器操作数的第二个寄存器,或打印双字存储器操作数的第二个字。(在大端系统上,D相当于L小端系统,D相当于 M。)
  • w: 没有效果。为了与需要此修饰符的GCC兼容才能打印W0-W31具有f 约束条件的MSA寄存器()。

NVPTX:

  • r: 没有效果。

PowerPC的:

  • L:打印双寄存器操作数的第二个寄存器。要求它已被连续分配到第一个。
  • I:如果操作数是一个整数常量,则打印字母’i’,否则不打印。用于打印’addi’和’add’指令。
  • y:对于内存操作数,打印双寄存器X-form指令的格式化程序。(目前始终打印r0,OPERAND)。
  • U:如果内存操作数是更新形式,则打印’u’,否则不打印。(注意:LLVM不支持更新表单,所以它现在总是不会打印任何内容)
  • X:如果内存操作数是索引形式,则打印’x’。(注意:LLVM不支持索引形式,所以目前这总是不会打印任何东西)

SPARC:

  • r: 没有效果。

SystemZ:

  • SystemZ仅实现n,并且也不会支持任何其他目标无关的改性剂。

X86:

  • c:打印一个无用的整数或符号名称。(后者是这个典型的与目标无关的修饰符的目标特定行为)。
  • A:*在它之前用一个“ ‘ 打印一个注册名称。
  • b:打印一个8位寄存器名称(例如al); 内存操作数不做任何事情。
  • h:打印上面的8位寄存器名称(例如ah); 内存操作数不做任何事情。
  • w:打印16位寄存器名称(例如ax); 内存操作数不做任何事情。
  • k:打印32位寄存器名称(例如eax); 内存操作数不做任何事情。
  • q:打印64位寄存器名称(例如rax),如果64位寄存器可用,则返回32位寄存器名称; 内存操作数不做任何事情。
  • n:取反并打印一个未修饰的整数,或者,对于非立即整数的操作数(例如可重定位符号表达式),在操作数前面打印一个’ - ‘。(可重定位符号表达式的行为是针对此通常与目标无关的修饰符的目标特定行为)
  • H:用额外的偏移量+8打印存储器引用。
  • P:打印内存引用或操作数以用作调用指令的参数。(例如(rip),即使它是PC相对的,也省略。)

XCore:

  • 没有额外的修饰符。

内联Asm元数据

包装内联asm节点的调用指令可能会附加一个“!srcloc”MDNode,它包含一个常量整数列表。如果存在,则当通过LLVMContext 错误报告机制报告错误时,代码生成器将使用该整数作为位置cookie值。这允许前端将内联asm中发生的后端错误与产生它的源代码关联起来。例如:

1
2
3
call void asm sideeffect "something bad", ""(), !srcloc !42
...
!42 = !{ i32 1234567 }

直到前端才能理解它在IR中的神奇数字。如果MDNode包含多个常量,则代码生成器将使用与发生错误的asm行相对应的那个常量。

0%