LLVM Language Reference Manual 阅读笔记

前言

本笔记是对《LLVM Language Reference Manual》中关键内容的摘要和翻译,通过这种方式督促自己系统性学习LLVM技术细节。由于LLVM大版本之间存在一些差异,因此本文选择的LLVM版本为7.0.1 Release。

标识符(Identifier)

LLVM中的标识符,按作用域不同,分为两种类型,即全局标识符(Global)和局部标识符(Local)。全局标识符通常为全局变量或函数名,以‘@’开头。局部标识符通常是寄存器名(Register Name)、类型(Type),以‘%’开头。

标识符有三种不同的格式,用于不同的场景:

  1. 具名数值(Named Values),通常是以字符串作为前缀,例如:%foo,@DivisionByZero,%a.really.long.identifier等。
  2. 匿名数值(Unnamed Values),通常由无符号整数构成,例如:%12,@2,%44等。
  3. 常量(Constants),在后续章节会单独进行讨论。

在设计上,LLVM要求标识符以‘@’或‘%’开头主要基于以下考虑 —— 无需担心与保留字(Reserved Words)冲突,将来可方便扩展新的保留字。此外,匿名标识符可以让编译器方便地添加临时变量,无需担心同符号表中已有符号产生冲突。

以下是LLVM代码的一个例子,完成一个变量‘%x’ 乘以8:

%result = mul i32 %X, 8

一种优化方式如下(将乘法转换为左移位操作):

%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

上面这段代码,有几点补充说明如下:

  1. LLVM是通过‘;’进行注释的。
  2. 当中间结果为保存到命名变量(Named Value)时,会自动创建匿名临时变量(Unamed Value)。
  3. 匿名临时变量是从0开始进行编号的,这个计数器的作用域是函数级别(per-function counting)。此外,基本块(Basic Blocks)、匿名函数(Unnamed function)的编号,以及函数参数的编号,也遵循类似规则。

高级结构(High-level structure)

模块结构(Module structure)

LLVM的程序由一个一个的模块(Module)组成,每一个模块包含了一组函数(Functions),全局变量(Global Variables)以及符号表(Symbol Table Entries)。模块可能会在链接时,被LLVM链接器合并,包括函数定义合并、全局变量定义合并,前向申明推导,以及合并符号表表项。以下是一个示例:

; 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”的全局变量,一个名称为“put”的函数外部声明,一个名称为“main”的函数定义,以及一个名称为“foo”的命名元数据。

通常,一个Module由一组Global Values组成(包括函数和全局变量等)。这些Global Values主要包含指向一个特定位置的指针,以及对应的连接类型(Linkage Types)。

(译注:本质上可以看做符号表的描述)

链接类型(Linkage Types)

所有的全局变量、函数都需要具备一个链接类型。(译注:实际上就是符号可见性)

private

如果一个全局变量的链接类型为私有,那么只能在当前Module才能访问该变量。特别地,当一段代码链接到一个Private Global Value的模块时,会触发必要的Private Global Value重命名,以比较名字冲突。通常,Private Global Value是不会被添加到目标文件(Object File)的符号表(Symbol Table)的。

internal

该类型与Private类型相似,但是会作为本地符号(Local Symbol)被添加到目标文件符号表中(例如,作为ELF文件STB_LOCAL类型)。该类型相当于C语言中static关键字。

available_externally

available_externally类型的全局符号永远不会被添加到LLVM Module对应的目标文件中。从链接器角度看,available_externally全局符号等同于外部声明(译注:相当于C语言extern关键字)。在知道全局符号定义的情况下,允许对齐实施内联(inline)和其他优化。

linkonce

在链接期,链接器会将linkonce类型的全局符号,同其他同名的全局变量合并。可以用于实现某些形式的内联函数、模板,以及其他需要由翻译单元(Translation Unit)生成的代码,但是后续可以使用更强的定义(相对于weak)进行覆盖。未被引用的linkonce类型的全局符号是可以被丢弃的。注意:linkonce类型的全局符号实际上是不允许编译器将函数主题内联到调用者的,因为编译器无法确定后续是否会有更强的定义将其覆盖。如果需要启用内联和其他优化,需要使用后续章节介绍的"linkonce_odr"。

译注一: 关于linkonce跟C++语言相关的应用场景

原文linkonce的解释有点难以理解。对于C++编译器来说,有很多时候需要产生重复代码,比如模板(Template)、外部内联函数(Extern Inline Function)、虚函数表(Virtual Function Table)都有可能在不同的编译单元中生成相同的代码。以模板为例,当模板在一个编译单元里被实例化时,它并不知道自己是否在别的编译单元已经被实例化了。所以,当一个模板在多个编译单元同时实例化成相同类型的时候,必然产生重复代码。

