背景图

LLVM语言参考手册(开始至高级结构)

概要

本文档是LLVM汇编语言的参考手册。LLVM是一种基于静态单赋值(SSA)的表现形式的语言。它提供类型安全、底层指令、灵活性以及干净地表示“所有”高级语言的能力。

介绍

LLVM代码表示被设计为以三种不同的形式使用:作为内存中编译器IR(中间码),作为磁盘上的位代码表示(适用于即时编译器进行快速加载)以及作为人可读汇编语言表达。这允许LLVM为有效的编译器的转换和分析提供强大的中间表示,同时提供调试和可视化转换的自然方式。LLVM的三种不同形式都是等价的。本文档描述了人类可读的表示形式和符号。

LLVM表示旨在轻量级和低级别,同时具有表现力,类型化和可扩展性。它的目标是成为一种“通用的IR”,通过处于足够低的水平,可以将高层次的想法清晰地映射到它(类似于微处理器是“通用IR”),允许将许多源语言映射到它们)。通过提供类型信息,LLVM可以用作优化的目标:例如,通过指针分析,可以证明C自动变量从不在当前函数之外访问,从而允许将它提升为简单的SSA值而不是一个内存位置。

良好的格式

重要的是要注意,本文档描述了“良好格式”的LLVM汇编语言。 解析器接受什么和被认为是“良好格式”之间是有区别。例如,下面的指令在语法上没问题,但不是良好形式:

1
%x = add i32 1, %x

因为%x的定义并不控制它的所有用途。 LLVM基础架构提供了一个验证过程,可用于验证LLVM模块是否格式良好。在解析输入的汇编程序之后和优化程序在输出bitcode之前,该过程由解析器自动运行。验证程序验证过程中指出的违规暗示转换过程中的错误或输入到解析器中的错误。

标识符

LLVM标识符有两种基本类型:全局和本地。全局标识符(函数,全局变量)以'@'字符开头。本地标识符(寄存器名称,类型)以'%'字符开头 。此外,有三种不同的标识符格式,用于不同的目的:

  1. 命名值用一串字符和前缀表示。例如%foo@DivisionByZero%a.really.long.identifier。实际使用的正则表达式是 '[%@][-a-zA-Z$._][-a-zA-Z$._0-9]*'。在名称中需要其他字符的标识符可以用引号包裹起来。通过使用"\xx",特殊字符可以被转义,xx是ASCII代码的十六进制字符的表示。这样,任何字符都可以用于名称值,甚至可以引用它们自己。全局变量可以使用前缀”\01”来阻止截断。
  2. 未命名的值用前缀表示为无符号数值。例如%12@2%44
  3. 常量,在下面的常量章节中进行了介绍。

LLVM要求值以一个前缀开始,原因有两个:编译器不需要担心名称与保留字的冲突,并且保留字的集合可能在将来被扩展而不会产生什么不利。此外,未命名标识符允许编译器快速创建临时变量,而不必设法避免符号表冲突。

LLVM中的保留字与其他语言中的保留字非常相似。对于不同的操作码('add''bitcast''ret'等等),对于原始类型名称('void''i32'等等),以及其他的关键字。这些保留字不能与变量名冲突,因为它们都没有以前缀字符('%''@')开始。

以下是将整数变量“%X’乘以8的LLVM代码示例:
简单的方式:

1
%result = mul i32 %X, 8

减少强度之后(也是最推荐的方式):

1
%result = shl i32 %X, 3

最复杂的方式是(很有意思):

1
2
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的几个重要的词法特征:

  1. 注释用分号';'分隔,直到行尾。
  2. 当计算结果未分配给指定命名值时,将创建未命名的临时对象。
  3. 未命名的临时对象按序号进行编号(使用预处理函数递增计数器,从0开始)。请注意,此编号中包含基本块和未命名的函数参数。例如,如果基本块入口没有给出标签名称并且所有函数参数都被命名,则它将得到编号0。

它也显示了我们在本文件中遵循的惯例。在展示说明时,我们将按照说明书的注释来说明所产生值的类型和名称。

高级结构

模块结构

LLVM程序由Module's组成,每个模块都是输入程序的翻译单元。每个模块由函数,全局变量和符号表入口(symbol table entries)组成。模块可以与LLVM链接器组合在一起,LLVM链接器合并函数(和全局变量)定义,解析前置声明并合并符号表入口。以下是“hello world”模块的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 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“命名元数据组成。

通常,模块由全局值列表组成(其中函数和全局变量都是全局值)。全局值由指向内存位置的指针表示(在本例中,指向char数组的指针和指向函数的指针),并且具有以下链接类型之一。

链接类型

所有全局变量和函数都具有以下链接类型之一:

private

具有“private”连接的全局值只能由当前模块中的对象直接访问。特别是,将代码链接到具有private全局值的模块中可能会导致private变量重新命名以避免冲突。由于符号对模块是私有的,因此所有引用都可以更新。这不会在对象文件的任何符号表中显示。

internal

与私有类似,但该值在对象文件中显示为本地符号(STB_LOCAL在ELF的情况下)。这就像C中的'static'关键字。

available_externally

具有“available_externally”链接的全局程序永远不会被发送到对应于LLVM模块的目标文件中。从链接器的角度来看,available_externally全局相当于external声明。它们的存在是为了允许内联和其他优化在知道全局变量(函数或语句块)的定义的情况下发生,这被称为模块之外的某个地方。具有available_externally链接的全局值可以随意丢弃,并允许内联和其他优化。这种连接类型只允许定义,而不允许声明。

linkonce

linkonce连接的全局值在链接发生时,可以与其他同名的全局值合。这可以用来实现某些形式的内联函数,模板(templates)或其他代码,这些代码必须在使用它的每个翻译单元中生成,但是稍后可能会在更明确的定义中覆盖正文。未引用的linkonce全局变量允许被丢弃。请注意,linkonce链接实际上并不允许优化器将此函数的主体内联到调用者中,因为它不知道该函数的定义是否是程序中的权威定义,或者是否会被更强的定义覆盖。要启用内联和其他优化器,请使用“linkonce_odr”链接。

weak

“weak”链接具有与linkonce链接相同的合并语义,除了未引用weak链接的全局可能不会被丢弃。这用于在C源代码中声明为weak的全局变量。

common

common连接与weak连接最为相似,但它们用于C中的临时定义,例如:全局作用域的int x。带有common连接的符号与weak是相同的方式合并;如果未引用common符号,它们可能不会被删除。common符号可能没有明确的部分,必须具有零初始化器,并且可能未被标记为“constant”。函数和别名可能没有共同的连接。

appending

appending”链接可能只适用于指向数组类型的全局变量。当两个具有appending链接的全局变量链接在一起时,这两个全局数组被附加在一起。这是LLVM类型安全的,当.o文件被链接时,系统链接器将具有相同的名称的“sections”附加在一起。

不幸的是,这不符合.o文件中的任何特性,所以它只能用于像llvm.global_ctors的变量,llvm专门解释这样的变量。

extern_weak

这个链接的语义遵循ELF目标文件模型:符号在链接之前是弱的,如果不链接,符号变为空,而不是未定义的引用。

linkonce_odrweak_odr

有些语言允许合并不同的全局变量,比如两个具有不同语义的函数。其他语言,例如C++,确保只有等效的全局变量被合并(“一个定义规则(one definition rule)” - “ODR”)。这些语言可以使用linkonce_odrweak_odr连接类型来表示全局将仅与等效的全局变量合并。对于其他不支持ODR的语言,这些链接类型与其非odr的链接相同。

external

如果没有使用上述标识符,则全局变量是外部可见的,这意味着它参与链接并可用于解析外部符号引用。

对于函数声明,除externalextern_weak之外的任何链接类型都是非法的。

调用约定

LLVM函数(functions)调用(calls)调用(invokes)都可以为该调用指定一个可选的调用约定。任何一对动态调用者(caller)/被调用者(callee)的调用约定都必须匹配,否则程序的行为是未定义的。LLVM支持以下调用约定,未来还可能添加更多调用约定:

ccc - C调用约定

这个调用约定(如果没有指定其他调用约定,则默认是这个调用约定)匹配目标C调用约定。这个调用约定支持可变参数函数调用,并且允许在声明的原型和实现的函数声明中存在一些不匹配(就像普通的C一样)。

fastcc” - 快速调用约定

这个调用约定试图尽可能快地进行调用(例如通过在寄存器中传递参数)。这个调用约定允许目标使用任何想要为目标生成快速代码的技巧,而不必遵守外部指定的ABI(应用程序二进制接口)。只有在使用此调用约定、GHC调用约定或HiPE调用约定时,才能优化尾部调用。此调用约定不支持可变参数,并要求所有被调用者的原型与函数定义的原型完全匹配。

coldcc” - 冷调用约定

这个调用约定尝试在调用不是共同执行的条件下尽可能高效地在调用者中编写代码。因此,这些调用通常会保留所有的寄存器,以便调用不会破坏调用方的任何实时范围(live range)。此调用约定不支持可变参数,并要求所有被调用者的原型与函数定义的原型完全匹配。此外,内联器不考虑这种内联函数调用。

cc 10” - GHC约定

这个调用约定专门供格拉斯哥Haskell编译器(GHC)使用。它在寄存器中的传递所有内容,通过禁用被调用者保存寄存器来达到极限。这种调用约定不应该被轻率使用,而只能用于特定情况下,例如替代实现函数式编程语言时经常使用的寄存器锁定性能技术。目前只有X86支持这种约定,它有以下限制:

  • 在X86-32上最多只支持4位类型参数。不支持浮点类型。
  • 在X86-64上最多只支持10位类型参数和6个浮点参数。

这种调用约定支持尾部调用优化,但要求主调方和被调方都在使用它。

cc 11” - HiPE调用约定

该调用约定专门用于高性能Erlang(HiPE)编译器,即爱立信开源Erlang/OTP系统的本地代码编译器。它比通常的C调用约定使用更多的寄存器进行参数传递,并且不定义被调者使用的保存寄存器。调用约定正确支持尾部调用优化,但要求调用者和被调用者都使用它。它使用寄存器固定机制,与GHC调用约定类似,用于将频繁访问的运行时组件固定到特定的硬件寄存器。目前只有X86支持这种约定(32位和64位)。

