LLVM学习

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.传统的编译器设计

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。

3.LLVM的设计


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架构中的编译器前端。

4.LLVM编译流程

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

clang -ccc-print-phases main.m
  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

替换前



替换后


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

命令执行的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


-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


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

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

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

clang main.o -o main

查看链接之后的符号

执行main

./main

输出 10

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容