如果不消除重复代码,会带来一下几个方面问题:

  1. 空间浪费,会导致最终生成的二进制文件大小膨胀;
  2. 地址较易出错,有可能两个指向同一个函数的指针会不相等;
  3. 指令运行效率低,如果同一份指令有多个副本,则指令Cache命中率会降低;

一个比较有效的做法,就是将每一个模板的实例代码都单独存放在一个段里,每一个段只包含一个模板实例。当不同的编译单元也产生相同的模板实例化函数之后,也会产生同样的段名,这样链接器在最终链接时就能区分不同的模板实例段,然后将它们合并入最后的代码段。

主流编译器均采用这种做法。GCC把这种类似的须在最终链接时合并的段称为“Link Once”,将这种类型的段命名为“.gnu.linkonce.name”,其中name为该模板函数实例的修饰后名称。

译注二:关于ODR的解释

当链接器试图为某个符号产生链接引用时,如果找不到符号定义,链接器会抛出错误信息。但是,如果在链接阶段找到了多个定义,会如何处理?

对于C++,这种情况是比较容易处理的,因为语言本身提供了一种称为一次定义法则(one definition rule)的约束,即链接阶段,一个符号有且只能定义一次。

weak

同linkonce类似,weak链接类型也具有合并语义,但是与不同的是,weak链接类型的符号不会被链接器丢弃。该类型主要对应于C语言weak关键字。

译注一:关于强符号和弱符号

  1. 链接器不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,链接器会报重复定义错误;
  2. 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号;
  3. 如果一个符号在所有目标文件中都是弱符号,那么选择空间占用最大的一个;

译注二:弱符号的应用场景

待补充

common

该链接类型同weak类型最为类似,但是主要用于C语言中临时性定义(tentative definition),例如在全局作用域下定义“int x;”。common类型的符号同样会被合并,即便没有被引用,该符号也不能被删除。common类型的符号可能没有一个显式(explicit)的段,必须有一个清零的初始化器,并且不能被标记为常量。函数及其别名没有common这种链接类型。

译注:关于common类型的补充说明

在C语言中,对于任何的函数或者已经初始化的全局变量,都有且只能有一次定义,但未初始化的全局变量的定义可以看成是一种临时定义(tentative definition)。C语言允许(至少不禁止)同一个符号在不同的源文件中进行临时性定义。

对于其他语言,一次性定义法则(ODR)并不总适用,此时,需要从多个副本中选择一个(如果大小不同,选最大的那个),并将剩余副本丢弃。这种模式成为COMMON。

GNU工具包提供-fno-common选项,可以让编译器强行将未初始化变量放于bss段,而不存放于common段。

appending

译注:这个类型似乎是LLVM特化的一种链接类型,主要用于将来自不同.o目标文件中的同名段进行合并。

extern_weak

这种链接类型的语义遵循ELF格式目标文件的规范:一个符号在未链接之前都是weak的,如果最终未链接,这个符号将变为null状态,而非未定义引用(undefined reference)。

linkonce_odr,weak_odr

某些编程语言允许不同的全局符号被合并,例如将具备不同语义的两个函数合并。对于其他语言,像C++,由于提供了ODR原则,仅允许合并完全相同的全局符号。对于不具备ODR规则的编程语言,可以使用linkonce_odr,weak_odr链接类型,提示链接器仅对完全相同的全局符号进行合并。而对于C++这类语言,linkonce_odr同linkonce等价,同理,weak_odr同weak等价。

external

如果未指定上面任何一种链接类型,则全局符号默认是全局可见的,这意味着这些符号将参与链接,需要在链接期确定外部符号引用关系。

注意:将一个函数标记为external或extern_weak是非法的。

调用惯例(Calling Conventions)

“ccc” —— The C Calling convention

如果未指定任何调用惯例,默认使用C调用惯例。这种调用惯例支持变参函数调用(Varargs),并能够容忍一些同函数原型调用。

“fastcc” —— The Fast Calling convention

这种调用惯例允许尝试各种可能的方法以生成尽可能快的函数调用代码(例如,采用寄存器进行参数传递)。这种调用惯例不支持变参函数调用(Varargs),并且要求callee同函数原型严格匹配。

译注:一些比较偏门的调用惯例就不在此列举了,具体可查阅手册原文。

符号可见性(Visibility Styles)

所有全局变量和函数都具备以下符号可见性中的一种:

“default” —— Default style