webkit_jscc” - WebKit的JavaScript调用约定

此调用约定已针对WebKit FTL JIT实施。它将堆栈上的参数从右向左传递(如cdecl那样),并在平台的惯用返回寄存器中返回一个值。

anyregcc” - 代码修补的动态调用约定

这是一个特殊的约定,它支持在调用点增补任意代码序列。这个约定强制调用参数进入寄存器,但允许它们动态分配。目前只能用于调用llvm.experimental.patchpoint,因为只有这个内部函数将其参数的位置记录在旁边表中。请参阅LLVM中的堆栈映射和修补程序点

preserve_mostcc” - PreserveMost调用约定

此调用约定会尽可能使调用方中的代码尽可能是非侵入性的。该约定的行为与C 调用约定的参数和返回值的传递方式相同,但它使用了一组不同的调用方/被调用方保存的寄存器。这减轻了在调用者调用之前和之后保存和恢复大型寄存器组的负担。如果参数在被调用方保存的寄存器中传递,那么它们将在整个调用过程中由被调用方保存。这不适用于在被调用方保存的寄存器中返回的值。

  • 在X86-64上,被调用方保存所有通用寄存器,R11除外。R11可以用作临时寄存器。浮点寄存器(XMMs/YMMs)不会保留,需要由调用者保存。

这个约定背后的想法是支持对具有热路径和冷路径的运行时(Runtime)函数调用。热路径通常是一小段不使用多个寄存器的代码。冷路径可能需要调用另一个函数,因此只需要保留调用者保存的寄存器,这些寄存器还未被调用者保存。就调用者/被调用者保存的寄存器方面,PreserveMost调用约定是与冷(cold)调用约定非常相似的,但它们用于不同类型的函数调用。coldcc适用于很少执行的函数调用,而preserve_mostcc函数调用旨在处于热路径上,并且相对执行更多一些。此外preserve_mostcc不会阻止内联器的内联函数调用。

这个调用约定将被未来版本的ObjectiveC运行时使用,因此此时应该仍被认为是实验性的。虽然此惯例是为了优化对ObjectiveC运行时的某些运行时调用而创建的,但它并不局限于此运行时,并且在将来也可能会被其他运行时使用。目前的实现只支持X86-64,但其目的是在未来支持更多架构。

preserve_allcc” - PreserveAll调用约定

此调用约定会尝试使调用方中的代码比PreserveMost调用约定更不具侵入性。此调用约定的行为与C调用约定的参数和返回值的传递方式相同,但它使用一组不同的调用方/被调用方保存的寄存器。这消除了在调用者的调用之前和之后保存和恢复大型寄存器组的负担。如果参数在被调用方保存的寄存器中传递,那么它们将在整个调用过程中由被调用方保存。这不适用于在被调用方保存的寄存器中返回的值。

  • 在X86-64上,被调用方保存所有通用寄存器,R11除外。R11可以用作临时寄存器。此外,它还保留所有浮点寄存器(XMM/YMM)。

这个约定背后的想法是支持对不需要调用任何其他函数的运行时(Runtime)函数的调用。

此调用约定与PreserveMost调用约定一样,将被未来版本的ObjectiveC运行时使用,此时应视为实验性的。

cxx_fast_tlscc” - 访问函数的CXX_FAST_TLS调用约定

Clang生成一个访问函数来访问C++风格的TLS。访问函数通常有一个入口块,一个出口块和一个初次运行的初始化块。入口和出口块可以访问一些TLS IR变量,每个访问将被降低到平台特定的序列。

此调用约定旨在通过保留尽可能多的寄存器(所有寄存器保存在快速路径中,由入口和出口块组成)来尽量减少调用者的开销。

这个调用约定的行为与C调用约定在参数和返回值的传递方式上是一样的,但它使用了一组不同的调用者/被调用者保存的寄存器。

鉴于每个平台都有自己的降序序列,因此它有自己的一组保存的寄存器,所以我们不能使用现有的PreserveMost

  • 在X86-64上,被调用方保存所有通用寄存器,RDIRAX除外。

swiftcc” - 这个调用约定用于Swift语言。

  • 在X86-64上,RCXR8可用于额外的整数返回,并且XMM2XMM3可用于其他FP/vector返回。
  • 在iOS平台上,我们使用AAPCS-VFP调用约定。
    cc <n>” - 编号调用约定
    任何调用约定都可以用数字指定,从而允许使用特定于目标的调用约定。目标特定调用约定从64开始。

可以根据需要添加/定义更多调用约定,以支持Pascal约定或任何其他众所周知的目标独立的调用约定。

可见性风格

所有全局变量和函数都具有以下可见性样式之一:

default” - 默认风格

在使用ELF(Executable and Linking Format,可执行文件)对象文件格式的目标上,默认可见性意味着该声明对其他模块可见,并且在共享库中,意味着声明的实体可能被覆盖。在Darwin上,default可见性意味着该声明对其他模块可见。default可见性对应于语言中的“外部链接”。

hidden” - 隐藏的风格

具有隐藏可见性的对象的两个声明指向同一个对象,如果它们在同一个共享对象中。通常,隐藏的可见性表示该符号不会被放入动态符号表中,因此其他模块(可执行文件或共享库)不能直接引用它。

protected” - 受保护的风格

ELF上,protected可见性表示该符号将被放置在动态符号表中,但定义的模块内的引用将绑定到本地符号。也就是说,该符号不能被另一个模块覆盖。

带有internalprivate链接类型的符号必须具有default可见性。

DLL存储类

所有全局变量,函数和别名都可以具有以下DLL存储类之一:

dllimport

dllimport”会导致编译器通过全局指针引用函数或变量,该全局指针指向由导出符号的DLL设置的指针。在Microsoft Windows目标上,指针名称由__imp_和函数或变量名称组成。

dllexport

dllexport”会导致编译器提供一个指向DLL中的指针的全局指针,以便它可以引用dllimport属性。在Microsoft Windows目标上,指针名称由__imp_和函数或变量名称组成。由于这个存储类是为了定义dll接口而存在,因此编译器,汇编器和链接器知道它是从外部引用的,并且不会删除该符号。

