LLVM学习

2,202 阅读9分钟

LLVM概述

LLVM是构架编译器的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)、以及空闲时间(idle-time),对开发保持开发,并且兼容已有的脚步。 LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博主主持研发的。2006年Chris Lattner加盟Apple Inc. 并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。 目前LLVM已经被Apple、Facebook、Google等公司采用。

1.传统的编译器设计

image.png

1.1 编译器前端(Frontend)

编译器前端的任务是解析源代码,进行词法分析、语法分析、语义分析,检查源代码是否存在错误,然后会构建为抽象语法树(Abstract Syntax Tree,AST),LLVM的前端还会生成中间代码(intermediate representation,IR)

1.2 优化器

优化器负责进行各种优化。改善代码的大小与运行时间,例如消除冗余的计算。

1.3 后端(Backend)

将代码映射到目标指令集.生成机器语言,并且进行机器相关的代码优化

2.苹果的编译器设计

Ojective C/C/C++使用的编译器前端是Clang,Swift是Swift编译器,后端都是LLVM。

image.png

3.LLVM的设计

image.png LLVM也分三个阶段,但是设计上略微的有些区别, LLVM不同的就是对于不同的语言它都提供了同一种中间表示(IR): 前端可以使用不同的工具对代码进行词法分析、语法分析、语义分析,生成抽象语法树。然后转为LLVM的中间表示,中间部分的优化器通过一系列的pass对IR做优化,后端负责将优化好的IR解释成对应不同架构的机器码。所以LLVM可以为任何编程语言编写独立的前端以及后端。

3.1 Clang与LLVM的关系

Clang是一个C++编写的,基于LLVM架构的轻量级编译器,发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。诞生之初是为了替代GCC编译器,相比GCC而言,它的编译速度快、占用内存小、更加方便进行二次开发。 它属于整个LLVM架构中的编译器前端。

image.png

4.LLVM编译流程

LLVM编译一个源文件的过程:预处理 -> 词法分析 -> Token -> 语法分析 -> AST -> 代码生成 -> LLVM IR -> 优化 -> 生成汇编代码 -> Link -> 目标文件 我们可以通过命令行工具打印出源码的编译主要流程

clang -ccc-print-phases main.m

image.png

  1. 输入文件(input):找到源文件
  2. 预处理阶段(preprocessor):这个过程处理宏定义的替换、头文件的导入
  3. 编译阶段(compiler):进行词法分析(生成Token)、语法分析(生成AST),最终生成中间代码IR 4.后端(backend):这里LLVM会通过一个一个的Pass去优化,每一个Pass做一些事情,最终生成汇编代码 5.汇编(assembler):生成目标文件.o 6.链接(linker):链接需要的动态库和静态库(系统的动态库与静态库在共享缓存中),生成可执行文件 7.绑定不同的架构(bind-arch):,生成对应的可执行文件

4.1 预处理阶段

通过执行如下命令,可以看到头文件的导入与宏定义的替换

clang -E main.m

替换前 image.png 替换后 image.png

4.2 编译阶段

4.2.1 词法分析

预处理完成之后,就会源代码进行词法分析,这里会把代码分割一个个的Token,比如大小括号,字符串以及等号等。

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

通过命令执行之后的代码

annot_module_include '#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        // insert code here...

        int a = 10;

        int b' Loc=<main.m:9:1>

int 'int' Loc=<main.m:10:1>

identifier 'main' [LeadingSpace] Loc=<main.m:10:5>

l_paren '(' Loc=<main.m:10:9>

int 'int' Loc=<main.m:10:10>

identifier 'argc' [LeadingSpace] Loc=<main.m:10:14>

comma ',' Loc=<main.m:10:18>

const 'const' [LeadingSpace] Loc=<main.m:10:20>

char 'char' [LeadingSpace] Loc=<main.m:10:26>

star '*' [LeadingSpace] Loc=<main.m:10:31>

identifier 'argv' [LeadingSpace] Loc=<main.m:10:33>

l_square '[' Loc=<main.m:10:37>

r_square ']' Loc=<main.m:10:38>

r_paren ')' Loc=<main.m:10:39>

l_brace '{' [LeadingSpace] Loc=<main.m:10:41>

at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:11:5>

identifier 'autoreleasepool' Loc=<main.m:11:6>

l_brace '{' [LeadingSpace] Loc=<main.m:11:22>

int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:13:9>

identifier 'a' [LeadingSpace] Loc=<main.m:13:13>

equal '=' [LeadingSpace] Loc=<main.m:13:15>

