LLVM 语言参考

概要

LLVM 是一个基于静态单赋值(Static Single Assignment, SSA)的中间表示。
提供类型安全,底层操作符,弹性和清晰的表示“一切”高级语言的能力。
是用在LLVM汇编策略所有情况的所有方面的公共代码表示。

引言

LLVM 代码表示,设计被用在三个形式上:

  • 作为内存中的编译器IR
  • 作为磁盘上的字节码表示(适应即时编译器(Just-In-Time)的快速加载)
  • 作为人类可读的汇编语言表示

这些允许LLVM为高效的编译器翻译和分析提供一个强大的中间表示,并且提供一个自然的意义去调试和可视化翻译。
LLVM三种不同的形式都是等价的。
这个文档描述人类可读的表示和记号。

良好的形式

很重要去提示,这个文档描述“良好形式”的LLVm汇编语言。
在语法分析器接受的和被认为是“良好形式”的之间是有区别的。
例如,下面这个指令语法上没问题,但不是良好形式的:

%x = add i32 1, %x

因为%x的定义并没有只是它所有的使用。(不懂)
LLVM基础设施提供一个verification pass可以用来验证一个LLVM模块是良好形式的。
这个pass是自动的被语法分析器启动的,在被解析输入的汇编之后,优化器输出字节码之前。
违规行为将被 verifier pass 指出表明漏洞存在于翻译pass或者输入到语法解析器。

标识符(Identifiers)

LLVM 标识符有两种基本类型:global 和 local。
全局标识符(函数,全局变量)以 @ 字符开头。
局部标识符(寄存器名字,类型)以 % 字符开头。
还有,标识符有三种不同的格式,为不同的目的:

  1. 命名值的表示以字符串为前缀。例如,%foo, @DivisionByZero, %a.really.long.identifier。实际的正则表达式使用 [%@][-a-zA-Z$._][-a-zA-Z$._0-9]*。标识符要求名字里的其他字符可以被引号包围。特殊字符可以是转义字符使用 "xx",如果xx是ASCII代码而且字符是十六进制。(。。。不想翻译了)
  2. 未命名的值的表示以一个无符号数字值作为前缀。例如,%12, @2, %44。
  3. 常量,在下面的常量那一节描述。

LLVM要求值以一个前缀开始有两个原因:编译器不需要担心名字与保留字冲突,并且保留字的集合在未来可以无代价的扩展。还有,未命名的标识符允许编译器快速的想出一个临时变量,不需要避免符号表冲突。

LLVM保留字和其他语言的保留字十分相似。
有不同操作代码的关键字('add', 'bitcast', 'ret', etc...),有原始类型名字('void', 'i32', etc...),和其他的东西。
这些保留字不会与变量名字冲突,因为没有一个保留字以一个前缀字符开始('%'或者'@')

这有一些LLVM code的例子,用来将整型变量 '%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

将 %X 乘以 8 的最后一种形式说明了LLVM的几种重要的词法特点:

  1. 注释由 ’;' 确定界限,并且直到行末。
  2. 未命名的临时量在计算结果没有赋值到命名值时创建。
  3. 未命名的临时量是序列编号的(使用一个函数递增计数器,从0开始)。注意基本快和未命名的函数参数是参与编号的。例如,如果入口基本快没有给定一个label名字并且所有函数参数都是命名的,会得到数字0.

它还显示了我们在本文档中遵守的约定。当示范指令时,我们会遵守一个指令附带一个注释,定义类型和产生值的名字。

高层结构

模块结构

LLVM 程序由模块组成,每个模块都是输入程序的翻译单元。
每个模块由函数,全局变量,和符号表实体组成。
模块可以被 LLVM 链接器组合到一起,这玩意合并函数(和全局变量)定义,解析前向声明,合并符号表实体。
这是一个 "hello world“ 模块的例子:

; 声明字符串常量为全局常量
@.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00"

; puts 函数的外部声明
declare i32 @puts(i8* nocapture) nounwind