其实这个指令是用来加载动态链接库,相关的信息可以查看:DllImport doesn’t work as advertised in Mono (Linux, C#)

线程局部存储(TLS)模型

一个变量可以被定义为thread_local,这意味着它不会被线程共享(每个线程将有一个变量的分离副本)。并非所有的目标机都支持线程局部变量。可选地,可以指定TLS模型

  • localdynamic: 对于仅在当前共享库中使用的变量。
  • initialexec: 对于模块中不会动态加载的变量。
  • localexec: 对于在可执行文件中定义的变量,只能在其中使用。

如果没有给出显式模型,则使用“general dynamic”模型。

这些模型对应于ELF TLS模型; 请参阅ELF对thread_local存储的处理获取更多信息,在这篇文章中提到的不同的场景下可能使用不同的模型。如果指定的TLS模型不受支持,或者可以选择一个更好的模型,则目标机可能会选择不同的TLS模型。

模型也可以在别名中指定,但它只能控制别名的访问方式。这对使用别名的地方不会有任何影响。

对于没有ELF TLS模型链接器支持的平台,-femulated-tls标志可用于生成GCC兼容的模拟TLS代码。

运行时抢占说明

全局变量,函数和别名可以有一个可选的运行时抢占说明符。如果没有明确给出抢先说明符,则假定符号是dso_preemptable

dso_preemptable

表示在运行时,可以用链接单元外部的符号替换该函数或变量。

dso_local

编译器可以假定标记为dso_local的函数或变量将解析为相同链接单元中的符号。即使定义不在此编译单元中,也会生成直接访问。

结构类型

LLVM IR(中间码)允许您指定“可识别(identified)”和“文字(Literal)”结构类型。文字类型在结构上是独一无二的,但是identified类型从来都不是独一无二的。一个不透明的结构类型也可以用来向前声明一个还不能使用的类型。

identified(可识别的)结构规范的一个例子是:

1
%mytype = type { %mytype*, i32 }

在LLVM 3.0发布之前,identified类型在结构上是独一无二的。在最新版本的LLVM中,只有文字类型是唯一的。

非整型指针类型

注意:非整型指针类型是正在进行的工作,并且此时它们应该被认为是实验性的。

LLVM IR可选地允许前端通过数据布局字符串将某些地址空间中的指针表示为“非整数” 。非整型指针类型表示具有未指定的按位表示的指针; 也就是说,完整性表示可能是目标机相关的或不稳定(不由固定整数支持)。

inttoptr指令将整数转换为非整型指针类型的ptrtoint指令是错误类型(ill-typed)的,ptrtoint指令将非整型指针类型的值转换为整数。所述指令的矢量版本也是不正确的(ill-typed)。

全局变量

全局变量定义了编译时分配的内存区域,而不是运行期。

全局变量定义必须被初始化。

其他编译单元中的全局变量也可以声明,在这种情况下,它们没有初始化程序。

无论是全局变量定义还是声明都可以有一个显式的部分放在里面,并且可以有一个可选的显式对齐。如果变量声明的显式或推断区段信息与其定义之间存在不匹配,则结果行为未定义。

一个变量可以被定义为一个全局constant,它表示变量的内容永远不会被修改(开启更好的优化,允许全局数据被放置在可执行文件的只读部分等)。请注意,需要运行时初始化的变量不能标记constant,因为变量在存储区中。

LLVM明确允许将全局变量的声明标记为常量,即使全局变量的定义中没有指定。此功能可用于对程序进行稍微更好的优化,但需要语言定义以保证基于“constantness”的优化对于在定义中不包含constant的编译单元有效。

作为SSA(静态单赋值)值,全局变量定义了程序中所有基本块的范围内的指针值(即它们是可控的)。全局变量总是定义一个指向其“内容”类型的指针,因为它们描述了一个内存区域,并且LLVM中的所有内存对象都是通过指针访问的。

全局变量可以用unnamed_addr来标记,表示地址不重要,只有内容。如果它们具有相同的初始化程序,标记为unnamed_addr的常量可以与其他常量合并。请注意,一个具有显着地址的常量可以与一个unnamed_addr常量合并,结果是一个地址明确的(significant)常量。

如果给出local_unnamed_addr属性,则这个属性的地址在模块内是不明确的。

全局变量可能被声明为驻留在特定目标机的编号地址空间中。对于支持它们的目标机,地址空间可能会影响优化的执行方式,可能也会影响使用什么目标机指令访问变量。默认地址空间为零。地址空间限定符必须在任何其他属性之前。

LLVM允许为全局指定显式部分(section)。如果目标机支持它,它会将globals发送到指定的section。此外,如果目标机有必要的支持,全局变量可以放在一个comdat中。

外部声明可能有明确的section指定。对于使用此信息的目标机器,section信息保留在LLVM IR中。将section信息附加到外部声明是一个断言,其定义位于指定的section。如果定义位于不同的section,则行为是未定义的。

默认情况下,全局初始化器通过假设在全局初始化器开始之前在模块内定义的全局变量未从其初始值修改而得到优化。即使对于可能从模块外部访问的变量(包括具有外部链接的或者出现在@llvm.useddllexported中的变量)也是如此。这个假设可以通过将变量标记为externally_initialized来消除。

可以为全局变量指定明确的对齐方式,它必须是2的幂。如果不存在,或者如果对齐方式设置为零,则全局对齐由目标机器设置为任何方便的方式。如果指定了明确的对齐,则全局被强制完全按照指定的对齐方式。如果全局变量具有分配的section,则不允许目标机和优化器过度对齐(over-align)全局变量。在这种情况下,额外的对齐方式可以被观察到:例如,代码可以假定全局变量被密集包装在它们的section中,并尝试将它们作为数组进行迭代,对齐填充将会破坏这个迭代。最大对齐是1 << 29.

全局变量还可以具有DLL存储类,可选的运行时抢占说明符,可选的全局属性和可选的附加元数据列表。

变量和别名可以有一个线程本地存储模型

句法(这个例子要好好学习,也包含了上面的知识):

1
2
3
4
5
6
7
@<GlobalVarName> = [Linkage] [PreemptionSpecifier] [Visibility]
[DLLStorageClass] [ThreadLocal]
[(unnamed_addr|local_unnamed_addr)] [AddrSpace]
[ExternallyInitialized]
<global | constant> <Type> [<InitializerConstant>]
[, section "name"] [, comdat [($name)]]
[, align <Alignment>] (, !name !N)*

例如,下面在带有初始值设定项,节(section)和对齐的编号地址空间中定义全局变量:

1
@G = addrspace(5) constant float 1.0, section "foo", align 4

以下示例仅声明一个全局变量

1
@G = external global i32

以下示例使用initialexecTLS模型定义了一个thread-local全局变量:

1
@G = thread_local(initialexec) global i32 0, align 4

函数

LLVM函数定义包括了“的define”关键字,可选的链接类型,可选的运行时间抢占标识符,可选的可见风格,可选的DLL存储类,可选的调用约定,一个可选的unnamed_addr属性,返回类型,可选的返回类型参数属性,函数名称,(可能为空的)参数列表(每个参数都带有可选的参数属性),可选的函数属性,可选的section(节),可选的对齐,可选的comdat,可选的垃圾收集器名称,可选的前缀,可选的序言,可选的个性,附加元数据的可选列表,开启大括号,基本块列表以及关闭大括号。

LLVM函数声明由“declare”关键字,可选的链接类型,可选的可见性样式,可选的DLL存储类,可选的调用约定,可选的unnamed_addrlocal_unnamed_addr属性,返回类型,返回类型的可选参数属性,函数名称,可能为空的参数列表,可选的对齐方式,可选的垃圾收集器名称,可选的前缀以及可选的序言

一个函数定义包含一个基本块列表,形成该函数的CFG(控制流图)。每个基本块可以有选择地以一个标签开始(赋予基本块一个符号表入口),包含指令列表,并以终止指令(如分支或函数返回)结束。如果未提供显式标签,一个块被赋值给一个隐含的编号标签,编号使用从计数器中返回下一个值,就像用于未命名的临时对象那样(参见上文)。例如,如果函数入口块没有明确的标签,则会分配标签“%0”,那么该块中的第一个未命名的临时块将为“%1”,以此类推。

函数中的第一个基本块在两个方面是特殊的:在函数入口时立即执行,并且不允许有祖先基本块(即不能有任何分支到函数的入口块)。由于该块可以没有前驱,它也不能有任何PHI节点

LLVM允许为函数指定显式section。如果目标机支持它,它将函数发送给指定的section。另外,该函数可以放置在COMDAT中。

可以为函数指定明确的对齐方式。如果不存在,或者如果对齐方式设置为零,则函数的对齐由目标机设置为任何感觉方便的方式。如果指定了明确的对齐方式,则该函数被强制至少具有那么多的对齐。所有对齐必须是2的幂。

如果unnamed_addr给出该属性,则知道该地址不重要,并且可以合并两个相同的函数。

如果local_unnamed_addr给出该属性,则该地址在模块内是不明显的。

句法:

1
2
3
4
5
6
define [linkage] [PreemptionSpecifier] [visibility] [DLLStorageClass]
[cconv] [ret attrs]
<ResultType> @<FunctionName> ([argument list])
[(unnamed_addr|local_unnamed_addr)] [fn Attrs] [section "name"]
[comdat [($name)]] [align N] [gc] [prefix Constant]
[prologue Constant] [personality Constant] (!name !N)* { ... }

参数列表是逗号分隔的参数序列,其中每个参数具有以下形式:

句法:

1
<type> [parameter Attrs] [name]

别名

与函数或变量不同,别名不会创建任何新数据。它们只是现有位置的新符号和元数据。

别名有一个名称和别名,可以是全局值或常量表达式。

别名可能具有可选的链接类型,可选的运行时抢占说明符,可选的可见性样式,可选的DLL存储类和可选的tls模型

句法:

1
@<Name> = [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] alias <AliaseeTy>, <AliaseeTy>* @<Aliasee>

链接必须是一个privateinternallinkonceweaklinkonce_odrweak_odrexternal。请注意,某些系统链接器可能无法正确处理丢弃具有别名的弱符号(就是内存溢出的问题)。

不是unnamed_addr的别名保证具有与别名表达式相同的地址。unnamed_addr只保证指向相同的内容。

如果local_unnamed_addr给出该属性,则该地址在模块内不显著。

由于别名只是第二个名称,因此有些限制适用,其中一些只能在生成对象文件时进行检查:

  • 定义别名的表达式必须在汇编时可计算。因为它只是一个名字,不能使用重定位。
  • 表达式中的别名不会很弱,因为中间别名被覆盖的可能性无法在对象文件中表示。
  • 表达式中的全局值不能是一个声明,因为这需要重定位,这是不可能的。

IFuncs

IFuncs就像别名一样,不会创建任何新的数据或func。它们只是动态链接器通过调用解析器函数在运行时解析的一个新符号。

IFuncs有一个名称和一个解析器,它是动态链接器调用的函数,它返回与该名称关联的另一个函数的地址。

IFunc可能具有可选的链接类型和可选的可见性样式

句法:

1
@<Name> = [Linkage] [Visibility] ifunc <IFuncTy>, <ResolverTy>* @<Resolver>

Comdats

Comdat IR提供对COFF和ELF目标文件COMDAT功能的访问。

关于什么是COMDAT,在这篇文章中提到COMDAT,即common data. 编译器将一些函数(具体是哪些函数,编译器自行决定)打包放到单独的section中,这有个专有名词叫COMDAT,即common data,意思是打包的函数或者打包的数据。按微软大拿Raymond Chen的说法,COMDAT这个概念最早来自FORTRAN语言。gcc和llvm对COMDAT都有对应的支持。链接器在链接阶段,可以对COMDAT中重复的函数进行消重(folding,折叠)。如果编译器不把函数打包成COMDAT项,链接器是不敢贸然优化掉对应的函数的,因为缺少这些函数的引用信息。

Comdats有一个代表COMDAT键的名称。如果链接器选择了某个其他键的键,则指定的这个键的所有全局对象只会在最终的对象文件中结束。如果有别名,别名将放置在相同的COMDAT中以及进行别名计算。

Comdats有一种选择类型来提供关于链接器如何在两个不同对象文件中的键之间进行选择的输入。

句法:

1
$<Name> = comdat SelectionKind

选择种类必须是以下之一:

  • any: 链接器可以选择任何COMDAT键,选择是任意的。
  • exactmatch: 链接器可以选择任何COMDAT键,但这些section必须包含相同的数据。
  • largest: 链接器将选择包含最大值COMDAT键的section。
  • noduplicates: 链接器要求只有具有此COMDAT密钥的section存在。
  • samesize: 链接器可以选择任何COMDAT键,但这些section必须包含相同数量的数据。

请注意,Mach-O平台不支持COMDAT key,而ELF和WebAssembly仅支持any作为选择类型。

这里是COMDAT组的一个例子,其中只有当COMDAT键的section最大时才会选择一个函数:

1
2
3
4
5
6
$foo = comdat largest
@foo = global i32 2, comdat($foo)

define void @bar() comdat($foo) {
ret void
}

作为一个语法糖,$name如果名称与全局名称相同,则可以省略:

1
2
$foo = comdat any
@foo = global i32 2, comdat

在COFF对象文件中,这将创建一个COMDAT section,它的选择类型是IMAGE_COMDAT_SELECT_LARGEST,包含@foo符号的内容和另一个COMDAT section;这个section的选择类型是IMAGE_COMDAT_SELECT_ASSOCIATIVE,这个选择类型与第一个COMDAT section相关并包含在@bar符号的内容。

全局对象的属性有一些限制。它或它的别名在定位COFF时必须与COMDAT组具有相同的名称。COFF对象的内容和大小可以在链接期间使用,根据选择种类确定选择哪个COMDAT组。因为对象的名称必须与COMDAT组的名称相匹配,所以全局对象的链接不能是本地的; 如果符号表中发生冲突,则可以重命名本地符号。

组合使用COMDATS和段(section)属性可能会产生令人惊讶的结果。例如:

1
2
3
4
$foo = comdat any
$bar = comdat any
@g1 = global i32 42, section "sec", comdat($foo)
@g2 = global i32 42, section "sec", comdat($bar)

从对象文件的角度来看,这需要创建具有相同名称的两个段(section)。这是必要的,因为全局变量属于不同的COMDAT组,在对象文件级别,COMDAT由段(section)表示。

请注意,除了使用COMDAT IR指定的内容之外,某些IR结构(如全局变量和函数)可能会在对象文件中创建COMDAT。当代码生成器配置为在各个段(section)中发出全局变量时(例如, 向llc提供-data-sections-function-sections选项时),就会出现这种情况。

命名元数据

命名的元数据是元数据的集合。元数据节点(但不是元数据字符串)是命名元数据的唯一有效操作数。

  1. 命名的元数据被表示为一个带有元数据前缀的字符串。元数据名称的规则与标识符相同,但引用的名称不被允许。”\xx”类型转义仍然有效,它允许任何字符成为名称的一部分。

句法:

1
2
3
4
5
6
; Some unnamed metadata nodes, which are referenced by the named metadata.
!0 = !{!"zero"}
!1 = !{!"one"}
!2 = !{!"two"}
; A named metadata.
!name = !{!0, !1, !2}

参数属性

返回类型和函数类型的每个参数都可能具有一组与其关联的参数属性。参数属性用于传递有关函数结果或参数的附加信息。参数属性被认为是函数的一部分,而不是函数类型,所以具有不同参数属性的函数可以具有相同的函数类型。

参数属性是遵循指定类型的简单关键字。如果需要多个参数属性,则它们是空格分隔的。例如:

1
2
3
declare i32 @printf(i8* noalias nocapture, ...)
declare i32 @atoi(i8 zeroext)
declare signext i8 @returns_signed_char()

请注意,function result (nounwind, readonly)的任何属性都会立即出现在参数列表之后。

目前,仅定义了以下参数属性:

  • zeroext: 这向代码生成器指示参数或返回值应该被调用者(对于参数)或被调用者(对于返回值)被零扩展到目标机ABI所需的范围。
  • signext: 这向代码生成器指示参数或返回值应该由调用方(对于参数)或被调用方(对于返回值)进行符号扩展到目标机的ABI(通常为32位)所需的范围内。
  • inreg: 这表明这个参数或返回值应该以特定的与目标机相关的方式处理,同时为函数调用或返回发送代码(通常通过将其放入寄存器而不是内存中,尽管有些目标机使用它来区分两个不同类型的寄存器)。使用此属性是特定于目标机的。
  • byval: 这表明指针参数应该真的按值传递给函数。该属性意味着指向者的隐藏拷贝在调用者和被调用者之间建立,所以被调用者无法修改调用者中的值。该属性仅对LLVM指针参数有效。它通常用于按值传递结构和数组,但在指向标量的指针上也是有效的。复制被认为属于调用者而不是被调用者(例如,readonly函数不应写入byval参数)。这不是返回值的有效属性。
    byval属性还支持使用align属性指定对齐方式。它指示要形成的堆栈槽的对齐以及指定给调用点的指针的已知对齐。如果未指定对齐,则代码生成器会进行特定于目标机的假设。
  • inalloca: inalloca参数属性允许调用者采取传出栈参数的地址。一个inalloca参数必须是堆栈内存指针,alloca指令生成堆栈内存。alloca或参数分配也必须使用inalloca关键字进行标记。只有最后一个参数可能具有该inalloca属性,并且该参数保证在内存中传递。
    参数分配最多可以由一个调用使用一次,因为调用可能会释放它。影响参数存储的属性,象inregnestsret,或byvalinalloca属性不能结合这些属性一起使用。inalloca属性还禁止LLVM隐式降低大型聚合返回值,这意味着前端作者必须用sret指针降低它们(意思就是不要直接返回大型的值,改用返回指针)。
    到达呼叫点时,参数分配必须是最新的还存活的堆栈分配,否则结果未定义。可以在参数分配之后、其调用点之前分配额外的堆栈空间,但必须使用[llvm.stackrestore](http://llvm.org/docs/LangRef.html#int-stackrestore)清除它。
    有关如何使用此属性的更多信息,请参阅InAlloca属性的设计和使用
  • sret: 这表明指针参数指定了作为源程序中函数返回值的结构地址。该指针必须由调用者保证是有效的:加载和存储到结构可以由被调用者假定不要陷入并且被正确对齐。这不是返回值的有效属性。
  • align <n>: 这表明优化器可能会假定指针值具有指定的对齐方式。
    请注意,该属性在与byval属性组合时具有额外的语义 。
  • noalias: 这表明,通过基于所述参数或返回值的指针值访问的对象不可以访问,该函数的执行过程中,通过不基于所述参数或返回值的指针值。返回值的属性也具有下面描述的附加语义。调用方与被调用方分担责任,确保满足这些要求。有关更多详细信息,请参阅别名分析中 NoAlias回应的讨论。
    请注意,这个noalias定义有意地类似于C99中用于函数参数的restrict定义。
    对于函数返回值,C99的restrict意义不大,而对LLVM的noalias是有意义的。此外,在函数参数上使用时,返回值上的noalias属性的语义强于该属性的语义。在函数返回值上,noalias属性指示该函数的作用类似于系统内存分配函数,它返回一个指针,指向调用者可访问的任何其他对象的存储器的已分配存储空间。
  • nocapture: 这表明被调用者不会创建超出被调用者本身的指针的任何副本。这不是返回值的有效属性。在易变的操作中使用的地址被认为是被捕获的。
  • nest: 这表明指针参数可以使用弹性(trampoline)内联函数删除 。这不是返回值的有效属性,只能应用于一个参数。
  • returned: 这表明函数总是返回参数作为其返回值。这是在生成调用者时使用的优​​化器和代码生成器的提示,允许值传播,尾部调用优化以及在某些情况下省略寄存器保存和恢复; 在生成被调用者时不检查或强制执行。该参数和函数返回类型必须是bitcast指令的有效操作数 。这不是返回值的有效属性,只能应用于一个参数。
  • nonnull: 这表明参数或返回指针不为null。该属性可能只适用于指针型参数。这不是LLVM检查或执行的,调用者必须确保传入的指针非空,或者被调用者必须确保返回的指针非空。
  • dereferenceable(<n>): 这表明参数或返回指针是可逆向引用的(dereferenceable,就是用*pr引用值)。该属性可能只适用于指针型参数。一个可逆向引用的指针可以从投机加载而没有陷入的风险。已知可逆向引用的字节数必须在括号中提供。字节数小于指点人类型的大小是合法的。该nonnull属性并不意味着可引用性(考虑一个指向数组末尾的一个元素的指针),但是dereferenceable(<n>)暗含nonnulladdrspace(0)中(这是默认地址空间)。
  • dereferenceable_or_null(<n>): 这表明参数或返回值不能同时为非空和不可逆向引用(达到<n>字节)。标记为dereferenceable_or_null(<n>)的所有非空指针是dereferenceable(<n>)。对于地址空间0 dereferenceable_or_null(<n>)意味着一个指针是正好一个dereferenceable(<n>)或null; 在其它的地址空间dereferenceable_or_null(<n>) 意味着一个指针中的至少一个dereferenceable(<n>)null(它也可以是nulldereferenceable(<n>))。该属性可能只适用于指针型参数。
  • swiftself: 这表明该参数是self/context参数。这不是返回值的有效属性,只能应用于一个参数。
  • swifterror: 这个属性被激发来模拟和优化Swift错误处理。它可以应用于指针指向指针类型或指针大小的alloca的参数。在调用点,与参数相对应的实际swifterror参数必须来自swifterrorallocaswifterror调用者的参数。甲swifterror值(参数或ALLOCA)只能加载和存储的,或用作一个swifterror参数。这不是返回值的有效属性,只能应用于一个参数。
    这些约束允许调用约定swifterror通过将它们与在调用边界的特定寄存器相关联来优化对变量的访问, 而不是将它们放置在内存中。由于这确实改变了调用约定,所以swifterror在参数上使用属性的函数不是ABI兼容的。
    这些约束条件还允许LLVM假定swifterror参数不会在函数swifterror中使其他内存可见,并且作为参数传递的 alloca不会转义。

垃圾收集器策略的名称

每个函数都可以指定一个垃圾收集器策略名称,它只是一个字符串:

1
define void @f() gc "name" { ... }

名称的支持值包括LLVM内置的值以及由加载的插件提供的值。指定GC策略将导致编译器更改其输出以支持指定的垃圾回收算法。请注意,LLVM本身不包含垃圾收集器,此功能仅限于生成可与外部提供的收集器互操作的机器代码。

前缀数据

前缀数据是与函数关联的数据,代码生成器在函数入口点之前立即发出的。此功能的目的是允许前端(frontends)将语言特定的运行时元数据与特定函数相关联,并通过函数指针使其可用,同时仍允许调用函数指针。

要访问给定函数的数据,程序可以将函数指针bitcast到指向常量类型和取消逆向引用索引(dereference index,可能是指内容-1处的索引)-1的指针。这意味着IR符号刚刚超过前缀数据的末尾。例如,以单个i32注释的函数为例,

1
define void @f() prefix i32 123 { ... }

前缀数据可以被引用为,

1
2
3
%0 = bitcast void* () @f to i32*
%a = getelementptr inbounds i32, i32* %0, i32 -1
%b = load i32, i32* %a

前缀数据的布局就好像它是前缀数据类型的全局变量的初始化程序。该函数将被放置为使得前缀数据的开始对齐。这意味着如果前缀数据的大小不是对齐大小的倍数,则函数的入口点不会对齐。如果需要对齐函数的入口点,则必须将填充添加到前缀数据中。

一个函数可能有前缀数据但没有主体。这与available_externally链接具有相似的语义,因为优化器可能会使用这些数据,但不会在目标机文件中发送数据。

序言(Prologue)资料

该prologue属性允许在函数体之前插入任意代码(以字节编码)。这可以用于启用函数热修补和仪器。

为了保持普通函数调用的语义,序言数据必须有特定的格式。具体来说,它必须以一系列字节开始,该字节序列解码为一系列机器指令,对模块的目标代码有效,将控制转移到紧接在序言数据之后的位置,而不执行任何其他可见动作。这允许内联和其他传递推理函数定义的语义,而不需要推理序言数据。显然这使得序言数据的格式高度依赖于目标。

x86体系结构的有效序言数据的一个简单示例是i8 144, 对指令nop进行编码:

1
define void @f() prologue i8 144 { ... }

通常,可以通过对跳过元数据的相对分支指令进行编码来形成序言数据,如x86_64体系结构的有效序言数据的示例,其中前两个字节编码为:jmp .+10

1
2
3
%0 = type <{ i8, i8, i8* }>

define void @f() prologue %0 <{ i8 235, i8 8, i8* @md}> { ... }

一个函数可能有序言数据,但没有主体。这与available_externally链接具有相似的语义,因为优化器可能会使用这些数据,但不会在目标文件中发送数据。

Personality功能

personality属性允许函数指定用于异常处理的函数。

属性组

属性组是由IR内的对象引用的属性组。它们对于保持.ll文件可读性很重要,因为许多函数将使用同一组属性。在与单个.c文件相对应的.ll文件的退化(degenerative)情况下,单个属性组将捕获用于构建该文件的重要命令行标志。

一个属性组是一个模块级别的对象。要使用属性组,对象引用属性组的ID(例如#37)。一个对象可能涉及多个属性组。在这种情况下,来自不同组的属性被合并。

下面是一个应该总是内联的函数的属性组的示例,其堆栈对齐方式为4,不应使用SSE指令:

1
2
3
4
5
6
7
8
; 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 { ... }

函数属性

函数属性被设置为传递关于函数的附加信息。函数属性被认为是函数的一部分,而不是函数类型的一部分,因此具有不同函数属性的函数可以具有相同的函数类型。

函数属性是遵循指定类型的简单关键字。如果需要多个属性,它们是空格分隔的。例如:

1
2
3
4
define void @f() noinline { ... }
define void @f() alwaysinline { ... }
define void @f() alwaysinline optsize { ... }
define void @f() optsize { ... }

  • alignstack(<n>): 这个属性表明,当发布序言和尾声(epilogue)时,后端应该强制堆栈指针对齐。在圆括号中指定所需的对齐方式,该对齐方式必须是2的幂。
  • allocsize(<EltSizeParam>[, <NumEltsParam>]): 该属性指示注释的函数将始终返回至少给定数量的字节(或null)。它的参数是零索引参数号; 如果提供了一个参数,则假定至少有CallSite.Args[EltSizeParam]字节在返回的指针处可用。如果提供了两个参数,则假定CallSite.Args[EltSizeParam] * CallSite.Args[NumEltsParam]字节可用。引用的参数必须是整数类型。没有关于返回的内存块的内容的假设。
  • alwaysinline: 该属性表示内联器应尽可能将该函数内联到调用者中,而忽略此调用者的任何活动内联大小阈值。
  • builtin: 这表明,即使函数的声明使用nobuiltin属性,调用点上的被调用函数也应该被识别为内置函数。这仅在调用点有效,才能直接调用nobuiltin属性声明的函数。
  • cold: 这个属性表明这个函数很少被调用。在计算边权重时,由冷函数调用后支配的基本块也被认为是冷的; 因此,轻量级。
  • convergent: 在一些并行执行模型中,存在无法根据任何附加值进行控制的操作。我们称之为这样的操作convergent,并用这个属性标记它们。
    convergent属性可能出现在函数或call/invoke指令上。当它出现在一个函数上时,它表示对这个函数的调用不应该取决于附加值的控制。例如,内在的llvm.nvvm.barrier0convergent,所以对这个内在的调用不能取决于附加值的控制。
    当它出现在一个call/invoke中时,convergent属性表明我们应该把这个调用看作是我们正在调用一个收敛函数。这对间接调用特别有用; 没有这个,我们可以把这样的调用视为目标不收敛。
    当可以证明函数不执行任何收敛操作时,优化器可以删除函数的convergent属性。同样,优化程序可以在call/invoke不能调用收敛函数时删除calls/invokes上的convergent
  • inaccessiblememonly: 该属性表明该函数只能访问正在编译的模块而无法访问的内存。这是一种较弱的readnone形式。
  • inaccessiblemem_or_argmemonly: 该属性表明该函数只能访问被编译的模块而无法访问的内存,或者其指针参数指向的内存。这是一种较弱的argmemonly形式。
  • inlinehint: 这个属性表明源代码包含一个暗示这个函数内联的提示是可取的(比如C / C ++中的“inline”关键字)。这只是一个暗示; 它对内联没有要求。
  • jumptable: 该属性表示应该在代码生成时将函数添加到跳转指令表中,并且应将所有对此函数的地址引用引用替换为对相应的跳转指令表函数指针的引用。请注意,这会为原始函数创建一个新指针,这意味着依赖于函数指针标识的代码可能会中断。所以,jumptable注解的任何函数也必须是unnamed_addr
  • minsize: 此属性表明,优化途径(passes)和代码生成器途径之间进行选择,以使该函数的代码大小尽可能小,并执行可能牺牲运行时性能的优化,以最小化生成的代码的大小。
  • naked: 该属性禁用该函数的序言/尾声(prologue/epilogue)发射。这可能会导致系统特定的后果。
  • no-jump-tables: 当此属性设置为true时,可以从生成的跳转表和查找表被禁用。这些表都是会转换为小写的字母。
  • nobuiltin: 这表明调用点的被调用方函数不被识别为内置函数。除非调用点使用该builtin属性,否则LLVM将保留原始调用并且不会使用基于内置函数的语义的等效代码替换它。这在调用点以及函数声明和定义中是有效的。
  • noduplicate: 此属性表示对函数的调用不能重复。对noduplicate函数的调用可能会在其父函数内移动,但不能在其父函数内复制。
    包含noduplicate调用的函数可能仍然是内联候选人,前提是调用不通过内联复制。这意味着该功能具有内部链接功能,并且只有一个调用点,所以原始调用在内联后死亡。
  • noimplicitfloat: 该属性禁用隐式浮点指令。
  • noinline: 该属性表示内联器在任何情况下都不应该内联该函数。该属性不能与alwaysinline属性一起使用。
  • nonlazybind: 该属性禁止该函数的延迟符号绑定。如果在程序启动期间未调用该函数,则可能会以更多的程序启动时间为代价来更快地调用该函数。
  • noredzone: 该属性指示代码生成器不应使用红色区域,即使目标特定的ABI通常允许它。
  • noreturn: 该函数属性指示函数永远不会正常返回。如果函数在动态返回时会在运行时产生未定义的行为。
  • norecurse: 该函数属性指示该函数不会直接或间接地调用自己的任何可能的调用路径。如果该函数执行递归,这会在运行时产生未定义的行为。
  • nounwind: 该函数属性指示该函数不会引发异常。如果该函数确实引发异常,则其运行时行为未定义。但是,标记为nounwind的函数仍可能陷入或生成异步异常。由LLVM识别以处理异步异常(如SEH)的异常处理方案仍将提供其实现定义的语义。
  • optforfuzzing: 该属性表示该函数应该针对最大模糊信号进行优化。
  • optnone: 该函数属性指示大多数优化过程将跳过此函数,但过程间优化过程除外。代码生成默认为“fast”指令选择器。该属性不能与alwaysinline属性一起使用; 此属性也与minsize属性和optsize属性不兼容。
    这个属性需要在noinline函数中指定属性,所以函数不会被内联到任何调用者中。只有具有该alwaysinline属性的函数才是用于内联到此函数主体中的有效候选项。
  • optsize: 此属性表明,优化传递(passes)和代码生成器传递之间进行选择,以保持此函数的代码大小较低,否则,只要不会显着影响运行时性能,就会专门减少代码大小进行优化。
  • patchable-function“: 这个属性告诉代码生成器,为这个函数生成的代码需要遵循特定的约定,以便运行时函数稍后可以修补它。该属性本身并不意味着对程序间优化的限制。所有修补语义效应可能必须通过连接类型单独传送。该属性的确切效果取决于其字符串值,目前有一个合法的可能性:
    • prologue-short-redirect“ - 这种类型的可修补函数旨在支持修补函数序言,以线程安全的方式将控制权重定向到函数之外。它保证函数的第一条指令足够大以容纳短跳转指令,并且将被充分对齐以允许通过原子比较和交换指令进行完全更改。尽管可以通过插入足够大的NOP来满足第一个要求,但LLVM可以并且将尝试将现有指令(即,不得不被发射的指令)重新用作大于短跳跃的可修改指令。
      prologue-short-redirect“目前仅在x86-64上受支持。
  • probe-stack“: 该属性表明该函数将在堆栈的末尾触发一个防护区域。它确保对堆栈的访问必须不会远离保护区域的大小,保护域是堆栈的先前访问。它需要一个必需的字符串值,即将被调用的堆栈探测函数的名称。
    如果具有”probe-stack“属性的函数内联到另一个”probe-stack“属性的函数中,对调用者而言,则结果函数具有”probe-stack“属性。如果具有”probe-stack“属性的函数被内联到完全没有”probe-stack“属性的函数中,则结果函数具有”probe-stack“被调用者的属性。
  • readnone: 在一个函数上,这个属性表明函数严格基于它的参数来计算它的结果(或者决定展开一个异常),而不需要逆向引用任何指针参数或者访问任何对调用者函数可见的可变状态(例如内存,控制寄存器等)。它不写任何指针参数(包括byval参数),也不会改变调用者可见的任何状态。这意味着虽然它不能通过调用C++异常抛出方法来展开异常(因为它们会写入内存),但可能会有非C++机制在不写入LLVM可见内存的情况下抛出异常。
    在参数上,该属性指示该函数不会对指针参数进行逆向引用,即使它可以读取或写入指针指向的内存(如果通过其他指针访问的话)。
  • readonly: 在一个函数中,这个属性表明函数不会通过任何指针参数(包括byval参数)进行写入,也不会修改调用者函数可见的任何状态(例如内存,控制寄存器等)。它可能会逆向引用(就是*号操作)指针参数并读取调用者可能设置的状态。readonly函数在调用相同的参数集和全局状态时始终返回相同的值(或者展开相同的异常)。这意味着虽然它不能通过调用C++异常抛出方法来展开异常(因为它们会写入内存),但可能会有非C++机制在不写入LLVM可见内存的情况下抛出异常。
    在一个参数上,这个属性表明函数不会通过这个指针参数写入,即使它可能写入指针指向的内存。
  • stack-probe-size“: 该属性控制堆栈探测器的行为:”probe-stack“属性或ABI所需的堆栈探测器(如果有的话)。它定义了防护区的大小。它确保如果函数可能会使用比保护区大小更多的堆栈空间,则会发出堆栈探测序列。它需要一个必需的整数值,默认为4096
    如果具有”stack-probe-size“属性的函数内联到另一个”stack-probe-size“属性函数中,则生成的函数具有”stack-probe-size“数值较小的属性。如果具有”stack-probe-size“属性的函数被内联到完全没有”stack-probe-size”属性的函数中,则结果函数具有”stack-probe-size“被调用者的属性。
  • no-stack-arg-probe“: 该属性禁用ABI所需的堆栈探测器(如果有的话)。
  • writeonly: 在一个函数上,这个属性表明函数可以写入但不从内存中读取。
    在一个参数上,这个属性表明函数可以写入但不读取这个指针参数(即使它可以从指针指向的内存中读取)。
  • argmemonly: 这个属性表明函数内部唯一的内存访问是加载并存储指针类型参数所指向的对象的任意偏移量。换句话说,函数中的所有内存操作都可以仅使用基于其函数参数的指针来引用内存。请注意,argmemonly可以与readonly属性一起使用,以便指定该函数只从其参数中读取。
  • returns_twice: 该属性表示该函数可以返回两次。C的setjmp这里有一个更好的例子)是这种功能的一个例子。编译器在这些函数的调用者中禁用某些优化(如tail调用)。
  • safestack: 此属性表示已为此函数启用SafeStack保护。
    如果具有safestack属性的函数被内联到一个函数,这个函数不具有safestack属性或具有一个sspsspstrongsspreq属性,然后将所得的函数将有一个safestack属性。
  • sanitize_address: 此属性表明已为此函数启用AddressSanitizer检查(动态地址安全分析)。
  • sanitize_memory: 此属性表示对此函数启用MemorySanitizer检查(对未初始化内存的访问的动态检测)。
  • sanitize_thread: 此属性表示为此函数启用了ThreadSanitizer检查(动态线程安全分析)。
  • sanitize_hwaddress: 此属性表示为此函数启用了HWAddressSanitizer检查(基于标记指针的动态地址安全分析)。
  • speculatable: 这个函数属性表明函数除了计算结果之外没有任何影响,并且没有未定义的行为。请注意,这speculatable还不足以断定沿着任何特定的执行路径,对此函数的调用次数不会在外部可观察到。该属性仅适用于函数和声明,而不适用于单个调用点。如果一个函数被错误地标记为speculatable,并且确实表现出未定义的行为,即使该调用点是死代码,也可能会观察到未定义的行为。
  • ssp: 该属性表示该函数应该发出一个堆栈溢出保护器(stack smashing detected)。它的形式是“canary” - 在从函数返回时检查局部变量以查看它是否被覆盖之前放置在堆栈上的随机值。启发式用于确定函数是否需要堆栈保护器。使用的启发式将使保护器具有以下特性的函数:

    • 大于ssp-buffer-size(默认8)的字符数组。
    • 包含大于ssp-buffer-size的字符数组的聚合。
    • 大小大于ssp-buffer-size的变量或者常量调用alloca().

    被确定为需要保护器的变量将被安排在堆栈上,以便它们与堆栈保护器防护装置相邻。
    如果一个具有ssp属性的函数被内联到一个没有ssp属性的函数中,那么结果函数将具有一个ssp属性。

  • sspreq: 该属性表示该函数应该始终发出堆栈溢出保护器。这覆盖了ssp函数属性。
    被确定为需要保护器的变量将被安排在堆栈上,以便它们与堆栈保护器防护装置相邻。具体的布局规则是:

    1. 大型数组和包含大型数组(>= ssp-buffer-size)的结构最接近堆栈保护器。
    2. 小数组和包含小数组(< ssp-buffer-size)的结构距离保护器第二近。
    3. 已经取得地址的变量是第三接近保护者。

    如果其具有sspreq属性的函数被内联到一个函数,它不具有sspreq属性或具有一个sspsspstrong属性,然后将所得的函数将有一个sspreq属性。

  • sspstrong: 该属性表示该函数应该发出一个堆栈溢出保护器。该属性在确定函数是否需要堆栈保护器时会使用强启发式。强大的启发式功能可以为以下函数提供保护:

    • 任何大小和类型的阵列
    • 包含任何大小和类型的数组的聚合。
    • 调用alloca()。
    • 已经取得地址的局部变量。

    被确定为需要保护器的变量将被安排在堆栈上,以便它们与堆栈保护器防护装置相邻。具体的布局规则是:

    1. 大型数组和包含大型数组的结构(>= ssp-buffer-size)最接近堆栈保护器。
    2. 小数组和包含小数组(< ssp-buffer-size)的结构距离保护器第二近。
    3. 已经取得地址的变量是第三接近保护者。

    这覆盖了ssp函数属性。
    如果一个具有sspstrong属性的函数被内联到一个没有sspstrong属性的函数中,那么结果函数将具有一个sspstrong属性。

  • strictfp: 该属性指示该函数是从需要严格浮点语义的作用域调用的。LLVM不会尝试任何需要假设浮点舍入模式的优化,或者可能会改变可能通过调用此函数来设置或清除的浮点状态标志的状态。
  • thunk“: 该属性表示该函数将通过尾部调用委托给某个其他函数。不应将thunk的原型用于优化目的。预计调用者将投掷thunk原型以匹配thunk目标原型。
  • uwtable: 这个属性表明被定位的ABI需要为这个函数生成一个展开的表入口,即使我们能够证明没有异常通过它。这通常适用于ELF x86-64 abi,​​但对于某些编译单元可以禁用它。
  • nocf_check: 此属性表示不会对属性实体执行控制流检查。它会禁用特定实体的-fcf-protection = <>以细化HW控制流保护机制。该标志是目标独立的,并且当前属于函数或函数指针。
  • shadowcallstack: 此属性表示为该函数启用了ShadowCallStack检查。仪器检查(instrumentation checks)函数的返回地址在函数prologeiplog之间没有改变。它目前是x86_64特定的。

全局属性

可以设置属性来传达关于全局变量的附加信息。与函数属性不同,全局变量上的属性被分组到单个属性组中

操作数捆绑(Operand Bundles)

操作数捆绑是可与某些LLVM指令相关联的SSA值的标签set集(当前仅包含calls和invokes)。在某种程度上,它们就像元数据,但删除它们是不正确的,并且会改变程序的语义。

句法:

1
2
3
4
operand bundle set ::= '[' operand bundle (, operand bundle )* ']'
operand bundle ::= tag '(' [ bundle operand ] (, bundle operand )* ')'
bundle operand ::= SSA value
tag ::= string constant

操作数绑定不是函数签名的一部分,并且可以从具有不同类型操作数绑定的多个位置调用给定函数。这反映了操作数绑定在概念上是call(或invoke)的一部分,而不是被调度的被调用者。

操作数绑定是一种通用机制,旨在支持托管语言的类似运行时自反功能。虽然操作数绑定的确切语义取决于绑定标记,但操作数绑定的存在可以影响程序的语义有一定的限制。这些限制被描述为“unknown”操作数绑定的语义。只要操作数绑定的行为可以在这些限制内进行描述,LLVM就不需要对操作数绑定有特殊的了解,就不会错误地编译包含它的程序。

  • 未知操作数绑定的捆绑操作数在控制权转移给被调用者或调用者之前以未知方式转义。
  • 使用操作数绑定进行调用和调用对入口和出口处的堆(即使调用目标是readnone或readonly)在堆上具有未知的读/写效果 ,除非它们被特定于调用点的属性覆盖。
  • 调用点的操作数绑定不能更改被调用函数的实现。只要它们考虑到头两个属性,程序间优化就像往常一样工作。

下面描述了更具体的操作数绑定类型。

逆优化(Deoptimization)操作数绑定

逆优化(Deoptimization)操作数绑定由”deopt“操作数绑定标签表征。这些操作数绑定表示它们所连接的调用点的替代“安全”延续,并且可以由适当的运行时使用,以便在指定的调用点对编译后的帧进行去优化。最多可以有一个”deopt“操作数绑定附加到调用点。逆优化(Deoptimization)的确切细节超出了语言参考的范围,但它通常涉及将编译帧重写为一组解释帧。

从编译器的角度来看,逆优化(Deoptimization)操作数绑定使得它们所连接的调用点至少是readonly。他们通读他们所有的指针类型操作数(即使它们没有被转义)和整个可见的堆。逆优化操作数绑定不会捕获它们的操作数,除非在逆优化过程中,在这种情况下,控制将不会返回到编译帧。

内联器知道如何通过具有逆优化操作数绑定的调用进行内联。就像通过一个正常的调用点进行内联,包括构成正常的和特殊的延续一样,通过调用点内联去耦优化操作数捆绑定需要适当地组成“安全”去优化延续。内联器通过在内联体中对每个逆优化延续预先实现父代的逆优化延续。例如内联@f@g在下面的例子中:

1
2
3
4
5
6
7
8
9
10
11
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
}

会导致

1
2
3
4
5
6
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
}

