LLVM IR 学习笔记(一)

什么是 LLVM IR

IR(Intermediate Representation)即中间表示,是程序编译器处理源代码时生成的中间的代码,它具有下列特点。

  • 等效于源代码
  • 可读性低于源代码
  • 相比于源代码更容易转化为机器码
  • 相比于源代码更容易执行代码优化

程序编译器通过将源代码转化为 IR 来使得一些编译工作更简单,不过如果是一些比较简单的编程语言有时也会直接将源码翻译为机器码。IR 是业内常用的一种处理方式,并非强制性的技术。

LLVM 是模块化的,包含一系列可重用的编译器工具链的集合,所以 LLVM IR 就是 LLVM 使用的 IR。

LLVM IR 的三种形式

LLVM IR 有三种等价的表现形式。

  1. 内存的编译器 IR。
  2. 存储在硬盘上的二进制 IR:可以快速地加载,比较适合 JIT 编译器。
  3. 人类可读的汇编 IR:一种汇编语法,可以被人类简单地理解。

本文也让将使用第三种形式的 IR 来介绍(不会有人愿意看二进制吧)。

标识符(Identifier)

LLVM IR 的标识符分为两种类型。

  • 全局标识符:以 @ 开头的符合标准的字符串。
  • 局部标识符:以 % 开头的符合标准的字符串。

所谓符合标准的字符串,其实使用的这个正则 [-a-zA-Z$._][-a-zA-Z$._0-9]*,所以下列标识符都是合法的。

  • %foo
  • @DivisionByZero
  • %a.really.long.identifier

比较特殊的是,如果前缀后的字符串可被理解为一个非负整数,比如 @12,这种标识符被叫做【未命名标识符】,它便编译器快速生成临时变量,不必将注意力分散到避免字符串重复的工作。

使用前缀来定义标识符的好处是不会和保留字冲突,比如保留字 mul 表示乘法,我们就不用担心有一个同名的标识符。

举个例子来看看 IR 看起来长什么样。

%result = mul i32 %x, 8

这段 IR 的作用是将变量 %x 乘以 8 并赋值给变量 %result,此外还有几种等价的形式。

%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

模块(Module)

LLVM 程序由若干个模块组成,每个模块都是一个最小的翻译单元,且每个模块都由函数,全局变量和符号表组成。

多个模块可以被 LLVM linker 组合在一起,这样可以合并符号表,也可以处理前置声明。

下面是一个简单的 LLVM 模块。

; Declare the string constant as a global constant.
@.str = private unnamed_addr constant [13 x i8] c"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(ptr nocapture) nounwind
; Definition of main function
define i32 @main() {
; Call puts function to write out the string to stdout.
call i32 @puts(ptr @.str)
ret i32 0
}
; Named metadata
!0 = !{i32 42, null, !"string"}
!foo = !{!0}
A
; 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(ptr nocapture) nounwind
; Definition of main function
define i32 @main() {
; Call puts function to write out the string to stdout.
call i32 @puts(ptr @.str)
ret i32 0
}
; Named metadata
!0 = !{i32 42, null, !"string"}
!foo = !{!0}
" ; External declaration of the puts function declare i32 @puts(ptr nocapture) nounwind ; Definition of main function define i32 @main() { ; Call puts function to write out the string to stdout. call i32 @puts(ptr @.str) ret i32 0 } ; Named metadata !0 = !{i32 42, null, !"string"} !foo = !{!0}

这个模块作用就是输出字符串 hello world!,最后几行是一些元数据,我们暂时不用关心它们。

链接类型(Linkage Types)

每个函数和全局变量都有一个链接类型,下面将简单介绍几种类型。

  • external:默认链接类型,它是全局外部可见的,这种类型参与链接并可以被作为外部符号被解析。
  • private:仅在当前的 LLVM 模块中可见。
  • internal:与 private 相同,只是在 ELF 文件中的符号格式不太一样。

其余的类型请参考官方文档

调用约定(Calling Conventions)

LLVM 的一些 IR 可以用来调用某个函数,比如 callinvoke,有时我们需要显示地写明调用约定。

简单介绍几种调用约定:

  • ccc:标准 C 语言调用约定。
  • fastcc:一种尽可能快的调用约定,它并非指代某个特定的调用约定,而是将选择权交给编译器。这就要求函数声明和调用时都必须显式地声明此调用约定。
  • coldcc:一种对调用方性能友好的调用约定,它假设这个函数很少被调用,基于此假设可以做一些代码优化。它同样不指代某个特定的调用约定,而是将选择权交给编译器。

其余的调用约定请参考官方文档

可见性(Visibility Styles)

每个函数和全局变量有可见性约束。

  • default:
    • EFL Object file:对于其它模块是可见的。
    • ELF Shared Libraries:这个符号是可以被覆盖的。
  • hidden:一般是外部不可见的。
  • protected:对于 ELF 文件,这表示该符号是外部可见的,但是不允许外部修改。

如果你的符号的链接类型是 private 或者 internal,那么它的可见性必须是 default

结构类型(Structure Type)

结构类型是若干个类型的集合,类似于 C 语言中的结构体。

%T1 = type { <type list> }     ; Identified normal struct type
%T2 = type <{ <type list> }>   ; Identified packed struct type

使用 {} 定义的结构类型会有 padding 来优化访问性能,而使用 <{}> 定时的结构类型则是不会有任何 padding,十分紧凑。

不透明结构类型(Opaque Structure Types)

类似于结构类型,只是我们不知道它的结构是什么,有点类似于前置声明的类型。

%X = type opaque
%52 = type opaque

本文作者:ADD-SP
本文链接https://www.addesp.com/archives/5650
版权声明:本博客所有文章除特别声明外,均默认采用 CC-BY-NC-SA 4.0 许可协议。
暂无评论

发送评论 编辑评论


上一篇