对于ELF目标文件格式,默认的符号可见性意味着所有的符号声明(declaration)都对其他module可见,并且,在共享库中,所声明的实体可能被覆盖。

“hidden” —— Hidden style

具备hidden属性的符号的两个声明,如果在同一个共享库(shared object)中,则引用同一个对象。通常,hidden属性的符号不会被合并到动态符号表中,其他模块也无法对其引用。

“protected” —— Protected style

对于ELF文件,protected属性的符号会被添加到符号表中,对该符号的引用只能绑定到定义模块的本地符号。也就是说,该符号不能被其他模块重写。

译注:比较常用的只有default和hidden,对于GCC,default等价于public。

DLL存储类别(DLL Storage Classes)

dllimport,dllexport

控制动态链接库符号导入导出的选项。同Windows平台dllimport和dllexport类似。

线程本地存储模型(Thread Local Storage Models)

一个变量可以被定义为thread_local,这意味着线程间不会共享该变量,会为该变量各自维护一个副本。

generaldynamic,localdynamic,initalexec,localexec

译注:上面三种模式,LLVM文档解释得不太清楚,因此需要单独做一个知识补充。参考《ELF Handling For Thread-Local Storage》。

线程本地存储(Thread Local Storage,即TLS),在设计之初,主要是因为现有的pthread编程接口虽然允许为单独的线程传递一个void *指针,但是接口的易用性较差。并且,当有模块或者代码动态链接到当前模块时,线程本地存储的设计将面临一些现实的挑战。基于上述因素,C/C++在语言层面引入了__thread关键字,可以用于修饰变量的定义和声明。虽然__thread并非C/C++语言官方扩展,但是鼓励编译器开发者实现这种ABI。

__thread关键字不仅仅用于用户态程序,例如,Linux下全局变量errno就是一种线程本地存储。另外,自动变量(Automatic Variable)的存储本身就是thread-local的,因此,为局部自动变量添加__thread是没有意义的。

thread-local的变量,其行为必须符合预期。当对其取地址时,应该返回当前线程下的本地存储。当模块动态加载时,线程本地存储自动创建,同样,当模块卸载时本地存储自动释放。对于C++而言,真正的限制是不可以强制要求线程本地存储准备一个静态构造器(Static Constructor),这个行为是有别于static关键字修饰的变量的。

为此,编译器并没有将thread-local类型的变量统一存放到.data和.bss段,而是将这些变量统一存放到.tdata和.tbss。

线程本地存储在运行期的模型一共有两种变体:

LLVM Language Reference Manual 阅读笔记_第1张图片

LLVM Language Reference Manual 阅读笔记_第2张图片

generaldynamic

如果未指定任何特定的TLS模式,默认采用该种模式。代码示例:

extern __thread int x;

&x;

localdynamic

For variables that are only used within the current shared library. 也就是说,thread-local变量如果只在当前共享库中使用,则将该变量的TLS模型设置为localdynamic。代码示例:

static __thread int x1;
static __thread int x2;

&x1;
&x2;

补充说明:localdynamic类别的thread-local变量的可见性为protected或者hidden。

initialexec

For variables in modules that will not be loaded dynamically. 也就是说,如果动态库和可执行程序同时加载,而非动态延迟加载,则定义在动态库中的thread-local变量可以采用该种模式。

localexec

For variables defined in the executable and only used within it. 也就是说,这种TLS模式适用于变量定义在可执行文件,且仅在当前可执行文件中使用的场景。

补充说明:TLS Model同地址无关代码的关系

如果在编译时指定了-fPIC,则提示编译器生成地址无关代码,此时TLS模式将自动设置为generaldynamic或localdynamic。当指定-fno-pic时,TLS模式将设置为initialexec或localexec。

运行时抢占提示符(Runtime Preemption Specifiers)

全局变量、函数和别名可以具有一个可选的运行时抢占提示符,如未指定,默认为dso_preemptable。

dso_preemptable

运行期允许函数或者变量被来自外部链接单元(linkage unit)的符号替换。

dso_local

编译器会假设具备dso_local属性的函数或者变量会被决议(resolve)成当前链接单元(linkage unit)中的符号。即使在当前编译单元(compilation unit)中未找到定义,依然会产生对该函数或变量的直接访问(direct access)。

全局变量(Global Variables)

全局变量在编译期(非运行期)定义了一块内存区域。

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

其他翻译单元(translation units)中的全局变量可以被申明,在这种情况下,是没有初始化器的(initializer)。

可以使用一个显式定义的段(section)放置全局变量的定义或声明,并可以指定一个可选的显式对齐。如果显式或隐式推导出的变量声明同其定义不匹配,则行为将会是不确定的。