前端(frontend)的责任是以句法上将调用者的逆最优化状态预先加入到被调用者的逆最优化状态的方式来构造或编码逆最优化状态,这在语义上等同于在被调用者的逆最佳化延续之后构成调用者的逆最佳化延续。

Funclet操作数捆绑

Funclet操作数绑定由”funclet“操作数绑定标签表征。这些操作数绑定表明调用点位于特定funclet内。最多可以有一个”funclet“操作数绑定附加到调用点,并且它必须只有一个捆绑操作数。

如果任何funclet EH焊盘(pads)已被“entered”但不是“exited”(根据EH文档中的描述),则对下面的执行一个call或者invoke是未定义的行为:

  • 没有”funclet“捆绑,并且不是对nounwind intrinsic的call,或者
  • 有一个”funclet绑定,它的操作数不是最近输入的尚未退出funclet EH板(pad)。

类似地,如果没有funclet EH焊盘(pads)进入但尚未退出,在一个”funclet“bundle中执行一个call或invoke是未定义的行为。

GC转换操作数绑定

GC转换操作数绑定由”gc-transition”操作数绑定标记表征。这些操作数绑定将一个调用标记为具有一个GC策略的函数与具有不同GC策略的函数之间的过渡。如果协调GC策略之间的转换需要在调用点生成额外的代码,则这些软件绑定可能包含生成的代码所需的任何值。有关更多详细信息,请参阅GC转场

