OC底层原理探索之LLVM上

1,971 阅读7分钟

解释器和编译器

首先创建两个文件,一个后缀是.py,一个后缀是.m image.png

使用python时直接运行了。而使用clang的时候生成了一个a.out的二进制文件。这说明了解释性语言和编译型语言最大的区别在于:解释性语言读到代码直接执行,编译型的需要先翻译成CPU能读取的二进制文件。 我们使用open /usr/bin可以看到我们系统的编译器

image.png clang可以编译c/c++/oc,刚才的解释器python也在这个目录之下。

LLVM概述

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

传统编译器设计

image.png

编译器前端:解析源代码,词法分析,语法分析,语义分析抽象语法树AST,LLVM前端还会生成中间代码IR 优化器:各种优化,改善代码的运行时间,例如消除冗余计算 编译器后端/代码生成器:将代码映射到目标指令集。生成机器语言,并且进行机器相关的代码优化 ​

LLVM的设计

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

LLVM牛逼之处在于:它将前端和后端分离开来,前端读完之后生成IR交给优化器,优化器优化完之后把这个IR交给后端,所以只要出现一款设备,就新增一款新的设备的后端,只要出现一个高级语言,就研发一款新的高级语言的前端。这样所有的高级语言和设备都能支持。 ​

Clang

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

编译整体流程

clang -ccc-print-phases main.m image.png 1.预处理阶段 导入头文件 宏定义 2.编译阶段:前端处理,词法分析-> IR 3.后端:通过pass节点优化成最终的汇编代码,优化就是就是对IR的优化 4.汇编:生成目标文件 5.链接:把库的代码链接生成镜像文件,磁盘里的是可执行文件,运行到内存的是镜像 6.根据不同的架构生成不同的可执行文件 ​

编译流程-预处理阶段

#import <stdio.h>
#define C 30
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 + C);
    return 0;
}

clang -E main.m >> main1.m得到main1.m文件,打开该文件感受下 image.png 这里实际上就是把头文件导入和宏展开了,拉到最下面main函数这里,这里的C已经替换成了30

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

tips:typedef不是预处理指令,经过clang -E之后并不会替换掉。 ​

编译流程-编译阶段

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m image.png 词法分析会把这里的代码切成一个个token,比如括号,等号等。在词法分析的基础上进行语法、语义分析,生成语法树clang -fmodules -fsyntax-only -Xclang -ast-dump main.m image.png 左边的test方法在词法分析之后,生成了右边的FunctionDecl,如果当前的语法有问题,在词法分析的时候会报错,在预处理阶段和词法分析则不会。 ​

编译流程-中间代码IR

完成以上步骤后就开始生成中间代码IR了,代码生成器(CodeGeneration)会将语法树自顶向下遍历逐步翻译成LLVMIR。通过下面命令可以生成II的文本文件,查看IR代码。 clang -S -fobjc-arc -emit-llvm main.m 生成了一个main.ll文件 ObjectiveC代码在这一步会进行runtime的桥接、property合成、ARC处理等IR的基本语法

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

image.png 上面是没有优化的加法运算,可以看到IR中的计算非常繁琐。我们可以使用下面命令来优化 clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll,LLVM的优化级别分别是 -0s -00 -01 -02-03 对应我们Xcode的设置选项 image.png 优化之后的main.ll文件中的test函数是 image.png xcode7以后开启bitcode苹果会做进一步的优化, 生成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 mainll.s 分别使用bc和ll文件已经原始m文件生成汇编 image.png 经过对比发现通过bitcode和Os优化之后的代码在main函数的时候都没有调用test函数,而是直接把test计算好的结果拿到,而原始代码未经过优化的会按照方法调用顺序来运行。 ​

汇编流程-生成目标文件

目标文件的生成,是汇编器以汇编代码为输入,将汇编代码转换成机器代码,最后输出目标文件.o clang -fmodules -c main.s -o main.o image.png 我们发现printf函数找不到,此时少了一步链接, 使用clang main.o -o main生成可执行文件。我们再查看当前的可执行文件,发现与之前的.o文件有所差别,这中间的差别就是链接器在发挥作用。 image.png 此时发现链接了外部的两个函数。一个是链接函数一个是绑定函数,链接只是打个标记,告诉需要去找哪个库。绑定是在执行的时候 external _printf (from libSystem) :_printf函数是在libSystem这个库中 external dyld_stub_binder (from libSystem):执行的时候,把外部的_printf函数地址和当前的符号进行绑定,绑定是在执行期,链接是在编译期,所以当前的阶段中binder函数是一定存在的

Clang插件环境配置

LLVM下载:由于国内的网络限制,我们需要借助镜像下载LLVM的源码链接

  1. 下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
  1. 在LLVM的tools目录下下载Clang:clang是LLVM的子项目
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
  1. 在LLVM的projects目录下下载compiler-rt,libcxx,libcxxabi
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
  1. 在Clang的tools下安装extra工具
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e xtra.git
  1. LLVM编译,由于最新的LLVM只支持cmake来编译了,我们还需要安装cmake
brew install cmake
  1. 通过xcode编译LLVM,cmake编译成Xcode项目:注意cmake的工程路径,编译完之后就可以使用Xcode打开
mkdir build xcode 
cd build_xcode
cmake -G Xcode ../llvm
  1. 使用Xcode编译Clang,如果选择自动创建Schemes,会把所有的工程都创建非常耗时,我们可以选择只添加这两个Schemes,然后这两个都进行编译,预计1H+

image.png

Clang创建插件

  1. /llvm/tools/clang/tools目录下新建插件

image.png

  1. 修改/llvm/tools/clang/tools下的CMakeLists.text文件,把新增的插件添加上去
add_clang_subdirectory(libclang) // 系统的
add_clang_subdirectory(xxx)      // 自己新增的插件
  1. 新建的插件加上配置文件,可以参考下其他的系统带的插件,都是包含了这个.cpp文件和CMakeLists.text

image.png 在这个CMakeLists.text文件写入

add_llvm_library( xxx MODULE BUILDTREE_ONLY xxx.cpp)

具体的实现代码,下篇见。