一个变量可以被定义为全局常量,即变量内容不允许被修改(可以触发更好的优化,并将其放到可执行程序的只读数据段)。注意,需要在运行时被初始化的变量不能被标记为常量(constant),因为编译器会为变量分配存储。

LLVM明确允许全局变量声明被标记为常量,即便其最终定义不是常量。这种功能可以让程序(轻微 slightly)更好地被优化,但是要求语言层面确保翻译单元(translation unit)在不包含定义的情况下支持这种基于常量化(constantness)的优化。

全局变量总是定义一个指向其内容类型的指针,因为它们描述了一款内存区域,并且这块区域里的所有对象都是通过指针访问。

全局变量可以被标记为unnamed_addr(译注:匿名),这表示全局变量地址是无符号标记的(not significant)。如果具有相同的初值,这类全局变量是可以合并的。注意,一个具有符号地址(significant address)的全局(命名)变量可以同一个unnamed_addr全局变量合并。

如果指定了local_unnamed_addr属性,则变量地址在当前模块中将不会被命名。

一个全局变量可以被声明放置在跟特定体系结构相关(target-specific)的地址空间中(address space)。对于支持它们的体系结构,其特定地址空间会对优化产生影响,决定使用何种类型的指令去访问这些变量。

LLVM允许为全局变量指定一个明确的段(section)。如果目标体系结构支持,则LLVM将会把全局变量放置到该段中。此外,全局变量也可以被防止到comdat段中。

外部声明可以拥有一个明确指定的段。当LLVM IR需要使用这些外部声明时,会访问这些段。

全局变量语法如下:

@ = [Linkage] [PreemptionSpecifier] [Visibility]
                   [DLLStorageClass] [ThreadLocal]
                   [(unnamed_addr|local_unnamed_addr)] [AddrSpace]
                   [ExternallyInitialized]
                     []
                   [, section "name"] [, comdat [($name)]]
                   [, align ] (, !name !N)*

例一:以下定义了具有编号地址(Numbered address)、初始化逻辑(Initializer)、段名和对齐方式的变量。

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

例二:以下声明了一个全局变量。

@G = external global i32

例三:以下定义了一个线程本地存储的变量,TLS模式为initialexec。

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

函数(Functions)

define [linkage] [PreemptionSpecifier] [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]

LLVM函数定义需包含一个define关键字,其他选项如上所示。

LLVM函数声明需包含一个declare关键字,其他选项同函数定义类似。

一个函数的定义包含一组基本块(Basic Blocks),由基本块组成了控制流图(Control Flow Graph,CFG)。每个基本块都可以以一个可选的标签(Label)——开头(该label即Basic Block的起始地址),包含一组指令,并以一个terminator instruction结尾(通常是分支跳转指令或ret指令)。如果未明确指定label,则该基本块会被指定一个隐含的数字编号的标签。例如,入股哦一个函数入口(function entry block)没有明确指定一个标签,则会被分配一个标签“%0”,则该block中的下一个匿名临时块将会被定命名为“%1”,以此类推。

函数中第一个基本块是比较特别的:第一,它是函数的入口,一调用函数会被立即执行;其次,它不允许有前置(predecessor)的基本块(例如,在该函数的入口不能有任何分支)。因为基本块可能没有predecessors,所以它们不允许有任何PHI nodes(其含义参见后续章节)。

LLVM允许给函数指定一个明确的段。如果目标体系结构下支持,则LLVM将会将函数放置到该段。除此以外,函数可能会放到COMDAT段。

可以为函数明确指定对齐方式(alignment)。如果未指定对齐方式,则对齐方式设置为0,函数的对齐方式将由特定体系结构设定。如果指定了对齐方式,函数将会被强制对齐,所有的对齐方式都必须遵循2的指数次方对齐。

如果指定了unnamed_addr属性,则两个完全相同(identical)的函数将会被合并。

如果指定local_unnamed_attr属性,则在当前模块内部,该函数的地址不会被符号化(significant)。

别名(Alias)

顾名思义,别名只是已经存在的函数或者变量的一个新的符号(second name)。

@ = [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] alias , * @

连接类型(Linkage)必须指定为private, internal, linkonce, weak, linkonce_odr, weak_odr, external中其中之一。

IFuncs

IFuncs和别名类似,仅创建一个新的符号,使得动态链接器(dynamic linker)可以在运行期推导。

IFuncs可以有一个名称和一个解析器(resolver),该resolver其实是一个函数,被dynamic linker调用,根据一个特定名称返回一个特定地址。

@ = [Linkage] [Visibility] ifunc , * @