模块级内联汇编

模块可能包含“模块级内联asm”块,它对应于GCC“文件范围inline asm”块。这些块由LLVM内部连接并作为一个单元进行处理,但如果需要可以在.ll文件中分开。语法非常简单:

1
2
module asm "inline asm code goes here"
module asm "more can go here"

字符串可以通过转义不可打印的字符来包含任何字符。使用的转义序列只是“\xx”,其中“xx”是数字的两位十六进制代码。

请注意,汇编字符串必须由LLVM的集成汇编程序解析(除非它被禁用),即使在发送.s文件时也是如此。

数据布局

模块可以指定目标特定数据布局字符串,该字符串指定数据如何布置在内存中。数据布局的语法很简单:

1
target datalayout = "layout specification"

该布局规范包括规范用减号字符分隔(“-”)的列表中。每个规范都以字母开头,并可能在字母后包含其他信息以定义数据布局的某些方面。接受的规格如下:

  • E: 指定目标以big-endian格式显示数据。也就是说,最重要的位具有最低的地址位置。
  • e: 指定目标以little-endian形式显示数据。也就是说,具有最低重要性的位具有最低的地址位置。
  • S<size>: 指定堆栈的自然对齐位数。堆栈变量的对齐提升仅限于自然堆栈对齐,以避免动态堆栈重新对齐。堆栈对齐必须是8位的倍数。如果省略,则自然堆栈对齐默认为“未指定”,这不会阻止任何对齐升级。
  • P<address space>: 指定对应于程序内存的地址空间。哈佛(Harvard)体系结构可以使用它来指定LLVM应该在哪些空间放置诸如函数之类的东西。如果省略,程序存储器空间默认为默认地址空间0,这对应于具有相同空间中的代码和数据的冯诺依曼体系结构。
  • A<address space>: 指定由’alloca‘创建的对象的地址空间。默认为默认地址空间0。
  • p[n]:<size>:<abi>:<pref>:<idx>: 这将指定大小的指针和它的<abi><pref>错误的地址的空间对齐n。第四个参数<idx>是用于地址计算的索引大小。如果未指定,则默认索引大小等于指针大小。所有尺寸都是位。地址空间n是可选的,如果未指定,则表示默认地址空间0.值n必须在[1,2^23)范围内。
  • i<size>:<abi>:<pref>: 这指定了给定<size>位的整数类型的对齐方式。值<size>必须在[1,2^23)范围内。
  • v<size>:<abi>:<pref>: 这指定了给定<size>位的向量类型的对齐方式。
  • f<size>:<abi>:<pref>: 这指定了给定<size>位的浮点类型的对齐方式。只有目标支持的<size>值才有效。所有目标都支持32(浮动)和64(双)。一些目标也支持80或128(不同长度的双重版本)。
  • a:<abi>:<pref>: 这指定了聚合类型对象的对齐方式。
  • m:<mangling>: 如果存在,则指定llvm名称在输出中被损坏。以mangling转义字符作为前缀的符号\01直接传递给汇编器而不使用转义字符。mangling风格选项是
    • e:ELF mangling:专用符号获取.L前缀。
    • m:Mips mangling:私有符号获取$前缀。
    • o:Mach-O修改:专用符号获取L前缀。其他符号会得到一个_前缀。
    • x:Windows x86 COFF mangling:私有符号获取通常的前缀。普通的C符号会得到一个_前缀。带有__stdcall__fastcall,和__vectorcall的函数具有附加的自定义修剪(mangling),@N其中N是用于传递参数的字节数。以?开头的C++符号不会以任何方式的变形(mangling)。
    • w:Windows COFF mangling:类似于x,除了普通的C符号不接受_前缀。
  • n<size1>:<size2>:<size3>...: 这为位目标CPU指定了一组本地整数宽度。例如,它可能包含32位PowerPC的n32,对于PowerPC 64是n32:64或对于X86-64是n8:16:32:64。这组元素被认为可以有效地支持大多数一般的算术运算。
  • ni:<address space0>:<address space1>:<address space2>...: 这指定具有指定地址空间的指针类型作为非整体指针类型。该0地址空间不能被指定为非积分(non-integral)。