numeric_constant '10' [LeadingSpace] Loc=<main.m:13:17>

semi ';' Loc=<main.m:13:19>

int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:14:9>

identifier 'b' [LeadingSpace] Loc=<main.m:14:13>

equal '=' [LeadingSpace] Loc=<main.m:14:15>

identifier 'a' [LeadingSpace] Loc=<main.m:14:17>

plus '+' [LeadingSpace] Loc=<main.m:14:19>

identifier 'COUNT' [LeadingSpace] Loc=<main.m:14:21>

semi ';' Loc=<main.m:14:26>

identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:15:9>

l_paren '(' Loc=<main.m:15:15>

string_literal '"%d"' Loc=<main.m:15:16>

comma ',' Loc=<main.m:15:20>

identifier 'b' Loc=<main.m:15:21>

r_paren ')' Loc=<main.m:15:22>

semi ';' Loc=<main.m:15:23>

r_brace '}' [StartOfLine] [LeadingSpace] Loc=<main.m:16:5>

return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:17:5>

numeric_constant '0' [LeadingSpace] Loc=<main.m:17:12>

semi ';' Loc=<main.m:17:13>

r_brace '}' [StartOfLine] Loc=<main.m:18:1>

eof '' Loc=<main.m:18:2>

4.2.2 语法分析

它的任务就是验证程序的语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽象语法树(AST),

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

image.png 命令执行的main函数代码如下

