底层原理-28-LLVM的流程

765 阅读8分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

1. LLVM概述

1.1 解释语言和编译语言

在我们日常开发中,比如iOS开发,使用Xcode编译成功才能运行,这个过程就是编译过程由LLVM编译器工作完成。计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释。两种方式只是翻译的时间不同。解释型语言读取代码就会执行,而编译型语言要先翻译成cpu可以读的二进制代码。我们日常开发的OC或swift就是编译语言,python就是编译语言,可以直接通过python解释器进行运行,而oc需要通过编译器编译成可执行二进制进行执行。

1.2 LLVM介绍

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

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

优化器(Optimizer)
优化器负责进行各种优化。改善代码的运行时间,例始消除冗余计算等。

后端(Backend)/代码生成器(CodeGenerator)
将代码映财到目标指令集。生成机器语言,并且进行机器相关的代码优化

iOS的编译器架构
Objcective C/C/C++使用的编译器前端是Clang,Swift是Swift,后端都是LLVM

image.png LLVM的设计
当编译器决定支持多种源语言或多种硬架构时,LLVM的最重要的地方就来了。
其它的编对器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。
LLVM设计的最重要方便是,使用通用的代码表示形式(IR ),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编译语言独立编写前端,并且可以为任意硬件架构独立编写后端。

  • 这种设计的思想就是中间层模式,我们添加一个中间层,新增语言的话只要添加对应的模式即可。

image.png

  • Clang clang是LLVM项目中的一个子项目,它是基于LLVM架构图的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度,它是负责C、C++、OC语言的编译器,属于整个LLVM架构中的 编译器前端,对于开发者来说,研究Clang可以给我们带来很多好处

2. LLVM编译流程

新建文件.m文件

int test(int a,int b){

    return a + b + 3;

}

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

    int a = test(1,2);

    printf("%d",a);

    return 0;

}

通过命令可以打印源码的编译流程

clang -ccc-print-phases main.m

image.png

+- 0: input, "main.m", objective-c
+- 1: preprocessor, {0}, objective-c-cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image

看下具体解释:
0:输入文件,即:找到源文件。
1:预处理阶段,即:处理包括宏的处理,头文件的导入。
2:编译阶段,即:进行词法分析,语法分析,检测语法是否正确,最终生成IR。
3:后端,即:这里LLVM会通过一个一个节点pass去优化,每个pass做一些事情,最终生成汇编代码。
4:汇编代码生成目标文件。
5:链接:之前dyld时候说过,链接动态库和静态库,生成可执行文件。
6:绑定,即:根据不同架构,生成对应的可执行文件。

2.1 预处理阶段

我们在main.m文件中添加宏

#define C 30

typedef int KB_INT_64;

int test(int a,int b){


    return a + b + C;

}

执行clang -E main.m在终端查看结果宏被替换了。

image.png

执行clang -E main.m >> main1.m查看生成对应文件生成替换后的源码

image.png 首先进行头文件导入,之后进行宏的替换。

  • 值得注意的是typedef不具备替换

image.png 运行clang -E main.m >> main1.m

image.png

2.2 编译阶段

进行词法分析,语法分析,检查语法是否正确生成中间代码IR

2.1 词法分析

执行clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m,进行词法分析这里会把代码分割一个个Token,比如大小括括号,等于号,字符串等。

image.png

2.2 语法分析

词法分析完成后就是语法分析,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽像语法树(Abstract Syntax Tree,AST)。语法分析程序判断源程序在结构上是否正确
我们执行clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

image.png

  • 如果导入头文件找不到,可以指定SDK
 clang -isysroot (自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m

 clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -fmodules -fsyntax-only -Xclang -ast-dump main.m

我们把代码改错一下少个}

image.png 再次编译检查语法报错

image.png

  • 分析下int test(int a,int b){}的词法分析
    • FunctionDecl:表示函数代表:used test 'int (int, int)'
      • ParmVarDecl:表示参数:used a 'int'
      • ParmVarDecl:表示参数:used b 'int',2个参数是同一个层级
      • CompoundStmt:表示括号的作用区域即{}区域
      • DeclStmt:描述下面的定义参数
      • VarDecl:参数定义:used d 'KB_INT_64':'int' cinit
      • IntegerLiteral:integer类型的常量
      • BinaryOperator:操作符。这里表示+ 树状结构翻译下类型于:return 20+ (30+(a+b))并不是我们想的那样按加法顺序实现,乘除会优先计算。

2.3 生成中间代码IR

完成以上步骤后,就开始生成中间代码IR了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR

  • 可以通过下面命令可以生成.ll的文本文件,查看IR代码。
clang -S -fobjc-arc -emit-llvm main.m

OC代码在这一步会进行runtime桥接,:property合成ARC处理


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

image.png 简单解释下

image.png 这就是test函数IR代码,这是没有经过优化的。
当然,IR文件在OC中是可以进行优化的,一般设置是在target - Build Setting - Optimization Level(优化器等级)中设置。LLVM的优化级别分别是-O0 -O1 -O2 -O3 -Os(第一个是大写英文字母O),下面是带优化的生成中间代码IR的命令

clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

image.png 代码量大大减少

  • xcode7以后开启bitcode,苹果会做进一步优化,生成.bc的中间代码,我们通过优化后的IR代码生成.bc代码
clang -emit-llvm -c main.ll -o main.bc//根据.ll生成.bc

2.4 生成汇编代码

  • 我们通过最终的.bc或者.ll代码生成汇编代码
 clang -S -fobjc-arc main.bc -o main.s 
 clang -S -fobjc-arc main.ll -o main.s

image.png

  • 生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s

image.png 代码量减少了些

2.5 生成目标文件

目标文件生成,将汇编代码作为输入,生成机器语言,输出目标文件

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

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

$xcrun nm -nm main.o

image.png

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

2.6 链接link

之前在dyld中讲了关于链接的具体流程,这里链接主要链接需要的动态库和静态库,之后生成可执行文件。

  • 静态库和可执行文件进行合并。
  • 动态库独立存在。 连接器把编译生成的.o文件和 .dyld .a文件链接,生成一个mach-o文件
clang main.o -o main//这里为啥不是用llvm,因为clang启动

查看链接之后的符号

$xcrun nm -nm main

image.png 这里有两个外部符号 _printfdyld_stub_binder

image.png
变成了可执行文件。

2.7 绑定

当我们的程序进入内存的的时候,外部函数会立即跟dyld_stub_binder绑定,这个dyld是强制执行,链接是打个标记,符号在哪个库中(编译期),绑定是在执行的时候把外部函数地址符号进行绑定(运行期),一定会有dyld_stub_binder这个符号,先绑定这个符号,其它函数的绑定由dyld_stub_binder执行,最后通过不同的架构,生成相对应的mach-o可执行文件。

3. 总结

LLVM设计就是为了解决日益增加的架构和平台,通过中间层生成中间代码IR,提高了扩展性,降低了耦合性,整个LLVM的流程大概如下:

未命名文件-12.jpg