在每个采用的<abi>:<pref>规范上,指定<pref>对齐是可选的。如果省略,则前面的内容也应该省略, 并且<pref>等于<abi>

在为给定目标构建数据布局时,LLVM从默认的一组规范开始,然后(可能)由datalayout关键字中的规范覆盖。默认规格在此列表中给出:

  • E - 大端
  • p:64:64:64 - 与64位对齐的64位指针。
  • p[n]:64:64:64 - 其他地址空间被假定为与默认地址空间相同。
  • S0 - 自然堆栈对齐未指定
  • i1:8:8 - i1是8位(字节)对齐
  • i8:8:8 - i8是8位(字节)对齐
  • i16:16:16 - i16是16位对齐的
  • i32:32:32 - i32是32位对齐的
  • i64:32:64 - i64具有32位的ABI对齐,但优选对齐64位
  • f16:16:16 - 一半是16位对齐的
  • f32:32:32 - 浮点数是32位对齐的
  • f64:64:64 - 双是64位对齐
  • f128:128:128 - 四位是128位对齐
  • v64:64:64 - 64位向量是64位对齐的
  • v128:128:128 - 128位向量是128位对齐的
  • a:0:64 - 聚合是64位对齐

当LLVM确定给定类型的对齐时,它使用以下规则:

  1. 如果所寻找的类型与其中一个规格完全匹配,则使用该规格。
  2. 如果未找到匹配项,并且所查找的类型是整数类型,则使用大于所查找类型的位宽的最小整数类型。如果没有任何规格大于位宽,则使用最大的整数类型。例如,给定上面的默认规格,i7类型将使用i8(次大)的对齐,而i65i256将使用i64(最大指定)的对齐。
  3. 如果找不到匹配,并且所寻找的类型是矢量类型,那么将使用小于所寻找的矢量类型的最大矢量类型作为回退。发生这种情况是因为<128 x double>可以用64 <2 x double>来实现,例如。

