iOS-深入了解LLVM编译器架构

前言

我们会经常听到编译器这个词语,我们就会想什么是编译器,它的功能是什么,跟我们的开发又有什么关系,这篇文章就带大家走入LLVM编译器架构,揭开编译器的神秘面纱。

1 什么是编译器

我们用Python(解释型)和C(编译型)来先对比下
Python代码如下

print("hello world\n")

我们通过python py1.py命令执行下,看下效果,如图

1

python是python的解释器,这个就是解释型语言的效果。
我们再来看C,代码如下

#include
int main(int argc,char * argv[]){
        printf("hello world\n");
        return 0;
}

我们通过命令clang hello.c,效果如下

2

我们看到并没有执行,而在我们的文件中多了一个a.out文件,在unix下,这是个可执行文件,我们再通过./a.out执行下,效果如图
3

我们看到了执行效果。
从这两个小小的案例可以看出,解释型语言和编译型语言的区别,
解释型语言读取代码就会执行,而编译型语言要先翻译成cpu可以读的二进制代码。
我们刚才的用的clang命令就是C,C++和Objective-C的编译器。
python就是python的解释器。
我们今天就从clang这个编译器开始说起。

2 LLVM介绍

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计划的主要资助者。
目前LLVM已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用。
传统编译器设计

4

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

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

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

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


5

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


6

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

3 编译流程分析

我们先看下一段代码,如下

#import 
int main(int argc, const char * argv[]) {
    return 0;
}

我们通过命令clang -ccc-print-phases main.m执行

7

我们看编译的流程是什么样的。

  • +- 0: input, "main.m", objective-c 读取代码。
  • +- 1: preprocessor, {0}, objective-c-cpp-output 预处理价段,把宏替换,.h的导入进去。
  • +- 2: compiler, {1}, ir 编译价段,前端编译器的任务。
  • +- 3: backend, {2}, assembler 编译器后端,pass(环节,节点)优化,生成汇编代码。
  • +- 4: assembler, {3}, object 生成目标文件。
  • +- 5: linker, {4}, image 链接外部函数,静态库,动态库,生成镜像文件即可执行文件
  • bind-arch, "x86_64", {5}, image 根据不同的架构生成不同的镜像文件。

编译流程的分析

1. 读取代码
读取我们编写的源代码。

2. 预处理
我们改下源码,如

#import 
#define C 30
int main(int argc, const char * argv[]) {
    int a = 10;
    int b = 20;
    printf("%d",a + b +C);
    return 0;
}

接着执行clang -E main.m >> main1.m,我们看下main1.m文件,

# 1 "main.m"
# 1 "" 1
# 1 "" 3
# 379 "" 3
# 1 "" 1
# 1 "" 2
# 1 "main.m" 2

这里是宏展开,我们看下main函数

int main(int argc, const char * argv[]) {
    int a = 10;
    int b = 20;
    printf("%d",a + b +30);
    return 0;
}

直接把我们的C这个宏展开直接替换成30。
我们还用过typedef,我们改下代码

#import 
typedef int RO_INT_64
int main(int argc, const char * argv[]) {
    RO_INT_64 a = 10;
    RO_INT_64 b = 20;
    printf("%d",a + b);
    return 0;
}

执行clang -E main.m >> main1.m,如

typedef int RO_INT_64
int main(int argc, const char * argv[]) {
    RO_INT_64 a = 10;
    RO_INT_64 b = 20;
    printf("%d",a + b);
    return 0;
}

没有展开,typedef只是取别名,增强可读性,不是预处理指令
3.编译价段
3.1词法分析
我们再执行命令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m,词法分析,会把代码切成token,如下所示

annot_module_include '#import 
#d'     Loc=
int 'int'    [StartOfLine]  Loc=
identifier 'main'    [LeadingSpace] Loc=
l_paren '('     Loc=
int 'int'       Loc=
identifier 'argc'    [LeadingSpace] Loc=
comma ','       Loc=
const 'const'    [LeadingSpace] Loc=
char 'char'  [LeadingSpace] Loc=
star '*'     [LeadingSpace] Loc=
identifier 'argv'    [LeadingSpace] Loc=
l_square '['        Loc=
r_square ']'        Loc=
r_paren ')'     Loc=
l_brace '{'  [LeadingSpace] Loc=
int 'int'    [StartOfLine] [LeadingSpace]   Loc=
identifier 'a'   [LeadingSpace] Loc=
equal '='    [LeadingSpace] Loc=
numeric_constant '10'    [LeadingSpace] Loc=
semi ';'        Loc=
int 'int'    [StartOfLine] [LeadingSpace]   Loc=
identifier 'b'   [LeadingSpace] Loc=
equal '='    [LeadingSpace] Loc=
numeric_constant '20'    [LeadingSpace] Loc=
semi ';'        Loc=
identifier 'printf'  [StartOfLine] [LeadingSpace]   Loc=
l_paren '('     Loc=
string_literal '"%d"'       Loc=
comma ','       Loc=
identifier 'a'      Loc=
plus '+'     [LeadingSpace] Loc=
identifier 'b'   [LeadingSpace] Loc=
plus '+'     [LeadingSpace] Loc=
numeric_constant '30'       Loc=>
r_paren ')'     Loc=
semi ';'        Loc=
return 'return'  [StartOfLine] [LeadingSpace]   Loc=
numeric_constant '0'     [LeadingSpace] Loc=
semi ';'        Loc=
r_brace '}'  [StartOfLine]  Loc=
eof ''      Loc=