; main 函数定义
define i32 @main() {   ; i32()*
  ; [13 x 18]* 转换为 i8* ...
  %cast210 = getelementptr [13 x i8], [13 x i8]* @.str, i64 0, i64 0

  ; 调用 puts 函数来打印字符串到标准输出
  call i32 @puts(i8* %cast210)
  ret i32 0
}

; 命名元信息
!0 = !{i32 42, null, !"string"}
!foo = !{!0}

这个例子由全局变量 .str,外部声明 puts 函数,main 函数定义和命名元信息 foo 组成。

一般来说,模块由全局值列表(包括函数和全局变量都是全局值)。
全局值由指向内存区域的指针表示(这种情况下,一个指向字符数组的指针,一个指向函数的指针),都具有下面描述的链接类型。

链接类型

全局变量和函数拥有下列链接类型之一:

private

带 private 链接的全局值只能被当前模块中的对象直接访问。
实践中,使用 private 全局值链接代码到模块会导致私有,会根据需要去重命名以避免冲突。
因为符号对模块是私有的,所有引用可以被更新。
这不会显示在目标文件的任何符号表中。

internal

类似于私有,但是值作为局部符号(ELF 中是 STB_LOCAL)出现在目标文件。
这与 C 语言中的 'static' 关键字的概念对应。

available_externally

带这个的全局值不会发射到与 LLVM 模块相关的目标文件中。
来自链接器的透视,一个 available_externally 全局等价于一个外部声明。
他们的存在用来允许内联和在了解全局定义的情况下进行其他优化,就是已知在模块外部某处的东西。
available_externally 链接类型的全局值被允许随意丢弃,允许内联和其他优化。
这个链接类型只在定义中允许,而不是声明。

linkonce

在链接发生时,带这个的全局值与其他同名全局值合并。
这可以用来实现一些内联函数,模板,或者必须在每个翻译单元生成并使用,但是身体必须在后面用更确定的定义覆写的代码的形式。
未被引用的 linkonce 全局量允许被丢弃。
注意 linkonce 链接类型并不真正允许优化器内联这个函数体到调用方,因为他不知道在程序中这个函数的定义是否为更确定的定义,或者会不会被更强的定义覆写。
要启用内联和其他优化,使用 linkonce_odr 链接类型。

weak

weak 链接类型跟 linkonce 有相同的合并予以,除过未被引用 weak 全局量不会被丢弃。
这用于 C 源码中定义的 "weak" 全局量。

common

与 weak 比较像,但是用于 C 中的暂定(tentative)定义,比如全局作用域的 "int x;"
common 链接类型的符号会像 weak 符号一样被合并,并且如果没有被引用也不会被删除。
common 符号不会有显式段,必须拥有零初始化器,不会标记为 'constant'。
函数和别名不会有 common 链接类型。

appending

只被应用到指向数组类型的指针的全局变量上。
当两个带 appending 链接类型的全局变量被链接到一起时,两个全局数组附加到一起。
这是 LLVM,类型安全,等价于在 .o 文件被链接时,拥有一个系统链接器用可识别名字将 "sections" 附加到一起。


不幸的是这与 .o 文件的任何功能都不对应,所以它只用在 llvm 特别解释的变量,如 llvm.global_ctors。

extern_weak

语义是遵循 ELF 对象文件模型: 直到被链接,符号是 weak 的,如果没有被链接,符号将是 null 而不是一个未定义的引用。

linkonce_odr, weak_odr

一些语言允许合并不同的全局量,比如不同语义的两个函数。
其他语言,比如 C++,确保只有等价的全局量被合并(唯一定义规则 —— ODR)。
这类语言可以使用 linkonce_odr 和 weak_odr 链接类型来表明只有等价的全局量被合并。
这些链接类型在其他方面与非 -odr 版本相同。

external

如果上述标识符都没有使用,全局量则为外部可见的,意味着它参与链接并且可以被用来解析外部符号引用。

函数声明使用 external 或者 extern_weak 以外的任何链接类型都是非法的。

调用约定

LLVM 函数,调用(call & invoke)都具有一个可选的调用约定说明。
任何动态 调用方/被调者 对的调用约定必须匹配,不然程序行为是未定义的。
下述调用约定被 LLVM 支持,更多会在未来添加:

ccc - C 调用约定(the C Calling Coverntion)

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

fastcc - 快速调用约定(The fast calling convention)

这个调用约定尝试使得调用尽可能快(例如通过寄存器传递东西)。
这个调用约定允许目标使用任何想要为目标生成快速代码的技巧,不需要符合外部指定的 ABI (Application Binary Interface)。
仅当使用GHC或HiPE约定时才能进行尾调用优化。
这个调用约定并不支持可变参并且要求所有被调者的原型精确匹配函数定义原型。

coldcc - 冷调用约定(The cold calling convention)

这个调用约定尝试使得调用方的代码尽可能高效,并且假设调用通常并不执行。
因此,调用经常保留所有寄存器,所以在调用方这一侧并不打断任何现场范围。
这个调用约定并不支持可变参数,并且要求所有被调者原型精确匹配函数定义原型。
另外内联函数在内联时并不考虑这种函数调用。

cc 10 - GHC 约定(GHC convention)

这个调用约定特别为 Glasgow Haskell Compiler(GHC) 实现。
在寄存器里传递一切,通过禁止被调者保存寄存器来达到极限。
这个调用约定不应轻轻使用,而是仅用于特殊情况,比如另类寄存器固定性能技术,经常用于实现函数式编程语言。
在这时,只有 X86 支持这个约定,并且附带下述限制:

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

这个调用约定支持尾调用优化但是要求调用方和被调者都使用它。

cc 11 - HiPE 调用约定(The HiPE calling convention)

这个调用约定特别为 High-Performance Erlang (HiPE) 编译器实现,Ericsson's Open Source Erlang/OTP system 的原生代码编译器。
...

webkit_jscc - WebKit 的 JavaScript 调用约定(WebKit’s JavaScript calling convention)

这个调用约定特别为 WebKit FTL JIT 实现。
从右向左将参数压栈(就像 cdecl 做的那样),在平台习惯的返回寄存器中返回值。

anyregcc - 动态调用约定,为代码补丁用(Dynamic calling convention for code patching)

这是特殊的调用约定,支持对任意代码序列打补丁以替换调用点。
这个调用约定强制调用参数进入寄存器但是允许他们动态分配。
这一般只用于调用 llvm.experimental.patchpoint,因为只有这个内在记录参数的位置,记录在边表。
详见 LLVM 中的栈映射和修补点

preserve_mostcc - PreserveMost 调用约定(The PreserveMost calling convention)

这个调用约定尝试让调用方代码尽可能不打扰。
约定在参数和返回值的传递上表现得与 C 调用约定等同,但是使用不同的 调用放/被调者 储存寄存器。
...

preserve_allcc - PreserveAll 调用约定

cxx_fast_tlscc - 访问函数的 CXX_FAST_TLS 调用约定(The CXX_FAST_TLS calling convention for access functions)

Clang 生成一个访问函数来访问 C++ 风格 TLS。
...

swiftcc - Swift 语言使用的调用约定

cc <n> - 编号的约定

任何调用约定可以通过数字指定,允许使用目标特定(target-specific)调用约定。
目标特定调用约定从 64 开始。

更多调用约定可以根据需要的添加/定义,为了支持任何其他著名目标无关约定。

可见性风格

所有全局变量和函数拥有以下可见性风格之一:

default - 默认
hidden - 隐藏
protected - 保护

internal 或者 private 链接来行的符号必须拥有 default 可见性。

DLL 储存类

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

dllimport
dllexport

线程本地储存模型

运行时抢占指示符

全局变量,函数和别名可以具有一个可选的运行时抢占指示符。
如果一个抢占指示符没有显式给出,那么这个符号假设为 dso_preemptable

dso_preemptable

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

dso_local

编译器会假设标记为 dso_local 的函数或者变量,会解析到到同一个链接单元的一个符号。
会产生直接访问,即使定义不在这个编译单元。

结构体类型

Last modification:June 20th, 2020 at 12:25 am
 Support
如果觉得我的文章对你有用,请随意赞赏