数据布局字符串的功能可能不是您所期望的。值得注意的是,这不是来自代码生成器应该使用的对齐前端(frontend)的规范。

相反,如果指定,则需要目标数据布局来匹配最终代码生成器所期望的内容。这个字符串被中级优化器用来改进代码,这只有在它与最终的代码生成器使用的匹配时才有效。没有办法生成IR,这并不会将这个特定于目标的细节嵌入到IR中。如果您未指定字符串,则将使用默认规格来生成数据布局,并且优化阶段将相应地运行,并针对这些默认规格向IR中引入目标特异性。

目标机三重表示法(Target Triple)

模块可以指定描述目标主机的目标机三元字符串。目标三元组的语法很简单:

1
target triple = "x86_64-apple-macosx10.7.0"

该目标机三重串包括由减号(“-”)字符分隔的一系列标识符。规范形式是:

1
2
ARCHITECTURE-VENDOR-OPERATING_SYSTEM
ARCHITECTURE-VENDOR-OPERATING_SYSTEM-ENVIRONMENT

这些信息被传递到后端,以便为适当的架构生成代码。可以使用-mtriple命令行选项在命令行上覆盖它。

指针别名规则

任何内存访问都必须通过与内存访问的地址范围关联的指针值来完成,否则行为是不确定的。根据以下规则,指针值与地址范围相关联:

  • 指针值与关联基于任何值的地址相关联。(这其实是比较绕口的废话)
  • 全局变量的地址与变量存储的地址范围相关联。
  • 分配指令的结果值与分配的存储器的地址范围相关联。
  • 默认地址空间中的空指针与无地址相关联。
  • 一个非零整数常量或从未在LLVM中定义的函数返回的指针值可能与通过LLVM提供的机制以外的机制分配的地址范围关联。这些范围不得与由LLVM提供的机制分配的任何地址范围重叠。

根据以下规则,指针值基于另一个指针值:

  • 由标量getelementptr操作符形成的指针值基于指针类型的操作符getelementptr
  • 在车道(lane)中的指针/升的矢量的结果的getelementptr操作是基于对车道(lane)指针升的的类型的载体的指针操作符getelementptr
  • bitcast的结果值是基于操作符bitcast
  • inttoptr形成的指针值是基于对造成(直接或间接)到指针的值的计算中的所有指针值。
  • “基于”的关系是传递性的。

请注意,这个“based”的定义有意地类似于C99 中“based”的定义,尽管它稍微弱一些。

LLVM IR不会将类型与内存相关联。load的结果类型仅指示要加载的内存的大小和对齐方式,以及对值的解释。类似的第一个操作数类型store仅指示存储的大小和对齐方式。

因此,基于类型的别名分析,又名TBAA,也就是说-fstrict-aliasing,不适用于通用的非LLVM IR。元数据可用于编码额外的信息,专门的优化过程可用于实现基于类型的别名分析。

易失性(volatile)内存访问