-ImportDecl 0x7fd3c7820378 <main.m:9:1> col:1 implicit Foundation
`-FunctionDecl 0x7fd3c7820640 <line:10:1, line:17:1> line:10:5 main 'int (int, const char **)'//main函数
  |-ParmVarDecl 0x7fd3c78203d0 <col:10, col:14> col:14 argc 'int'//第一个参数argc
  |-ParmVarDecl 0x7fd3c78204f0 <col:20, col:38> col:33 argv 'const char **':'const char **' //第二个参数 argv
  `-CompoundStmt 0x7fd3c7820de0 <col:41, line:17:1> //复合声明 {}
    |-ObjCAutoreleasePoolStmt 0x7fd3c7820d98 <line:11:5, line:15:5>//自动释放池声明
    | `-CompoundStmt 0x7fd3c7820d78 <line:11:22, line:15:5>//复合声明{}
    |   |-DeclStmt 0x7fd3c7820bf8 <line:13:9, col:19> //倾斜声明
    |   | `-VarDecl 0x7fd3c7820790 <col:9, col:17> col:13 used a 'int' cinit//变量int a
    |   |   `-IntegerLiteral 0x7fd3c78207f8 <col:17> 'int' 10//整形字面量10
    |   `-CallExpr 0x7fd3c7820d00 <line:14:9, col:22> 'int'//调用printf(<#const char *restrict, ...#>)
    |     |-ImplicitCastExpr 0x7fd3c7820ce8 <col:9> 'int (*)(const char *, ...)' <FunctionToPointerDecay>//print 隐式函数表达式int (*)(const char *, ...)
    |     | `-DeclRefExpr 0x7fd3c7820c10 <col:9> 'int (const char *, ...)' Function 0x7fd3c7820820 'printf' 'int (const char *, ...)'//printf函数描述,函数地址,名称,返回值类型,参数类型
    |     |-ImplicitCastExpr 0x7fd3c7820d48 <col:16> 'const char *' <NoOp>//printf函数第一个参数
    |     | `-ImplicitCastExpr 0x7fd3c7820d30 <col:16> 'char *' <ArrayToPointerDecay>//printf函数第二个参数
    |     |   `-StringLiteral 0x7fd3c7820c68 <col:16> 'char [3]' lvalue "%d"//第三个变量 %d
    |     `-ImplicitCastExpr 0x7fd3c7820d60 <col:21> 'int' <LValueToRValue> 
    |       `-DeclRefExpr 0x7fd3c7820c88 <col:21> 'int' lvalue Var 0x7fd3c7820790 'a' 'int'//int a
    `-ReturnStmt 0x7fd3c7820dd0 <line:16:5, col:12>//返回声明
      `-IntegerLiteral 0x7fd3c7820db0 <col:12> 'int' 0//返回0

4.3 生成中间代码IR

完成以上步骤后就开始生成中间代码IR,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR,可以通过以下命令生成.ll文件,查看IR代码。注意:没有经过编译器优化

clang -S -fobjc-arc -emit-llvm main.m

4.3.1 IR的基本语法

@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回

执行命令之后的main函数代码如下:

; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.6"

@.str = private unnamed_addr constant [3 x i8] c"%d\00", align 1

; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4 // 开辟一个4字节内存 %3
  %4 = alloca i32, align 4 // 开辟一个4字节内存 %4
  %5 = alloca i8**, align 8 // 开辟一个8字节内存 %5
  %6 = alloca i32, align 4  // 开辟一个4字节内存 %6
  store i32 0, i32* %3, align 4  // 将%3写入内存
  store i32 %0, i32* %4, align 4 // 将%4写入内存
  store i8** %1, i8*** %5, align 8 // 将%5写入内存
  %7 = call i8* @llvm.objc.autoreleasePoolPush() #1 //调用autoreleasePoolPush
  store i32 10, i32* %6, align 4 //将10写入 %6,即 %6 = 10
  %8 = load i32, i32* %6, align 4 //读取 %6 赋%8 = 10
  %9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %8)//调用printf函数
  call void @llvm.objc.autoreleasePoolPop(i8* %7)//调用 autoreleasePoolPop
  ret i32 0 //返回0
}

4.3.2 IR的优化

LLVM的优化级别分别是-O0 -O1 -O3 -Os -Ofast -Oz

image.png

image.png -O3:最快,但是包最大 -Os:是xcode默认的优化级别,它平衡了包体积大小与速度 -Oz:加载慢,但是包体积小,它内部是将多个函数汇编代码相同的指令放到一个新的函数 我们可以通过以下命令行去查看优化之后的IR

clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
//未开启优化之前的main函数
define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca i32, align 4
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %7 = call i8* @llvm.objc.autoreleasePoolPush() #1
  store i32 10, i32* %6, align 4
  %8 = load i32, i32* %6, align 4
  %9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %8)
  call void @llvm.objc.autoreleasePoolPop(i8* %7)
  ret i32 0
}
//开启-Os优化之后的main函数
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
  %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
  %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 10) #3, !clang.arc.no_objc_arc_exceptions !9
  tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #1
  ret i32 0
}

从上面我们可以很明显的看到了编译器做了很大的优化。

4.4 bitCode

xcode以后开启bitcode苹果会进一步的优化,生成.bc的中间代码,我们可以优化后的IR代码生成.bc代码,

clang -emit-llvm -c main.ll -o main.bc

4.5 生成汇编代码

我们可以通过最终的.bc或者.ll代码生成汇编代码

clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15, 6	sdk_version 10, 15, 6
	.globl	_main                   ## -- Begin function main
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$32, %rsp
	movl	%edi, -4(%rbp)          ## 4-byte Spill
	movq	%rsi, -16(%rbp)         ## 8-byte Spill
	callq	_objc_autoreleasePoolPush
	leaq	L_.str(%rip), %rdi
	movl	$10, %esi
	movq	%rax, -24(%rbp)         ## 8-byte Spill
	movb	$0, %al
	callq	_printf
	movq	-24(%rbp), %rdi         ## 8-byte Reload
	movl	%eax, -28(%rbp)         ## 4-byte Spill
	callq	_objc_autoreleasePoolPop
	xorl	%eax, %eax
	addq	$32, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"%d"

	.section	__DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
	.long	0
	.long	64
.subsections_via_symbols

生成汇编代码也可以进行优化

clang -Os -S -fobjc-arc main.m -o main.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15, 6	sdk_version 10, 15, 6
	.globl	_main                   ## -- Begin function main
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	pushq	%rbx
	pushq	%rax
	.cfi_offset %rbx, -24
	callq	_objc_autoreleasePoolPush
	movq	%rax, %rbx
	leaq	L_.str(%rip), %rdi
	movl	$10, %esi
	xorl	%eax, %eax
	callq	_printf
	movq	%rbx, %rdi
	callq	_objc_autoreleasePoolPop
	xorl	%eax, %eax
	addq	$8, %rsp
	popq	%rbx
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"%d"

	.section	__DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
	.long	0
	.long	64
.subsections_via_symbols

4.6 生成目标文件(汇编器)

目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器语言,最后输出目标文件(object file)

clang -fmodules -c main.s -o main.o

通过nm命令,可以查看main.o中的符号

xcrun nm -nm main.o

image.png _printf 是一个undefined external的 undefined:表示在当前文件暂时找不到符号_printf external:表示这个符号是外部可以访问的

4.7 生成可执行文件(链接)

链接器把编译生成的.o文件和(.dylib .a)文件链接之后,生成一个mach_o文件

clang main.o -o main

查看链接之后的符号

image.png

执行main

./main

输出 10