这篇文档是LLVM
汇编语言(assembly language
)的参考手册。LLVM是一个基于静态单赋值(Static Single Assignment
,简写为SSA
)的表示形式,它提供了类型安全(type safety
)、低级别操作(low-level operations
)、灵活性、清晰表示“所有”高级语言的能力。它是贯穿LLVM编译策略全阶段的通用代码表示。
LLVM
代码表示形式被设计为使用三种不同的格式:
Intermediate Representation
,简称IR
);bitcode
)表示(适合JIT
编译器快速加载);这些允许LLVM
为编译器的高效转换和分析提供强大的IR
,同时提供一个自然的方法来调试和可视化转换。这LLVM
的三种不同方式是等价的。本文档描述了人类可读的表示和标注。
LLVM
表示的目标是轻量级和低级别,同时变得更具表示力、类型化、可扩展性。它的目的是变成“通用IR
”序列,在一个足够的低级别上高级别思维可以清晰地映射到它(类似微处理器都是“通用IR”,使得众多源语言可以映射到它)。根据提供类型信息,LLVM
可以作为优化的目标:例如,通过指针分析证明,一个C
自动变量从没被当前函数以外的外部访问到,允许它提升到一个简单的SSA
值从而代替内存地址。
注意这个文档描述“格式良好的”LLVM
汇编语言。这有区别于“可以被解释的就是(well-formed)格式良好的” 的概念。例如,下面的指令语法上是可接受的,但不是格式良好的:
%x = add i32 1, %x
因为%x
的定义并不支配它所有的使用者。LLVM
基础结构提供了一个验证pass,它可以用于验证LLVM
模块是否格式良好的。这个pass是在解释器解释输入汇编之后和优化器输出位码之前,由解释器和优化器自动运行的。被verifier pass指出的违规行为表现为转换passes中的bug或解释器的输入中的bug。
LLVM
标识符有两种基本类型:全局和局部。全局标识符(函数、全局变量)以‘@
’字符开始。局部标识符(寄存器名,类型)以‘%
’字符开始。此外,还有三种用于不同目的的标识符格式:
命名值
被表示为一个带有前缀字符的string。例如,%foo、@DivisionByZero、%a.really.long.identifier。它的正则表达式可以描述为‘[%@][-a-zA-Z$._][-a-zA-Z$._0-9]*
’。需要它们名称中其他字符的标识符可以使用引号包围。特殊字符使用“\xx”可能溢出,这里xx是字符十六进制的ASCII码。这种方式下,任何字符可以在一个name value中使用,甚至引号自身。“\01”前缀可以禁止全局变量的名字改编。未命名值
被表示为一个带有前缀的无符号数值。例如,%12、@2、%44。LLVM
需要值以一个前缀开始的原因有两点:编译器不需要担心名称会和保留字冲突;保留字集在未来可以无危害的扩展。此外,未命名标识符允许编译器迅速提出一个临时变量,且避免和符号表冲突。
LLVM
的保留字和其他语言的保留字很相似。有不同的操作码opcode
(‘add
’、‘bitcast
’、‘ret
’等等),原始类型名(‘void
’、‘i32
’等等),以及其他关键词。这些保留字不可能和变量名冲突,因为它们不以‘%’
或者‘@’
前缀开始。
下面是整型变量%x
乘以8的LLVM
代码例子:
简单的表示:
%result = mul i32 %X, 8
强度削减(strength reduction
)后:
%result = shl i32 %X, 3
困难方法:
%0 = add i32 %X, %X ; yields i32:%0
%1 = add i32 %0, %0 ; yields i32:%1
%result = add i32 %1, %1
%X
乘以8的最后一种方法说明了LLVM
的几个重要的词法特点:
它也展示了在这个文档我们应该遵循的一条约定。当演示指令时,我们应该在这条指令后面添加注释,这个注释定义了被生成值的类型和名称。
LLVM
程序由Module
组成,每个模块都是输入程序的一个转换单元。每个模块由函数、全局变量、符号表条目组成。模块可能通过LLVM linker
组合在一起,LLVM linker
合并方法(和全局变量)定义,解决预声明,合并符号表条目。下面是“hello world”
模块的一个示例:
; Declare the string constant as a global constant.
@.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00"
; External declaration of the puts function
declare i32 @puts(i8* nocapture) nounwind
; Definition of main function
define i32 @main() { ; i32()*
; Convert [13 x i8]* to i8 ...
%cast210 = getelementptr [13 x i8], [13 x i8] @.str, i64 0, i64 0
; Call puts function to write out the string to stdout.
call i32 @puts(i8* %cast210)
ret i32 0
}
; Named metadata
!0 = !{i32 42, null, !"string"}
!foo = !{!0}
这个例子由一个全局变量“.str”
、一个外部声明的“puts”
函数,一个函数定义“main”
和命名元数据“foo”
组成。
通常来讲,一个模块由全局值(函数和全局变量都是全局值)列表组成。全局值通过一个指向内存位置的指针来表示(在这个例子中,一个指向字符数组的指针,一个指向函数的指针),并且有下面的链接类型中的一个。
所有全局变量和函数都有下面链接类型中的一种:
private
“private”
链接类型的全局值只能被当前模块中的对象直接访问。特别地,使用一个private全局值链接代码到一个模块,在必要情况下为了避免冲突可能会被重命名。因为这个符号对这个模块来说是private的,所有引用可能被更新。这不会出现在目标文件中的任何符号表中。internal
available_externally
“available_externally”
链接类型的全局值永远不会被发送到与LLVM模块对应的对象文件中。从 linker 的观点来看,一个available_externally全局值等同于一个外部声明。它们的存在是为了允许内联和其他优化在已知全局定义的情况下进行,已知全局定义位于模块之外。具有“available_externally”链接类型的全局值允许随意定义,允许内联和其他优化行为。这个链接类型仅允许定义,而不能声明。linkonce
“linkonce”
链接类型的全局值当链接发生时与其他同名的全局值合并。这可能常用于实现一些内联函数、模板或者其他必须在每个转换单元中生成使用的代码等形式,但是之后主体可以被一个更详细的定义覆盖。未引用的linkonce全局值允许被丢弃。注意,linkonce链接实际上并不允许优化器来内联函数的函数体到调用者中,因为它不知道函数定义是程序的最终定义或是否将被一个更明确定义重写。使用“linkonce_odr”链接可以使得内联和其他优化有效。weak
common
“int X;”
。具有“common”链接类型的符号和“weak”符号被合并的方式相同,但这些符号即使未被引用也不会被删除。“common”符号可能不会有一个明确的部分,必须有一个0初始化器,且不可能被标志为‘constant’。函数和别名不可以具有“common”链接类型。appending
llvm.global_ctors
这些llvm特别解释的变量。extern_weak
linkonce_odr, weak_odr
“one definition rule”
简写为“ODR ”
) 。这些语言可以使用linkonce_odr
和weak_odr
链接类型来表明全局值将只与等效的全局值合并。否则,这些链接类型与它们的non-odr
版本相同。external
一个函数声明拥有除“external”
或“extern_weak”
以外的链接类型是不合法的。
LLVM
的函数、call和invoke都有一个可选的调用约定来指定调用。每一对动态调用者/被调用者的调用约定必须匹配,或者程序行为是未定义的。下面的调用约定是LLVM
所支持的,未来将会添加更多的:
“ccc” —— C调用约定
这个调用约定(在没有指定其他调用约定的前提下是默认调用约定)匹配目标C调用约定。这个调用约定支持变长参数函数调用,允许在声明的原型中有一些不匹配以及实现了函数的声明(与普通C一样)。
“fastcc” —— 快速调用约定
这个调用约定试图使调用尽可能地快速(例如通过寄存器传递)。这种调用约定允许目标使用任何它想要为目标生成快速代码的技巧,而不必遵守外部指定的ABI(应用程序二进制接口,Application Binary Interface)。只有在使用GHC或HiPE约定时,才能优化Tail调用。这个调用约定不支持变长参数,且要求所有被调用对象的原型与函数定义的原型精确匹配。
“coldcc” —— 冷调用约定
这个调用约定尝试在假定调用通常不执行的情况下,使调用方中的代码尽可能高效。因此,这些调用通常保存所有寄存器,以便调用不会破坏调用方中的任何活动范围。这个调用约定不支持变长参数,且要求所有被调用对象的原型与函数定义的原型精确匹配。更进一步地讲,内联器不会考虑把这种函数进行内联。
“cc 10” —— GHC(Glasgow Haskell Compiler)约定
这个调用约定是专门为Glasgow Haskell编译器(GHC)实现的。它在寄存器中传递所有内容,通过禁用被调用者保存寄存器来达到这个目的。这个调用约定不应该轻轻地使用,这种调用约定不应该轻易使用,而应该只用于特定的情况,比如在实现函数式编程语言时经常使用的寄存器固定性能技术的替代方法。目前只有X86支持这个约定,它有以下限制:
这个调用约定支持 tail 调用优化,但是要求调用者和被调用者都使用它。
“cc 11” —— HiPE调用约定
这个调用约定是专门为HiPE (高性能Erlang,High-Performance Erlang)编译器实现的,它是Ericsson开源Erlang/OTP系统的本机代码编译器。它使用比普通C调用约定更多的寄存器来传递参数,并且定义了no callee-saved寄存器。这个调用约定正确地支持 tail 调用优化,但要求调用方和被调用方都使用它。它使用一种寄存器固定机制,类似于GHC约定
,用于将经常访问的运行时组件固定到特定的硬件寄存器。目前只有X86支持这种约定(32位和64位)。
“webkit_jscc” —— Webkit的JavaScript调用约定
这个调用约定为WebKit FTL JIT实现。它将栈上的参数从右向左传递(与cdecl一样),并在平台的常规返回寄存器中返回一个值。
“anyregcc” —— 用于代码修补的动态调用约定
这是一种特殊的约定,它支持在调用站点上修补一个任意代码序列。这个调用约定强制调用参数到寄存器中,但是允许它们被动态分配。目前只能在调用llvm.experimental.patchpoint时使用这个调用约定。因为只有这个内在的东西记录了它的参数在边表中的位置。参见LLVM中的堆栈映射和补丁点。
“preserve_mostcc” —— PreserveMost调用约定
这个调用约定试图使调用者中的代码尽可能不受干扰。这个约定在传递参数和返回值的方式上与C调用约定
相同,但是它使用一组不同的调用者/被调用者保存的寄存器。这减轻了在调用方中调用前后保存和恢复大型寄存器集的负担。如果参数在被调用者保存的寄存器中传递,那么它们将由被调用者在整个调用过程中保存。这不适用于在调用保存的寄存器中返回的值。
这个约定背后的思想是支持对具有一个热路径和一个冷路径的运行时函数的调用。热路径通常是一小段代码,不使用很多寄存器。冷路径可能需要调用另一个函数,因此只需要保存调用者保存的寄存器,这些寄存器还没有被调用者保存。就调用者/被调用者保存的寄存器而言,PreserveMost调用约定
与cold调用约定
非常相似,但是它们用于不同类型的函数调用。coldcc是用于很少执行的函数调用,而preserve_mostcc函数调用是针对热路径的,并且肯定执行了很多次。此外,preserve_mostcc并不会阻止内联程序内联函数调用。
这个调用约定将被ObjectiveC运行时的一个未来版本所使用,因此在这个时候仍然应该被认为是实验性的。尽管创建此约定是为了优化对ObjectiveC运行时的某些运行时调用,但它并不限于此运行时,将来可能还会被其他运行时使用。目前的实现只支持X86-64,但其目的是在将来支持更多的体系结构。
“preserve_allcc” —— PreserveAll调用约定
这个调用约定试图使调用者中的代码比PreserveMost调用约定更具侵入性。这个调用约定在传递参数和返回值方面的行为也与C调用约定相同,但是它使用了一组不同的调用者/被调用者保存的寄存器。这消除了在调用方中调用前后保存和恢复大型寄存器集的负担。如果参数在被调用者保存的寄存器中传递,那么它们将由被调用者在整个调用过程中保存。这不适用于在调用保存的寄存器中返回的值。
这种约定背后的思想是支持对运行时函数的调用,而不需要调用任何其他函数。
与PreserveMost调用约定
一样,这个调用约定将被ObjectiveC运行时的一个未来版本使用,因此在这个时候仍然应该被认为是实验性的。
“cxx_fast_tlscc” —— 用于访问函数的CXX_FAST_TLS调用约定
Clang生成一个访问函数来访问C++风格的TLS。这个访问函数通常有一个入口块,一个出口块以及一个初始化块,这样就可以在第一时间运行。这个入口和出口块可以访问少量TLS IR变量,每次访问都将降低到特定于平台的顺序。
这个调用约定的目的是通过保存尽可能多的寄存器(所有驻留在快速路径上的寄存器,由入口块和出口块组成)来最小化调用者的开销。
这个调用约定在传递参数和返回值方面的行为与C调用约定
相同,但是它使用一组不同的调用者/被调用者保存的寄存器。
由于每个平台都有自己的降低序列,因此有自己的保存寄存器集,因此我们不能使用现有的PreserveMost。
“swiftcc” —— 用于Swift语言的调用约定
“cc ”-编号约定
任何一个调用约定可能有数字指定,允许使用目标明确的调用约定。目标明确调用约定是在64位开始的。
更多调用约定会以使用为依据被添加/定义,如支持Pascal
约定或者其他知名目的独立的约定。
所有全局变量和函数有一个如下的可见性模式:
“default”-默认模式
“external linkage”
是一致的。“hidden”-隐藏模式
“protected”-保护模式
internal
和private
链接的符号表必须有默认可见性。
所有全局变量、函数和别名可以有一个如下DLL
存储类别:
dllimport
dllexport
一个变量可能被定义为thread_local
,这意味着它不会被线程分享(每个线程有独立的变量拷贝)。并不是所有目标支持线程本地存储变量。作为一个选项,TLS
模型可能被指定:
localdynamic
initialexec
localexec
如果没有给于明确类型,“general dynamic”
将会被使用。
这种模型与ELF TLS
模型是一致的;更多关于不同模块可能被使用的情况信息详见ELF Handling For Thread-Local Storage。如果指定的模型不支持或者有更合适的模型可供选择,目标可能会选择不同TLS
模型。
一个模型可以在别名中指定,但是它仅支配别名怎么访问。它不会在别名中有实际效果。
对于链接器不支持ELF TLS
模型的平台,-femulated-tls
标记可被使用生成GCC
兼容的竞争TLS
代码。
全局变量、函数和别名可以有一个可选的运行时抢占说明符。如果没有显式地给出抢占说明符,则假定符号为dso_preemptable。
LLVM
的中间语言允许同时指定“identified”
和“literal”
结构类型。文字类型是唯一结构,但是确定类型永远不是唯一的。一个不透明结构类型也可以用于直接定义一个不可使用的类型。
确定结构说明的例子如下:
%mytype = type { %mytype*, i32 }
在LLVM 3.0
发布版本之前,确定类型是结构唯一的。仅文字类型在LLVM
最近版本是唯一的。
注意:非整型指针类型是在进程中执行的,它们此时被认为是实验的。
LLVM
中间语言选择允许前端代表在特定地址空间指针通过:ref:datalayout string
作为“non-integral”
。非整型指针类型展示有未指明按位表现的指针,那是整型表现可能目标独立或者易变的(不被合适整型返回)。
inttoptr
指令转换整型到非整型指针类型是病态类型,因此ptrtoint
指令转换非整型指针类型值到整型。指令中提到的向量版本也是病态类型。
全局变量在编译期定义存储分配范围,而不是运行时。
全局变量定义必须被初始化。
全局变量在其他的转换单元也可以被声明,但在这种情况下它们没有初始化公式。
每个全局变量的定义或声明可以有明确部分来放置或者可能有可选明确的队列指定。
一个变量可以被定义为全局长了,它指明了变量内容永远不会被改变(为了更好优化,允许全局数据放置在执行块的只读部分,诸如此类)。注意的是需要运行时初始化的变量不可以标记constant
,那只能存储到变量。
LLVM
明确允许全局变量声明标记为常量,甚至全局变量的最终定义不是的。这个功能常用于程序轻微更好的优化,但是这要求语言定义保证基于‘constantness’
的优化对于不包含这个定义的编译单元是有效的。
作为SSA
值,全局变量定义为指针值,其作用域(例如它们的影响范围)是程序中的所有基本块。全局变量总是定义为一个其‘content’
所对应类型的指针,因为它们描述一个存储范围,所有这些LLVM
中的存储对象被通过这个指针访问。
全局变量可以被unnamed_addr
标识,表明它的地址是没有意义的,仅仅是指向对应内容。一个被这样标识的常量可以被合并到其他拥有相同初始化公式的常量。注意,一个地址有意义的常量可以被合并一个unnamed_addr
常量,并且合并结果是地址有意义的常量。
如果给于一个local_unnamed_addr
属性,在模块内的地址是没有意义的。
一个全局变量可能被声明驻留在一个目标指定的编号地址空间。对于支持它们的目标,地址空间会影响优化被怎么执行及使用什么指令来访问变量。默认的地址空间是0
。地址空间限定符必须放在其他任何属性前。
LLVM允许一个明确部分用于指定全局变量。如果目标支持它,它会发步全局变量到这个指定部分。 此外如果目标必要支持,全局变量可以存放在此选项允许编译器以封装函数。
默认下,全局初始化公式通过假设被定义全局变量优化,它们的来自在全局初始化公式开始之前初始值的模块是不会被修改。对于可能从外部访问的变量,包括外部链接、或者出现在@llvm.used
、或者 dllexported
变量,这种也是正确的。这种猜想会被带有externally_initialized
的变量抑制。
一个显式队列可能被标识于一个全局变量,这个一定是2的次幂。如果队列不存在,或者队列被设置为0,那么这个全局变量的队列将会被目标根据方便性设置。如果一个显式队列被标识,这个全局变量被迫完全拥有精确队列。如果这个全局变量有一个被分配到部分,目标和优化器不会允许全局变量超过对齐。在这种情况下,额外的队列是显而易见的:例如,代码可能猜想全局变量被集中放置到它们的部分中,并尝试以数组形式遍历它们,但队列填充会打破遍历过程。 最大的队列是1 << 29
。
全局变量同样可以拥有一个DLL
存储类别和可选的附加元数据列表,变量与别名可以有TLS模型。
语法如下:
@ = [Linkage] [Visibility] [DLLStorageClass] [ThreadLocal]
[(unnamed_addr|local_unnamed_addr)] [AddrSpace]
[ExternallyInitialized]
[]
[, section "name"] [, comdat [($name)]]
[, align ] (, !name !N)*
例如,下面定义了在编号地址空间的全局变量,它有初始化公式、部分和队列:
@G = addrspace(5) constant float 1.0, section "foo", align 4
下面的例子仅仅声明了全局变量:
@G = external global i32
下面的例子定义了一个带有initialexec
的TLS模型的线程本地全局变量:
@G = thread_local(initialexec) global i32 0, align 4
LLVM
函数定义由“define”
关键字,一个可选的链接标识,一个可选的可见性模式,一个可选的DLL存储类别,一个可选的调用约定,一个可选的unnamed_addr
属性,一个返回值类型,一个可选的返回值类型的参数属性,一个函数名,一个(可能为空的)参数列表(每一个都带有可选的参数属性),可选的函数属性,一个可选的部分,一个可选的队列,一个可选的此选项允许编译器以封装函数,一个可选垃圾回收期的名称,一个可选的前缀,一个序言,一个可选的特征,一个可选附加元数据列表,一个左括号,一个基本块列表和一个右括号。
LLVM
函数声明由“declare”
关键字,一个可选的链接标识,一个可选的可见性模式,一个可选的DLL存储类别,一个可选的调用约定,一个可选的unnamed_addr
属性,一个返回值类型,一个可选的返回值类型的参数属性,一个函数名,一个可能为空的参数列表,一个可选的队列,一个可选垃圾回收器名称,一个可选的前缀和一个可选的序言。
一个函数定义包含一个基本块的列表,可以为函数形成CFG
(控制流图形)。每一个基本块可选的开始于一个标签(给定这个基本块一个符号表入口),包含一个指令列表,并且以一个终止指令(例如分支或函数返回)结束。如果一个明确标签不被提供,一个块会分配到一个隐式编号标签,使用与匿名临时变量使用同一个计数器的下一个值。例如,如果一个函数入口块不具有一个显示标签,他会被分配到一个标签“%0”
,然后第一个在这个块中的匿名临时变量将会是“%1”
,诸如此类。
函数中的第一基本块特殊在两个方面:它是在函数进入后马上执行的,并且它是不允许有前置基本块(即函数入口块不能拥有任何分支)。因为这个块没有前置块,所以也不能有任何PHI
节点。
LLVM
允许函数指定一个明确部分。如果目标支持它,它会放步函数到这个指定块。此外,函数可以被放置在COMDAT
。
一个显示队列可能会被指定到一个函数。如果没有展示,或者队列被设置为0
,那么这个函数的队列将会根据目标的方便设定。如果一个显式队列被指定,这个函数被迫至少想指定的那么多队列。所有队列必须为2的次幂。
如果unnamed_addr
属性被给于,函数地址会被认为是没有意义,并且两个相同的函数之间可以被合并。
如果local_unnamed_addr
属性被给于,地址将会在模块内没有意义。
语法:
define [linkage] [visibility] [DLLStorageClass]
[cconv] [ret attrs]
@ ([argument list])
[(unnamed_addr|local_unnamed_addr)] [fn Attrs] [section "name"]
[comdat [($name)]] [align N] [gc] [prefix Constant]
[prologue Constant] [personality Constant] (!name !N)* { ... }
参数列表是逗号分隔参数序列,每个参数如下形式所示:
语法:
[parameter Attrs] [name]
别名和函数活变量不同,不产生任何新的数据。它仅仅是已存在位置的一个新的符号和元数据。
别名有一个名称和一个别名,不管是全局变量还是常量表达式。
别名可以拥有一个可选的链接标识,一个可选的可见性模式,一个可选的DLL存储类别和一个可选的tls模型。
语法:
@ = [Linkage] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] alias , * @
链接标识必须是private
,linker_private
,linker_private_weak
,internal
,linkonce
,weak
,linkonce_odr
,weak_odr
,external
中的一个。注意一些系统链接器可能会不正确地处理一个降级弱符号作为非别名。
非unnamed_addr
的别名被别名表达式的同样地址保护。unnamed_addr
仅仅保护指向同样的内容。
如果local_unnamed_addr
属性被给于,地址将会在模块内没有意义。
因此别名仅仅是第二名称,一些在产生对象文件是仅被检查的约束提供:
IFuncs
和别名一样,不产生任何新的数据或函数。它们仅仅一个动态链器调用一个解析函数在运行时解析(resolves)的新符号。
IFuncs
有一个名字和一个通过动态链接器返回另外一个函数地址获得的相关名称的函数调用解析器。
IFuncs
有一个可选的链接类型和一个可选的可视性模型。
语法:
@ = [Linkage] [Visibility] ifunc , * @
Comdat IR提供了进入COFF
和ELF
对象文件的COMDAT
功能性。
Comdat
有个展示COMDAT key
的名称。如果链接器选择用key
覆盖其他的,所有指定key
的全局对象将会在最终对象中结束。如果真有的话,别名放置在和需要计算别名的COMDAT
一样的位置。
语法:
$ = comdat SelectionKind
这种选择必须是下面的一种:
any
COMDAT key
,这个选择是随意的。exactmatch
COMDAT key
,但是选择部分必须包含同样的数据。largest
COMDAT key
部分。noduplicates
COMDAT key
存在的唯一部分。samesize
COMDAT key
,但是部分必须包含相同数量的数据。注意,Mach-O
平台不支持COMDAT
,ELF仅支持any
作为选项。
下面是一个如果COMDAT key
部分是最大的,函数仅将被选择的COMDAT组例子:
$foo = comdat largest
@foo = global i32 2, comdat($foo)
define void @bar() comdat($foo) {
ret void
}
作为一个语法糖,$name
如果和全局名称一致将会被遗漏:
$foo = comdat any
@foo = global i32 2, comdat
在COFF对象文件中,这将创建一个包含@foo符号内容的IMAGE_COMDAT_SELECT_LARGEST选择类型的COMDAT部分和另一个与COMDAT第一部分和包含@bar符号内容相关的IMAGE_COMDAT_SELECT_ASSOCIATIVE选择类型的COMDAT部分。
全局对象属性有一定的限制。当针对COFF时候,它,或别名,必须有和COMDAT组一样的名称。这个对象的内容和大小可能在链接时根据选择类型决定选择COMDAT组。因为对象名称必须与COMDAT组名称匹配,全局对象的链接器不能是局部的;如果局部符号在符号表与其他的冲突了可以修改名称。
COMDAT和部分属性的结合使用可以产生令人惊讶的结果。例如:
$foo = comdat any
$bar = comdat any
@g1 = global i32 42, section "sec", comdat($foo)
@g2 = global i32 42, section "sec", comdat($bar)
根据对象文件观点,这需要创建具有相同名称的两个部分。在对象文件层次,因为属于不同的COMDAT组和COMDAT的全局通过部分表现,所以这是必要的。
注意,特定中间语言构造全局变量和函数可以在除了任何使用COMDAT中间语言指定的对象文件创建COMDAT。这是当代码生成器被配置为在个别部分发出全局(比如-data-sections或者-function-sections支持llc)。
命名元数据是一个元数据的集合。元数据节点(但非元数据字符串)是唯一对于具名元数据有效的操作数。
1.命名元数据被表示为一个带有元数据前缀的字符串。元数据名称的规则与标识符相同,但不允许引用名称。“\xx”类型的反斜杠仍然有效,它允许任何字符成为名称的一部分。
语法:
; Some unnamed metadata nodes, which are referenced by the named metadata.
!0 = !{!"zero"}
!1 = !{!"one"}
!2 = !{!"two"}
; A named metadata.
!name = !{!0, !1, !2}
返回类型和每一个函数的参数类型可能拥有一个参数属性集。参数属性是用于交流函数返回值和参数的额外信息。可以认为参数类型是函数的一部分,但不是函数类型的一部分,因此用不同参数属性的函数可以拥有相同的函数类型。
参数属性是跟随指定类型后的简单关键字。如果需要多个参数属性,它们需要被空白符分隔开。下面是详细例子:
declare i32 @printf(i8* noalias nocapture, ...)
declare i32 @atoi(i8 zeroext)
declare signext i8 @returns_signed_char()
注意,任何对于函数结果(nounwind, readonly)的属性跟随在参数列表后。
当前仅定义了以下的参数属性:
zeroext
signext
inreg
byval
inalloca
sret
align
noalias
nocapture
nest
returned
每一个函数可以制定一个垃圾回收期的名称,这个名称是一个简单的字符串:
define void @f() gc "name" { ... }
编译器声明了这个名字的可能值。指定一个收集器将会导致编译器会为了支持这个垃圾回收算法修改它的输出。
前缀数据是与函数关联的数据,代码生成器将在函数的入口点之前立即发出该函数。该特性的目的是允许前端将特定于语言的运行时元数据与特定函数关联起来,并通过函数指针使其可用,同时仍然允许调用函数指针。
要访问给定函数的数据,程序可以将函数指针位转换为指向常量类型和取消引用索引-1的指针。这意味着IR符号指向的位置刚好超过前缀数据的末尾。例如,以一个用i32注释的函数为例,
define void @f() prefix i32 123 { ... }
前缀数据可以引用为:
%0 = bitcast void* () @f to i32*
%a = getelementptr inbounds i32, i32* %0, i32 -1
%b = load i32, i32* %a
前缀数据的布局就像它是前缀数据类型的全局变量的初始化器一样。函数的位置将使前缀数据的开头对齐。这意味着如果前缀数据的大小不是对齐大小的倍数,函数的入口点将不会对齐。如果需要对函数的入口点进行对齐,则必须向前缀数据添加填充。
函数可以有前缀数据,但没有正文。这与available_external链接具有类似的语义,因为数据可以由优化器使用,但不会在目标文件中发出。
序言属性允许在函数体之前插入任意代码(以字节编码)。这可以用于启用功能热补丁和检测。
为了维护普通函数调用的语义,序言数据必须具有特定的格式。具体地说,它必须从一个字节序列开始,这个字节序列解码为一个机器指令序列,该指令序列对模块的目标有效,它将控制传输到紧接着序言数据的位置,而不执行任何其他可见操作。这允许内联程序和其他传递来推断函数定义的语义,而不需要推断序言数据。显然,这使得序言数据的格式高度依赖于目标。
x86架构的有效开场白数据的一个简单例子是i8 144,它编码了nop指令:
define void @f() prefix i8 144 { ... }
一般来说,前置数据可以跳过元数据通过编码一个相关的branch指令,就像下面这个例子中的对于X86_64体系有效的前置数据,它的前两个byte将被编码jmp.+10
%0 = type <{ i8, i8, i8* }>
define void @f() prologue %0 <{ i8 235, i8 8, i8* @md}> { ... }
函数可能只拥有前置数据但没有主体。这与 available_externally 链接标识有着相同的语义,即数据可能会被优化器使用的不会被发散到对象文件中。
personality属性允许函数指定用于异常处理的函数。
属性组是在IR中的对象引用的属性的集合。他们对于保持 .ll 可读有着重要意义,因为许都函数会使用相同的属性集。在退化的情况下一个 .ll 文件对应着单一的 .c 文件,这个单一的属性组讲捕获那些用于构建这个文件的重要命令行标识。
一个属性组是一个模块层次的对象。要使用一个属性组,一个对象可以引用这个属性组的ID(例如 #37)。一个对象可能引用超过一个属性组。在这种情况下来自于不同属性组的属性会被合并。
这里有一个规定一个函数应该内联的属性组的例子,并拥有一个堆栈对齐属性值为4,且不使用SSE指令
; Target-independent attributes:
attributes #0 = { alwaysinline alignstack=4 }
; Target-dependent attributes:
attributes #1 = { "no-sse" }
; Function @f has attributes: alwaysinline, alignstack=4, and "no-sse".
define void @f() #0 #1 { ... }
函数属性用于函数交流额外信息的。函数属性被认为是函数的一部分,而不是函数类型的一部分,所以拥有不同函数属性的函数可以对应着同一个函数类型。
函数属性是紧跟在指定的函数类型后的简单的关键字。如果以后需要指定多个函数属性,那么使用空格符分隔它们。例如:
define void @f() noinline { ... }
define void @f() alwaysinline { ... }
define void @f() alwaysinline optsize { ... }
define void @f() optsize { ... }
这将覆盖 ssp 功能属性。
如果一个函数,它有一个 sspstrong 属性被内联到一个不具有 sspstrong 属性的函数,那么最后得到的函数将具有 sspstrong 属性。
属性可以设置为传递关于全局变量的附加信息。与函数属性不同,全局变量上的属性被分组为单个属性组。
操作数包是一组标记的SSA值,它们可以与某些LLVM指令相关联(目前仅调用s和调用s)。
语法:
operand bundle set ::= '[' operand bundle (, operand bundle )* ']'
operand bundle ::= tag '(' [ bundle operand ] (, bundle operand )* ')'
bundle operand ::= SSA value
tag ::= string constant
操作数束不是函数签名的一部分,给定的函数可以从具有不同类型操作数束的多个位置调用。这反映了一个事实,即操作数包在概念上是调用(或调用)的一部分,而不是被分派到的被调用方。
操作数包是一种通用机制,用于支持托管语言的运行时自检类功能。虽然操作数捆绑包的确切语义依赖于捆绑包标记,但是对于操作数捆绑包的存在对程序语义的影响程度有一定的限制。这些限制被描述为“未知”操作数包的语义。只要操作数包的行为在这些限制中是可描述的,LLVM就不需要对操作数包有专门的知识,就不会编译包含它的程序。
下面将描述更具体的操作数束类型。
反优化操作数束的特征是“deopt”操作数束标记。这些操作数包表示它们附加到的调用站点的另一种“安全”延续,合适的运行时可以使用它们在指定的调用站点对编译后的框架进行反优化。调用站点最多可以有一个“deopt”操作数包。反优化的具体细节超出了语言引用的范围,但它通常涉及将编译后的框架重写为一组解释框架。
从编译器的角度来看,反优化操作数包使它们附加到的调用站点至少为只读。它们读取所有指针类型的操作数(即使它们没有转义)和整个可见堆。反优化操作数束不会捕获它们的操作数,除非在反优化期间,在这种情况下,控制将不会返回到编译后的框架。
内联程序知道如何内联通过具有反优化操作数包的调用。就像通过一个普通调用站点进行内联涉及到组合正常延续和异常延续一样,通过一个具有反优化操作数包的调用站点进行内联需要适当地组合“安全”反优化延续。内联程序通过将父类的反优化延续前置到内联体中的每个反优化延续来实现这一点。例如,在下面的例子中把@f内联到@g中
define void @f() {
call void @x() ;; no deopt state
call void @y() [ "deopt"(i32 10) ]
call void @y() [ "deopt"(i32 10), "unknown"(i8* null) ]
ret void
}
define void @g() {
call void @f() [ "deopt"(i32 20) ]
ret void
}
将导致
define void @g() {
call void @x() ;; still no deopt state
call void @y() [ "deopt"(i32 20, i32 10) ]
call void @y() [ "deopt"(i32 20, i32 10), "unknown"(i8* null) ]
ret void
}
前端的职责是构造或编码反优化状态,使调用方的反优化状态在语法上前置到被调用方的反优化状态,在语义上等价于在被调用方的反优化延续之后组合调用方的反优化延续。
函数操作数包的特征是“函数”操作数包标记。这些操作数包表示调用站点位于特定函式内。一个调用站点最多可以有一个“funclet”操作数包,而且它必须恰好有一个操作数包。
如果任何funclet EH pad已经“输入”但没有“退出”(根据EH doc中的描述),则执行以下调用或调用是未定义的行为:
类似地,如果没有函数EH pad被导入(但尚未退出),则使用“函数”包执行调用或调用是未定义的行为。
GC转换操作数束的特征是“GC -transition”操作数束标记。这些操作数包将调用标记为具有一个GC策略的函数与具有不同GC策略的函数之间的转换。如果协调GC策略之间的转换需要在调用站点上生成额外的代码,那么这些bundle可能包含生成代码所需的任何值。有关更多细节,请参见GC转换。
LLVM类型系统是中间表示最重要的特性之一。通过类型化,可以直接对中间表示执行许多优化,而不必在转换之前进行额外的分析。一个强大的类型系统可以更容易地读取生成的代码,并支持新的分析和转换,而这些分析和转换在普通的三地址码表示上是不可行的。
概述:
void类型不代表任何值,也没有大小。
语法:
void
概述:
函数类型可以看作是函数签名。它由一个返回类型和一个形式参数类型列表组成。函数类型的返回类型是void类型或第一类类型 —— label
和 metadata
类型除外。
语法:
()
其中'
是一个类型说明符的逗号分隔列表。可选地,参数列表可以包含一个类型 …
,表示函数接受可变数量的参数。变量参数函数可以使用变量参数处理内在函数来访问它们的参数。'
是除 label
和 metadata
类型之外的任何类型。
第一类类型可能是最重要的。这些类型的值是唯一可以由指令生成的值。
从 ·CodeGen· 的角度来看,这些类型在寄存器中是有效的。
概述:
整数类型是一种非常简单的类型,它为所需的整数类型指定任意的位宽。可以指定从1位到2^23-1(约800万)之间的任何位宽。
语法:
iN
该整数将占用的位数由 N
值指定。
例子:
i1是一个1位的整数。
i32是一个32位的整数。
i1942652是一个非常大的整数,超过一百万比特。
half、float、double和fp128的二进制格式分别对应于binary16、binary32、binary64和binary128的IEEE-754-2008规范。
概述:
x86_mmx
类型表示在 x86 机器上的 MMX 寄存器中持有的值。它所允许的操作非常有限:参数和返回值、加载和存储以及位转换。用户指定的MMX指令表示为带有这种类型的参数和/或结果的内部调用或asm调用。没有这种类型的数组、向量或常量。
语法:
x86_mmx
概述:
指针类型用于指定内存位置。指针通常用于引用内存中的对象。
指针类型可以有一个可选的地址空间属性,该属性定义指向对象所在的编号地址空间。默认的地址空间是number 0。非零地址空间的语义是特定于目标的。
注意,LLVM不允许指针指向void (void*),也不允许指针指向label (label*)。使用i8 *代替。
语法:
*
概述:
向量类型是表示元素向量的简单派生类型。当使用一条指令(SIMD)并行操作多个原语数据时,将使用向量类型。向量类型需要大小(元素数量)、底层基本数据类型和可伸缩属性来表示在编译时不知道确切硬件向量长度的向量。向量类型被认为是第一类。
语法:
< <# elements> x > ; Fixed-length vector
< vscale x <# elements> x > ; Scalable vector
元素个数为大于0的常数整数值;elementtype可以是任何整数、浮点或指针类型。大小为零的向量是不允许的。对于可伸缩向量,元素总数是指定元素数的常数倍(称为vscale);vscale是在编译时未知的正整数,在运行时对所有可伸缩向量都是相同的硬件依赖常数。因此,特定可伸缩向量类型的大小在IR中是恒定的,即使直到运行时才能确定其确切的字节大小。
概述:
标签类型表示代码标签。
语法:
label
概述:
当一个值与一条指令相关联,但该值的所有用法都不能尝试内省或使其模糊时,将使用token类型。因此,使用phi或select of type token是不合适的。
语法:
token
概述:
metadata类型表示嵌入的metadata。除了函数参数外,不能从metadata创建派生类型。
语法:
metadata
聚合类型是派生类型的一个子集,可以包含多个成员类型。Arrays 和 Structs 是聚合类型。Vectors 不被认为是聚合类型。
概述:
数组类型是一种非常简单的派生类型,它按顺序排列内存中的元素。数组类型需要一个大小(元素的数量)和一个底层数据类型。
语法:
[<# elements> x ]
<# elements>
元素个数为常数整数值;elementtype
可以是任何具有大小的类型。
对于索引超出静态类型所暗示的数组末端没有限制(尽管在某些情况下对于索引超出已分配对象的边界有限制)。这意味着一维“可变大小数组”寻址可以在零长度数组类型的LLVM中实现。例如,LLVM中“pascal风格数组”的实现可以使用“{i32, [0 x float]}”类型。
概述:
结构类型用于表示内存中的数据成员集合。结构的元素可以是任何具有大小的类型。
通过使用’ getelementptr ‘指令获得指向字段的指针,可以使用’ load ‘和’ store '访问内存中的结构。使用“extractvalue”和“insertvalue”指令访问寄存器中的Structures。
结构可以选择“打包”结构,这表示结构的对齐方式是一个字节,并且元素之间没有填充。在非打包结构中,字段类型之间的填充是由模块中的DataLayout字符串定义的,这是匹配底层代码生成器所需的内容所必需的。
结构可以是“文字的”,也可以是“标识的”。文字结构是与其他类型(例如{i32, i32}*)内联定义的,而标识类型总是在顶层用名称定义。文字类型是由它们的内容所独特的,并且永远不可能是递归的或不透明的,因为没有方法来编写它们。识别的类型可以是递归的,可以是不透明的,而且从来都不是惟一的。
语法:
%T1 = type { } ; Identified normal struct type
%T2 = type <{ }> ; Identified packed struct type
概述:
不透明结构类型用于表示没有指定主体的命名结构类型。这对应(例如)前向声明结构的C概念。
语法:
%X = type opaque
%52 = type opaque