Comdats

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

Comdats有一个名字,代表COMDAT键(key)。Comdats有一个selection kind,即链接器如何选择位于两个不同目标文件中的key。

$ = comdat SelectionKind

any

如果selection kind是any类型,代表链接器可以选择任意COMDAT Key。

exactmatch

链接器可以选择任意COMDAT Key的前提条件,是section必须包含相同数据。

largest

链接器选择包含最大COMDAT Key的段。

noduplicates

链接器要求有且仅有一个段包含这个COMDAT Key。

samesize

链接器要求具有COMDAT Key的段必须具有相同的大小。

以下是一个具体的示例:

$foo = comdat largest
@foo = global i32 2, comdat($foo)

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

在COFF格式目标文件中,我们将创建一个COMDAT节(IMAGE_COMDAT_SELECT_LARGEST),保存@foo符号对应的内容,还会创建另一个COMDAT节(IMAGE_COMDAT_SELECT_ASSOCIATIVE),自动关联第一个COMDAT节,保存@bar符号对应的内容。

译注一:LLVM对于COMDAT的描述看得有些云里雾里,因此还是需要补充一些材料。(https://docs.oracle.com/cd/E24847_01/html/E22196/chapter6-93046.html#chapter6-94076)

在ELF目标文件中,有一个节头表,通过该表,可以定位文件的所有节。其中,COMDAT节由其节名称(sh_name)唯一标识。如果链接编辑器余导节名称相同的SHT_SUNW_COMDAT类型的多个节,则将保留一个节,丢弃其余节。任何应用于丢弃的SHT_SUNW_COMDAT节的重定位都会被忽略。在丢弃的节中,定义的任何符号都会被删除。

此外,使用-xF选项调用编译器时,链接器还支持对节重新排序的节命名约定,如果将函数放入名为.sectname%funcname的SHT_SUNW_COMDAT节中,则最后保留的几个SHT_SUNW_COMDAT节都将并入名为.sectname的节中。此方法可用于将SHT_SUNW_COMDAT节放入作为最终目标位置的.text、.data或其他任何节。

译注二:COMDAT实际上跟前面提到的linkonce链接模式有紧密联系。本质上,可以用来消除多个目标文件中因模板实例化、虚函数表重复代码。

命名元数据(Named Metadata)

命名元数据(named metadata)是元数据(metadata)的集合。Metadata nodes(见后续章节)是命名元数据唯一有效的操作数(operand)。

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

参数属性(Parameter Attributes)

一个函数的返回值类型和每一个参数都拥有一组属性(parameter attributes)。

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

目前,仅支持以下参数属性:

zeroext

提示代码生成器(code generator),输入参数或返回值应当由编译器的调用者(对于入参)或被调用者(对于返回值)按照目标体系结构的ABI要求进行零扩展(zero-extended)。

signext

提示代码生成器(code generator),输入参数或返回值应当由编译器的调用者(对于入参)或被调用者(对于返回值)按照目标体系结构的ABI要求(通常是32-bits)进行符号扩展(sign-extended)。

inreg

将函数参数或者返回值放在寄存器中,而非内存。

byval

函数参数通过值传递方式(隐含了内存拷贝)。

inalloca

sret

传入的参数是一个指向结构体的指针,并且这个结构体会被作为返回值。

align

优化器(optimizer)假定指针指向的值,具备指定的对齐方式。

noalias

nocapture

nest

returned

nonnull

dereferenceable()

dereferenceable_or_null()

swiftself

swifterror

Garbage Collector Strategy Names

任何函数都可以指定一个垃圾收集器策略名称:

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

注意,LLVM并不提供垃圾收集器,垃圾收集器本身需要由外部提供。

Prefix Data

内容待补充。

Prologue Data

内容待补充。

Personality Function

内容待补充。

Attribute Groups

内容待补充。

Global Attributes

内容待补充。

Operand Bundles

内容待补充。

Module-Level Inline Assembly

模块可包含模块级别的内联汇编块(module-level inline asm block),对应于GCC文件内联汇编块(file scope inline asm)

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

注意,内联汇编中的字符串必须能够被LLVM自带的汇编器识别。

Data Layout

一个模块可以指定一个target-specific的data layout字符串,用于指定数据应当以何种方式装入内存。

target datalayout = "layout specification"

布局规范(layout specification)包含了一组由‘-’分隔的字符串。

E

数据布局采用大端方式(big-endian)。

e

数据布局采用小端方式(little-endian)。

S

 

(未完待续)

 

你可能感兴趣的:(LLVM Language Reference Manual 阅读笔记)