某些内存访问,如load‘s,store‘s和llvm.memcpy‘s可能被标记volatile。优化器不得更改易失性操作的数量或更改其相对于其他volatile操作的执行顺序。优化器可以改变相对于非volatile操作的易失性操作的顺序。这不是Java的“volatile”,并且没有跨线程同步行为。

即使那些内部函数被标记为volatile,IR级别的volatile加载和存储也不能安全地优化到llvm.memcpyllvm.memmove内在函数中。同样,后端不应该拆分或合并目标机器合法的易失性加载/存储指令。

合理
平台可能依赖volatile加载,并且本地支持的数据宽度存储将作为单条指令执行。例如,在C中,这适用于具有本地硬件支持的易失性基本类型的l值,但不一定适用于聚合类型。前端支持这些预期,这在IR中是故意没有说明的。上述规则确保IR转换不会违反前端(应该是前置的语言)与该语言的合同。

并发操作的内存模型

LLVM IR没有定义任何启动并行执行线程或注册信号处理程序的方法。尽管如此,还是有特定于平台的方式来创建它们,并且我们定义LLVM IR他们存在的行为。该模型受C++ 0x内存模型的启发。

有关此模型的更多非正式介绍,请参阅LLVM原子指令和并发指南

我们将发生之前的偏序定义为最小偏序(partial order).

  • 是单线程程序顺序的超集,并且
  • 当同步b时,包含一个从ab的边缘。通过特定于平台的技术(如pthread锁,线程创建,线程连接等)以及原子指令引入同步对。(另请参阅原子内存排序约束)。

请注意,程序顺序不会在线程和该线程内执行的信号之间引入边界之前发生的事件。

每个(定义的)读取操作(加载指令,memcpy,原子加载/读取-修改-写入等)R读取由(定义的)写入操作写入的一系列字节(存储指令,原子存储/读取-修改-写入,memcpy等)。就本节而言,已初始化的全局变量被认为是写入了初始化程序,它是原子化的,并且在任何其他读或写有问题的内存之前发生。对于读R的每个字节,Rbyte 可能会看到对相同字节的任何写入,除了:

  • 如果write1 发生在write2之前,并且write2发生在Rbyte之前,则Rbyte不会看到Write1
  • 如果Rbyte在write3之前发生,则Rbyte不会看到write3

鉴于该定义,Rbyte定义如下:

  • 如果R是volatile的,则结果与目标相关。(Volatile应该提供可以在C/C++中支持sig_atomic_t的保证,并且可以用于访问不像正常内存那样行为的地址,它通常不会提供跨线程同步。)
  • 否则,如果没有写入Rbyte发生之前的相同字节,则Rbyte会对该字节返回undef
  • 否则,如果Rbyte可能只看到一次写入,则Rbyte将返回该写入写入的值。
  • 否则,如果R是原子的,并且所有写入的Rbyte可能看到的都是原子的,它将选择其中一个写入的值。请参阅原子内存排序约束部分了解如何进行选择的其他限制条件。
  • 否则Rbyte返回undef。

R返回由它读取的一系列字节组成的值。这意味着该值内的一些字节可能是undef,没有整个值的undef。请注意,这只定义了操作的语义; 这并不意味着目标将发出多个指令来读取一系列字节。

请注意,在没有使用任何原子内在函数的情况下,此模型仅对单线程执行所需的IR转换放置一个限制:将store引入可能不会被存储的字节一般是不允许的。(具体来说,在另一个线程可以写入和读取地址的情况下,引入一个store可以改变一个load,可以看到只有一个写入可能看到多个写入的load。)

原子内存排序约束

原子指令(cmpxchgatomicrmwfenceatomic loadatomic store)使用排序参数来确定与它们同步的同一地址上的其他原子指令。这些语义是从Java和C ++ 0x中借用的,但是更通俗一点。如果这些描述不够精确,请检查这些规格(请参阅Atomic指南中的规格参考)。fence指令对待这些排序有些不同,因为他们没有收到地址。有关详细信息,请参阅该说明文档。

有关排序约束的更简单介绍,请参阅LLVM原子指令和并发指南

  • unordered: 可以读取的一组值由发生前的部分顺序决定。除非某些操作写入,否则无法读取值。这旨在提供足够强大的保证来模拟Java的非volatile共享变量。此顺序不能指定为读取-修改-写入操作; 它不足以使它们以任何有趣的方式成为原子。
  • monotonic: 除了保证unordered之外,每个地址上的monotonic操作都有单个总顺序。所有修改顺序都必须与先发生的订单兼容。不能保证修改顺序可以合并到整个程序的全局总顺序中(而这通常是不可能的)。原子读取-修改-写入操作(cmpxchgatomicrmw)中的读取会在写入值之前立即读取修改顺序中的值。如果在同一地址的另一个原子读取之前发生一次原子读取,则稍后的读取必须在地址的修改顺序中看到相同的值或更高的值。这不允许重新排序monotonic(或更强大)的操作。如果地址是由一个线程monotonic-ally写入和其他线程monotonic-ally读取的 - 反复读取该地址,其他线程最终必须看到写入。这对应于C++ 0x/C1x的memory_order_relaxed
  • acquire: 除了monotonic的保障,一个进行同步边缘可以与形成release操作。这是为了模拟C++的memory_order_acquire
  • release: 除了保证monotonic,如果此操作写入随后由acquire操作读取的值,则与该操作同步。(这不是一个完整的描述;请参阅发布序列的C++0x定义。)这对应于C ++ 0x/C1x的memory_order_release
  • acq_rel (获取+释放): acquirerelease操作作为地址的一部分。这对应于C ++ 0x / C1x memory_order_acq_rel
  • seq_cst (顺序一致): 除了保证acq_relacquire对于仅读取的操作,release对于仅写入的操作),对于所有地址上的所有顺序一致的操作,存在全局总顺序,这与在部分顺序之前发生的以及与所有受影响地址的修改顺序一致。每个按顺序一致的读取将按照此全局顺序查看最后一个先前写入相同地址的内容。这对应于C ++ 0x / C1x memory_order_seq_cst和Java volatile

如果标记了一个原子操作syncscope("singlethread"),它只会同步并仅参与在同一线程中运行的其他操作(例如,在信号处理程序中)的seq_cst总排序。

如果标记了一个原子操作syncscope("<target-scope>"),其中 <target-scope>是目标特定的同步范围,那么它与目标相关,如果它与其他操作的seq_cst总排序同步并参与其中。

否则,未标记的原子操作syncscope("singlethread")syncscope("<target-scope>") 与同步和参与未标记syncscope("singlethread")syncscope("<target-scope>")的其它操作的seq_cst总排序 。

浮点环境

默认的LLVM浮点环境假定浮点指令没有副作用。结果假设为舍入到舍入模式。此环境中不保留浮点异常状态。因此,在这些示例中,不会尝试创建或保留无效操作(SNaN)或零除异常:

1
2
3
4
5
  %A = fdiv 0x7ff0000000000001, %X  ; 64-bit SNaN hex value
%B = fdiv %X, 0.0
Safe:
%A = NaN
%B = NaN

这种无异常假设的好处是可以自由推测浮点运算,而无需对浮点模型进行任何其他快速数学放松(fast-math relaxations)。

需要与此不同的行为的代码应使用约束浮点内部函数

快速数学标志

LLVM IR浮点运算(faddfsubfmulfdivfremfcmp)和call可以使用以下标志来启用其他不安全的浮点转换。

  • nnan: 没有NaNs - 允许优化假设参数和结果不是NaN。需要进行这样的优化才能在NaN上保留已定义的行为,但结果的值未定义。
  • ninf: 无Infs - 允许优化假设参数和结果不是+/-Inf。需要进行这样的优化才能在+/-Inf上保留已定义的行为,但结果的值未定义。
  • nsz: 无签名零 - 允许优化将零参数或结果的符号视为无关紧要。
  • arcp: 允许互惠(Reciprocal) - 允许优化使用参数的倒数而不是执行除法。
  • contract: 允许浮点收缩(例如,融合乘法,然后将加法融合为融合乘加)。
  • afn: 近似函数 - 允许用近似计算代替函数(sinlogsqrt等)。有关可应用于LLVM的内在数学函数的位置,请参阅浮点内在定义。
  • reassoc: 允许重新关联转换为浮点指令。这可能会显着改变浮点结果。
  • fast: 这个标志暗示其他所有的。

使用列表顺序指令

使用列表指令对每个使用列表的内存顺序进行编码,从而允许重新创建订单。<order-indexes>是分配给引用值用途的索引的逗号分隔列表。引用值的使用列表会立即按这些索引排序。

Use-List指令可能出现在函数作用域或全局作用域中。它们不是指令,对IR的语义没有影响。当它们在函数范围内时,它们必须出现在最终基本块的终止符之后。

如果基本块的地址是通过blockaddress()表达式获取的, uselistorder_bb则可用于从其函数范围之外对其使用列表重新排序。

句法:

1
2
uselistorder <ty> <value>, { <order-indexes> }
uselistorder_bb @function, %block { <order-indexes> }

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define void @foo(i32 %arg1, i32 %arg2) {
entry:
; ... instructions ...
bb:
; ... instructions ...

; At function scope.
uselistorder i32 %arg1, { 1, 0, 2 }
uselistorder label %bb, { 1, 0 }
}

; At global scope.
uselistorder i32* @global, { 1, 2, 0 }
uselistorder i32 7, { 1, 0 }
uselistorder i32 (i32) @bar, { 1, 0 }
uselistorder_bb @foo, %bb, { 5, 1, 3, 2, 0, 4 }

源文件名

源文件名字符串设置为原来的模块标识符,当通过clang前端从源代码编译时,例如,这将是编译的源文件的名称。然后通过IR和比特码进行保存。

目前,这对于为配置文件数据中使用的本地函数生成一致的唯一全局标识符是非常必要的,该配置文件将源文件名预先设置为本地函数名称。

源文件名的语法很简单:

1
source_filename = "/path/to/source.c"

总结

这篇文章,翻译起来的确是非常麻烦,由于没有通读全文,里面应该有不少错误的地方,日后会重新修改。

0%