LLVM

1,121 阅读6分钟

基础

解释型语言 & 编译型语言

  • 解释型语言:读到代码即执行
  • 编译型语言:需要先将代码翻译成cpu能读懂的二进制文件再执行

LLVM概述

  • LLVM是构架编译器的框架系统,以C++编写而成
  • 用以优化以任意程序语言编写的程序的编译时间、链接时间、运行时间、空闲时间
  • 对开发者保持开放,并兼容已有脚本

编译器设计

传统

graph LR
A((Source Code)) --> B(Fronted前端)
subgraph 编译器
B --> C(Optimizer优化器)
C --> D(Backend后端/代码生成器)
end
D --> E((Machine Code机器码))
  • 前端:解析源代码。词法分析、语法分析、语义分析,检查源代码是否存在错误,然后构建抽象语法树(LLVM的前端还会生成IR中间代码)
  • 优化器:进行各种优化。改善代码的运行时间,比如消除冗余计算等。
  • 后端/代码生成器:将代码映射到目标指令集,生成机器语言,并进行机器语言的优化。

iOS的编译器架构

  • LLVM使用通用的代码表现形式(IR),可以为任何编程语言独立编写前端,并为任意硬件架构独立编写后端。
  • OC/C/C++的编译器前端是Clang,Swift是SwiftC
graph LR
A(OC/C/C++) --> |Clang|B(LLVM Optimizer)
C(Swift) --> |SwiftC|B
B --> C(LLVM Code Generator)

编译流程

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

执行clang -ccc-print-phases main.m查看输出

               +- 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
  1. 传入文件
  2. 预处理阶段
  3. 编译生成IR(前端)
  4. 后端,通过一个个pass进行相应优化,生成assembler汇编代码
  5. 链接器,链接需要的动态库和静态库。
  6. 通过不同的架构,生成对应的可执行文件(此时为x86_64架构)

预处理阶段

我们加上如下代码

#import <stdio.h>
#define NUM 30
int main(int argc, const char * argv[]){
    printf("%d",NUM);
    return 0;

}

执行指令:clang -E main.m并查看一下

···
// 头文件的导入
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityInternal.h" 1 3 4
# 137 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/Availability.h" 2 3 4
# 70
···
int main(int argc, const char * argv[]){
    printf("%d",30); // 宏的替换
return 0;
}

可以看出这个阶段主要做了:

  1. 导入头文件
  2. 宏的替换

编译阶段

词法分析

对上述代码执行指令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m查看结果: image.png 可以看出这个阶段主要做了:

  • 根据"{","}","","=","char","const"等字符,将代码切成一个个Token

语法分析

对上述代码执行指令clang -fmodules -fsyntax-only -Xclang -ast-dump main.m查看结果 image.png 我们以main函数为例

`-FunctionDecl 0x7ff49f05ae00 <line:4:1, line:7:1> line:4:5 main 'int (int, const char **)'
# 参数
  |-ParmVarDecl 0x7ff49e0342d0 <col:10, col:14> col:14 argc 'int'
  |-ParmVarDecl 0x7ff49e034418 <col:20, col:38> col:33 argv 'const char **':'const char **'
  `-CompoundStmt 0x7ff49f05b570 <col:40, line:7:1>
  # 函数调用
    |-CallExpr 0x7ff49f05b4e0 <line:5:5, col:20> 'int'
    # 函数指针
    | |-ImplicitCastExpr 0x7ff49f05b4c8 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
    | | `-DeclRefExpr 0x7ff49f05b3c8 <col:5> 'int (const char *, ...)' Function 0x7ff49f05af50 'printf' 'int (const char *, ...)'
    | |-ImplicitCastExpr 0x7ff49f05b528 <col:12> 'const char *' <NoOp>
    | | `-ImplicitCastExpr 0x7ff49f05b510 <col:12> 'char *' <ArrayToPointerDecay>
    | |   `-StringLiteral 0x7ff49f05b430 <col:12> 'char [3]' lvalue "%d"
    # 这里可以看到,我们通过这个指令生成的AST是已经经过了预处理的,宏定义已经被替换
    | `-IntegerLiteral 0x7ff49f05b450 <line:2:13> 'int' 30
    # 返回值
    `-ReturnStmt 0x7ff49f05b560 <line:6:1, col:8>
      `-IntegerLiteral 0x7ff49f05b540 <col:8> 'int' 0

