LLVM 1: IR 汇编语言参考
概述
LLVM IR 是基于 SSA 的 中间代码表示格式。它提供安全类型、底层操作、灵活性及完全性(能够自描述”所有”高级语言)。它也是 LLVM 编译框架中各个阶段间通用的代码表示形式。
LLVM IR 可以使用三种不同的格式:in-memory、on-disk bitcode和汇编文本格式。本文就第三种格式的 LLVM IR 做一些总结,更多信息见参考链接。
标识符
LLVM 区分两种基本类型标识符:全局标识符和局部标识符。全局标识符(函数和全局变量)以@开头,局部标识符以%开头。另外在格式上又可以分为三类:
- 命名标识符:带有
@或%前缀的字符串,如%foo、@bar123等。 - 非命名标识符:带有
@或%前缀的数字,如%2、@42等。 - 常量(
Constant)
所有保留字都不带%或@前缀,所有不会与变量标识符冲突。这些保留字包括操作类型关键字(如add、bitcast、ret等)、原语类型(如void、i32等)及其他。
所有以 !开头的为 Metadata 标识符。
另外,注释信息以 ; 开头。
高级结构
Module(模块)
LLVM 程序由各个Module组成,它是输入程序转换为 IR 后的顶层单元。Module包括函数、全局变量和符号表。不同的 Modules 可以被 LLVM Linker链接到一起。下面以 HelloWorld模块示例:
1 | ; Declare the string constant as a global constant. |
其中.str、puts和main为全局变量,全局变量均为指针,并指向一个内存位置。任何全局变量都必须指定链接类型(Linkage Type),如.str指定为private。
链接类型
private: 这类全局变量只能被当前模块内的对象访问,且在与其他模块链接时可能会被重命名。另外,它不会出现在 Object 文件的任何符号表中。internal:与private类似,但会作为局部符号出现在符号表中,它对应于 C 语言中使用static声明的变量或函数。available_externally:从链接器角度看,该类型等价于external类型。它可以允许被优化或者内联。该类型变量不会被发送到 Object 文件,而只会被优化器使用。linkonce:可以用于实现内联、模板等高级语言特性。未被引用的linkonce变量会被废弃。另外,使用linkonce类型的函数并不一定允许优化器内联它的函数体(Body)到引用它的地方,因为优化器可能并不知道当前函数体就是最后的定义,也不知道将来是否会被更强的定义覆盖。如果要使能内联或者其他优化,可以使用linkonce_odr链接类型。weak:在merge语义上与linkonce相同,除了未被引用的weak全局变量不会被废弃。common:大多数情况下类似于weak,但它用于 C 语言中未初始化的全局变量定义,例如int x;。common类型的符号在merge时的行为与weak类型符号相同,而且在未被引用时,可能不会被删除。common类变量必须声明零初始化器(Zero Initializer)。appending:该类型可能只能应用于数组指针。extern_weak:linkonce_odr, weak_odr:该类型的变量只允许等价的全局变量可以被合并。external:默认链接类型,该类型变量对外部可见,并可以在链接阶段用于外部符号解析。
任何声明的全局变量或函数(与定义区分)具有除external或extern_weak之外的任何链接类型都是非法的。
调用规范
LLVM 支持多种调用规范:
ccc: 默认类型,匹配 C 语言的调用规范。它支持varargs函数调用,并在一定程度上容许函数原型声明和函数定义间的不匹配。fastcc:该类型尝试使得函数调用尽量快速完成,它允许目标使用任何技巧来加速函数调用(它可以不遵循当前平台的 ABI 规范)。它不支持varargs函数调用,并要求函数声明和定义的完全匹配。coldcc:用于很少被执行的函数调用。cc 10:GHC(Glasgow Haskell Compiler ) 规范,它使用寄存器传递所有参数,通过禁止使用CSR(Callee-Saved-Registers)来达到该目的。该规范只在一些特殊情况下使用。另外,它支持tailcall(尾调用)优化。目前只有 X86 有限地支持该规范。cc 11:HiPE( High-Performance Erlang) 规范,它使用(相比于ccc规范)更多的寄存器来传递参数,并且没有定义CSR。它使用和GHC规范相同的 register pinning 机制,用于将频繁访问的运行时组件固定到特定的硬件寄存器。目前只有 X86 支持该规范。webkit_jscc:WebKit的 JavaScript 调用规范。它已经被WebKit FTL JIT实现。它将参数从右到左依次保存到栈上,然后通过平台特定的返回寄存器返回结果值。anyregcc:该规范使用寄存器传递参数,但允许动态分配所用寄存器。preserve_mostcc:该规范尝试尽量使得调用者的代码尽量少地被干扰。它与ccc规范在参数和返回值传递方式上是相同的,但使用不同的 caller/callee-saved 寄存器 集合,这使得可以减轻调用者在保存/恢复大量寄存器操作上的开销。该规范与coldcc相比,更多地被用于频繁调用的函数。目前,它仍然是实验性的,但在未来会作为Object-C Runtime的调用规范。preserve_allcc:该规范比preserve_mostcc更进一步地减少调用者代码所受到的干扰。cxx_fast_tlscc:tailcc:该规范确保在尾部的函数调用可以进行尾调用优化。尾调用优化就是直接使用被调用者的栈帧替代调用者的栈帧,这样可以节省内存,但需要确保被调用者不再引用调用者内部的变量。swiftcc:swift语言的调用规范。cc <n>:使用编号为n的调用规范。
可见性
LLVM 支持下面几种可见性样式(Visibility Styles):
default:hidden:通常,hidden代表该符号不会被放到动态符号表中。protected:
任何使用private或internal链接类型的符号的可见性都为default。
线程本地存储模型
TLS(Thread Local Stroage) 是指一个变量可以被定义为 thread_local,即它不会被线程共享(每个线程都会有一个该变量的拷贝),也就不存在线程间数据竞争问题。TLS 不一定被所有目标平台支持,另外,它包括以下几种模型:
localdynamic: 只在当前共享库中使用的变量。initialexec:在模块中不会被动态载入的变量。localexec: 在可执行文件中定义且只在其中使用的变量。
默认使用 general dynamic模型。
对于 pthread 库,可以使用相应的 API 来使用 TLS。
对于 GCC 可以使用语言级别的特殊语法,如:
1 | __thread int i; |
运行时抢占说明符
Runtime Preemption Specifiers 主要用于全局变量、函数和别名(alias),它有下面的类型:
dso_preemptable:默认类型。表示函数或者变量在运行时会被外部的链接单元替换。dso_local:表示函数或变量将解析为同一链接单元中的符号。即使定义不在此编译单元内,也将生成直接访问,例如使用declare声明的函数。
结构类型
LLVM IR 支持定义 C 语言中的结构体struct类型;同样的,C++ 中的类(class)也可以使用相同的底层原语。structure types 包括 identified和literal类型。示例如下:
1 | struct Point {int X; int Y}; |
转换为 LLVM IR 后:
1 | %struct.Node = type { i32, %struct.Node* } |
全局变量
结合前面的叙述以及本节内容,我们可以总结全局变量的几个特性:
- 全局变量存储在编译阶段分配的区域内(例如
.data段),也可以显式地指定某个段。 - 全局变量的定义必须初始化,但对于声明的全局变量(在其他模块中定义)可以不用初始化。
- 全局变量可以指定一个
链接类型(如果没有声明,则使用默认类型)。 - 全局变量可以指定一个
运行时抢占说明符。 - 全局变量可以指定数据对齐格式,使用
align <n>。 - 全局变量可以指定为
constant,代表该变量的值不会被修改。 - 全局变量总是定义一个指针指向它的内容(content)。
- 全局变量可以指定为
unnamed_addr,表明地址并不重要,仅指内容。两个该类型变量如果拥有相同地初始化器(或相同地定义)可以进行合并。 - 全局变量可以指定为
local_unnamed_addr,表明地址在Module内不重要。 - 全局变量可以被声明为驻留在目标特定的已经编号的地址空间中。
- 全局变量可以指定全局属性(
#<n>),或者一个附加的元数据列表。 - 变量和别名可以指定 TLS 模型。
- 全局变量(或者全局数组的成员)不能是
可扩展向量类型,因为它们的大小在编译阶段未知。
语法表示
1 | @<GlobalVarName> = [Linkage] [PreemptionSpecifier] [Visibility] |
一些示例:
1 | @G = addrspace(5) constant float 1.0, section "foo", align 4 |
函数
函数定义语法:
1 | define [linkage] [PreemptionSpecifier] [visibility] [DLLStorageClass] |
函数声明语法:
1 | declare [linkage] [visibility] [DLLStorageClass] |
函数定义由单个或多个基本块组成,它们构成了函数的 CFG(控制流图)。每个基本块可以在开头显式地声明一个label,如果没有指定,LLVM会自动为其分配一个非命名标识符作为label。
如果没有指定地址空间,则默认设置为布局字符串中指定的程序地址空间(“A”、“G”或“P”)。
Alias 和 IFunc
COMDATS
COMDAT 段被多个目标文件所定义的辅助段。该段的作用是将在多个已编译模块中重复的代码和数据的逻辑块组合在一起。COMDAT 在 C++ 的虚函数表和模板的编译链接中,起着非常重要的作用。
例如,在头文件myclass.h中定义了模板类MyClass<T>,然后 x.cpp 和 y.cpp 都包含了该头文件,并且都使用int实例化这个模板类,即在x.o和y.o中都有MyClass<int>的实例代码。但可执行文件中只需要一份代码即可,因此在链接时会有一个重复代码消除的步骤。
LLVM 定义 COMDAT 的语法:
1 | $<Name> = comdat SelectionKind |
Name 代表 comdat key,在两个 Object 文件中相同 comdat key 的 COMDAT 段将会被合并,合并的方式由 SelectionKind 指定。它包括下面几种:
any:任意选择exactmatch:任意选择,但段中必须包含相同的数据。largestnodeduplicatesamesize
目前,ELF 只支持any和nodeduplicate,COFF支持所有类型。
命名元数据
语法:
1 | ; Some unnamed metadata nodes, |
DWARF 调试数据可以使用Metadata表示。
参数属性
函数的返回类型和参数类型都可以关联一组参数属性(Parameter Attributes)。参数属性是函数的一部分,而不是函数类型的一部分。一些示例如下:
1 | declare i32 @printf(ptr noalias nocapture, ...) |
目前支持的参数属性如下:
signext:指示代码生成器进行符号扩展。zeroext:指示代码生成器进行零扩展。inreg:byval(<ty>):按值传递参数byref(<ty>):按引用传递参数noalias:nocapture: 表明被调用函数不能捕获指针。nonnull:表明参数或返回指针非空。readonly:writeonly:
注意与下文中的函数属性区分。
GC 策略名
在上文中提及,在函数定义和声明中可以指定 gc。其语法为:
1 | define void @f() gc "name" { ... } |
可以使用 LLVM 内置的 GC 策略名 ,或者使用插件提供的策略名。LLVM 本身不包含 GC 支持,它局限于生成特定的机器代码,该机器代码可以与外部提供的 GC 交互。
Prologue 数据
Prologue 属性使得允许在函数开头插入任意代码。它可以用来实现函数hot-patching和instrumentation。Prologue 数据基本上都是和具体目标相关的。
属性组
在 LLVM IR 中可能有多个对象使用相同的属性集,使用属性组,可以使得 IR 减少冗余,更加具有可读性。示例如下:
1 | ; Target-independent attributes: |
函数属性
示例如下:
1 | define void @f() noinline { ... } |
函数属性是函数的一部分,而不是函数类型的一部分,即具有不同函数属性的函数可以具有相同的函数类型。
目前支持的函数类型种类如下:
alignstack(<n>):alwaysinline:allockind("KIND"):alwaysinline:指示内联器总是尝试内联该函数。cold:hot:noinline:指示内联器绝不会内联该函数。noreturn:norecurse:nosync:nounwind: 表明函数绝不会抛出异常。optnone: 表明除了过程间优化 Pass,其他大多数优化 Pass 都应该跳过该函数。speculative_load_hardening: 消减幽灵攻击。ssp: 表明该函数需要emit**栈溢出保护器(Stack Sashing Protector)**。uwtable:mustprogress
数据布局
语法:
1 | target datalayout = "layout specification" |
布局规范包含一个子规范列表,子规范之间通过'-'符号分隔,例如:
1 | target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" |
子规范类型如下:
E:目标以大端格式进行数据布局。e:目标以小端格式进行数据布局。S<size>:指定栈的natural 对齐,以bit为单位。P<address space>:指定程序code 所在的地址空间。G<address space>:指定全局变量所在的地址空间,如果没有指定,则默认为0。A<address space>:指定由alloca创建的对象所在的地址空间,默认为0。p[n]:<size>:<abi>[:<pref>][:<idx>]:指定地址空间n的指针类型的大小size和对齐。i<size>:<abi>[:<pref>]:指定size大小的整数的对齐方式。v<size>:<abi>[:<pref>]:指定size大小的向量(vector)的对齐方式。f<size>:<abi>[:<pref>]:指定size大小的浮点数的对齐方式。a:<abi>[:<pref>]:指定聚合类型对象的对齐方式。F<type><abi>:指定函数指针的对齐方式。m:<mangling>:指定Name Mangling的方式,具体有以下几种:e:使用ELF方式,对于私有符号使用.L前缀。
n<size1>:<size2>:<size3>...:指定目标 CPU 所支持的整数的类型。
目标三元组信息
语法:
1 | target triple = "x86_64-pc-linux-gnu" |
格式:
1 | ARCHITECTURE-VENDOR-OPERATING_SYSTEM |
指针别名规则
指针捕获
给定一个函数调用,而且一个指针作为该调用的参数或者存储在该调用之前的内存中,如果该调用拷贝该指针的任一部分到该函数外部的某个地方,则称该函数捕获该指针。