会把代码切成token,比如大小括号,等于号还有字符串等。

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

8

我们把代码改错,看下效果
9

这里有错误提示。
我分析下语法树

-FunctionDecl 0x7f9aed0bee00  line:5:5 main 'int (int, const char **)'
  |-ParmVarDecl 0x7f9aed01e140  col:14 argc 'int'
  |-ParmVarDecl 0x7f9aed01e288  col:33 argv 'const char **':'const char **'
  `-CompoundStmt 0x7f9aed0bf7d0 
    |-DeclStmt 0x7f9aed0bf010 
    | `-VarDecl 0x7f9aed0bef88  col:15 used a 'RO_INT_64':'int' cinit
    |   `-IntegerLiteral 0x7f9aed0beff0  'int' 10
    |-DeclStmt 0x7f9aed0bf538 
    | `-VarDecl 0x7f9aed0bf038  col:15 used b 'RO_INT_64':'int' cinit
    |   `-IntegerLiteral 0x7f9aed0bf0a0  'int' 20
    |-CallExpr 0x7f9aed0bf740  'int'
    | |-ImplicitCastExpr 0x7f9aed0bf728  'int (*)(const char *, ...)' 
    | | `-DeclRefExpr 0x7f9aed0bf550  'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'
    | |-ImplicitCastExpr 0x7f9aed0bf788  'const char *' 
    | | `-ImplicitCastExpr 0x7f9aed0bf770  'char *' 
    | |   `-StringLiteral 0x7f9aed0bf5b0  'char [3]' lvalue "%d"
    | `-BinaryOperator 0x7f9aed0bf6b0  'int' '+'
    |   |-BinaryOperator 0x7f9aed0bf670  'int' '+'
    |   | |-ImplicitCastExpr 0x7f9aed0bf640  'RO_INT_64':'int' 
    |   | | `-DeclRefExpr 0x7f9aed0bf5d0  'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int'
    |   | `-ImplicitCastExpr 0x7f9aed0bf658  'RO_INT_64':'int' 
    |   |   `-DeclRefExpr 0x7f9aed0bf608  'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int'
    |   `-IntegerLiteral 0x7f9aed0bf690  'int' 30
    `-ReturnStmt 0x7f9aed0bf7c0 
      `-IntegerLiteral 0x7f9aed0bf7a0  'int' 0
  • FunctionDecl 0x7f9aed0bee00 line:5:5 main 'int (int, const char )'
    |-ParmVarDecl 0x7f9aed01e140 col:14 argc 'int'
    |-ParmVarDecl 0x7f9aed01e288 col:33 argv 'const char ':'const char ** 这里就是main函数,返回值int,参数int和char,参数名称arc,int类型,参数argv const char
    类型
  • |-CallExpr 0x7f9aed0bf740 'int'
    | |-ImplicitCastExpr 0x7f9aed0bf728 'int (*)(const char *, ...)'
    | | `-DeclRefExpr 0x7f9aed0bf550 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'这里有一个函数的调用printf,返回int类型。
  • |-ImplicitCastExpr 0x7f9aed0bf788 'const char *'
    | | -ImplicitCastExpr 0x7f9aed0bf770 'char *' | |-StringLiteral 0x7f9aed0bf5b0 'char [3]' lvalue "%d" 这是第一个参数
  • |-DeclStmt 0x7f9aed0bf010
    | -VarDecl 0x7f9aed0bef88 col:15 used a 'RO_INT_64':'int' cinit |-IntegerLiteral 0x7f9aed0beff0 'int' 10
    |-DeclStmt 0x7f9aed0bf538
    | `-VarDecl 0x7f9aed0bf038 col:15 used b 'RO_INT_64':'int' 这里是a,b
  • | | -ImplicitCastExpr 0x7f9aed0bf770 'char *' | |-StringLiteral 0x7f9aed0bf5b0 'char [3]' lvalue "%d"这是第一个参数
  • BinaryOperator 0x7f9aed0bf6b0 'int' '+'是+运算结果,
  • BinaryOperator 0x7f9aed0bf670 'int' '+'
    | | |-ImplicitCastExpr 0x7f9aed0bf640 'RO_INT_64':'int'
    | | | -DeclRefExpr 0x7f9aed0bf5d0 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int' | |-ImplicitCastExpr 0x7f9aed0bf658 'RO_INT_64':'int'
    | | -DeclRefExpr 0x7f9aed0bf608 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int' |-IntegerLiteral 0x7f9aed0bf690 'int' 30 第一个加法运算的结果+30
  • ReturnStmt 0x7f9aed0bf7c0 这里是返回
  • 返回int类型值为0

3.4 生成中间代码(intermediate representation )
代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR。
IR基本语法
@全局标识
%局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit
store写入内存
load读取数据
call调用函数
ret返回

我们改下代码

#import 
#define C 30
typedef int RO_INT_64;

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 -S -fobjc-arc -emit-llvm main.m,会生成main.ll文件,我们看下main.ll文件内容

define i32 @test(i32 %0, i32 %1) #0 { #test(int a, int b )
  %3 = alloca i32, align 4 #开辟空间 4字节对齐 int a3;
  %4 = alloca i32, align 4 #开辟空间 4字节对齐 int a4;
  store i32 %0, i32* %3, align 4 # a3=a;
  store i32 %1, i32* %4, align 4 # a4=b;
  %5 = load i32, i32* %3, align 4 # int a5=a3;
  %6 = load i32, i32* %4, align 4 # int a6=a4;
  %7 = add nsw i32 %5, %6 # int a7 = a5+a6;
  %8 = add nsw i32 %7, 3 # int a8= a7+ 3;
  ret i32 %8 # return a8;
}

这就是test函数IR代码,这是没有经过优化的。
IR的优化
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
经过优化会简洁很多,这里不再赘述。
xcode中的Optimization Level可以设置。
bitCode
clang -emit-llvm -c main.ll -o main.bc

4 生成汇编代码

我们通过最终的.bc或者.ll代码生成汇编代码
命令
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
执行命令
clang -S -fobjc-arc main.ll -o main.s

_test:                                  ## @test
    .cfi_startproc
## %bb.0:
    pushq   %rbp 
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    addl    -8(%rbp), %eax
    addl    $3, %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .globl  _main                           ## -- Begin function main
    .p2align    4, 0x90

这是x86的汇编指令集。
我们再执行这个clang -Os -S -fobjc-arc main.m -o main.s优化的命令

_test:                                  ## @test
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
                                        ## kill: def $esi killed $esi def $rsi
                                        ## kill: def $edi killed $edi def $rdi
    leal    3(%rdi,%rsi), %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .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
    leaq    L_.str(%rip), %rdi
    movl    $6, %esi

这是经过优化过的,main的函数调用的test直接优化成了6。

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

目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输了目标文件(object file)。这里属于后端的作务。
执行命令clang -fmodules -c main.s -o main.o,生成的main.o就是目标文件。通过xcrun nm -nm main.o查看符号,如下所示

                 (undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main

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

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

连接器把编译产生的.o文件和(.dylib.a)文件,生成一个macho-o文件。
我们执行命令clang main.o -o main生成了可执行文件main。
我们再通过命令xcrun nm -nm main,如下

              (undefined) external _printf (from libSystem)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f6d (__TEXT,__text) external _test
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private

这里有两个外部符号_printf(可以找到)和dyld_stub_binder
当我们的程序进入内存的的时候,外部函数会立即跟dyld_stub_binder绑定,这个dyld是强制执行,链接是打个标记,符号在哪个库中(编译期),绑定是在执行的时候把外部函数地址和符号进行绑定(运行期),一定会有dyld_stub_binder这个符号,先绑定这个符号,其它函数的绑定由dyld_stub_binder执行。

总结编译器的流程:

  • 前端:读取代码,词法分析,语法分析,语义分析,生成AST(生成IR)
  • 优化器:根据一个个的pass进行优化,
  • 后端:生成汇编,根据不同的架构生成可执行文件

LLVM最大的好处:前后端分离。
pass的解释:就是“遍历一遍IR,可以同时对它做一些操作”的意思。翻译成中文应该叫“趟”。 在实现上,LLVM的核心库中会给你一些 Pass类 去继承。你需要实现它的一些方法。 最后使用LLVM的编译器会把它翻译得到的IR传入Pass里,给你遍历和修改

总结

这篇文章带大家初步了解了编译器的原理,LLVM的架构。分析了编译的流程,希望这篇文章可以让大家学习到新的知识。

你可能感兴趣的:(iOS-深入了解LLVM编译器架构)