这个阶段主要做了:

  1. 验证语法是否正确。在词法分析的基础上将单词序列组合成各类语法短语,如“程序”、“语句”、“表达式”等等
  2. 将所有节点组成抽象语法树AST

生成中间代码IR(Intermediate Representation)

首先我们学习一点IR的基本语法:
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回

代码如下:

#import <stdio.h>

int testFunc(int a,int b) {
    return a + b;
}

int main(int argc, const char * argv[]){
    int c = testFunc(1,2);
    printf("%d",c);
    return 0;
}

指令clang -S -fobjc-arc -emit-llvm main.m查看生成的main.ll文件(截取部分)

···
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @testFunc(i32 %0, i32 %1) #0 {
  // 开辟两个32位的内存空间并进行内存对齐
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  // 将传入的参数a0和a1存储到上面开辟的a3和a4中
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  // 将a3和a4的值又放到a5和a6中【奇奇怪怪】
  %5 = load i32, i32* %3, align 4
  %6 = load i32, i32* %4, align 4
  // 执行加法运算,将结果放到a7中
  %7 = add nsw i32 %5, %6
  // 返回32位(4字节)的返回值a7
  ret i32 %7
}

; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32 %0, i8** %1) #1 {
    ···
  // call调用testFunc方法,传入两个32位的参数12
  %7 = call i32 @testFunc(i32 1, i32 2) 
    ···
  ret i32 0
}
···

IR的优化

  • 可以看到上面的处理过程中,其实是有一些冗余操作的,这是因为我们目前的代码优化等级为None,也就是Xcode -> Build Settings -> Optimization Level image.png
  • 我们可以在指令中指定优化等级,如clang -Os -S -fobjc-arc -emit-llvm main.m,此时的testFuncmain就变成了
define i32 @testFunc(i32 %0, i32 %1) local_unnamed_addr #0 {
  %3 = add nsw i32 %1, %0
  ret i32 %3
}

// main函数中更是连调用testFunc都没有了,直接算出了结果3
define i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 {
  %3 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 3) #3, !clang.arc.no_objc_arc_exceptions !9
  ret i32 0
}

bitcode

  • XCode7之后,开启bitcode苹果会做进一步的优化,生成.bc中间代码。
  • 根据不同架构生成对应.bc。
  • 我们通过IR代码生成.bc代码。clang -emit-llvm -c main.ll -o main.bc

生成汇编

  • 我们可以通过.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.bc -o main.s

生成目标文件【汇编器】

  • 目标文件的生成,是汇编器以汇编代码作为输入,汇编代码 -> 机器代码 -> 目标文件objcet file
  • 这个部分只是clang提供了指令,但完成是后端去完成的
  • 指令:clang -fmodules -c main.s -o main.o
  • 生成.o文件后,我们可以通过xcrun nm -nm main.o指令查看该文件中的符号
                 (undefined) external _printf
0000000000000000 (__TEXT,__text) external _testFunc
000000000000000a (__TEXT,__text) external _main

会发现找不到printf这个方法,这就需要我们下面的步骤了

undefined:当前文件找不到该符号 external:该符号可以被外部访问

生成可执行文件【链接器】

  • 链接器把编译生成的.o文件和库(.dylib,.a)文件,生成可执行文件
  • 指令:clang main.o -o 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
0000000100003f54 (__TEXT,__text) external _testFunc
0000000100003f5e (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private

链接:要知道外部符号是在哪个库里(相当于打个标记,要去找哪个库)
绑定:在执行的时候,将外部函数地址和符号绑定在一起,所以说只要链接就一定有dyld_stub_binder这个外部函数