了解 LLVM 编译器

LLVM概述

LLVM是架构编译器的框架系统,以C++编写而成,用于优化任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)。

LLVM的设计

LLVM的一大特色就是,有着独立的、完善的、严格约束的中间代码表示。这种中间代码,就是LLVM的字节码,是LLVM抽象的精髓,前端生成这种中间代码,后端自动进行各类优化分析,让用LLVM开发的编译器,都能用上最先见的后端优化技术。


传统编译器设计
  • Frontend

        编译器前端的任务是解析源代码(编译阶段),它会进行 词法分析、语法分析、语义分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM的前端还会生成中间代码(intermediate representation,简称IR),可以理解为llvm是编译器 + 优化器, 接收的是IR中间代码,输出的还是IR,给后端,经过后端翻译成目标指令集

  • 优化器 Optimizer

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

  • 后端 Backend(代码生成器 Code Generator)

        将代码映射到目标指令集,生成机器代码,并且进行机器代码相关的代码优化。

LLVM将中间代码优化这个流程做到了极致,LLVM工具链,不但可以生成所支持的各个后端平台的代码,更可以方便的将各语言的前端编译后的模块链接到一起,你可以方便的在你的语言中调用C函数。

LLVM的设计

iOS的编译器架构

OCCC++ 使用的编译器前端是 Clang ,后端都是LLVM,如下图所示

iOS的编译器

Clang概述

Clang 是一个 C++ 编写、基于 LLVM、发布于 LLVM BSD 许可证下的 C/C++/Objective C/Objective C++ 编译器

Clang 编译流程

通过命令可以打印源码的编译阶段
clang -ccc-print-phases 源文件路径

得到结果如下:

  1. 输入文件:找到源文件
    input, "main.m", objective-c

  2. 预处理阶段:这个过程处理包括宏的替换,头文件的导入
    preprocessor, {0}, objective-c-cpp-output

  3. 编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成IR
    compiler, {1}, ir

  4. 后端:这里LLVM会通过一个一个的pass去优化,每个pass做一些事情,最终生成汇编代码
    backend, {2}, assembler

  5. 汇编代码生成目标文件
    assembler, {3}, object

  6. 链接:链接需要的动态库和静态库,生成可执行文件
    linker, {4}, image(镜像文件)

  7. 绑定:通过不同的架构,生成对应的可执行文件
    bind-arch, "x86_64", {5}, image

预处理阶段

    这个阶段主要是处理包括宏的替换,头文件的导入,可以执行命令 clang -E 源文件路径,执行完毕可以看到头文件的导入和宏的替换。define则在预处理阶段会被替换,所以经常被是用来进行代码混淆,目的是为了 app 安全,实现逻辑是:将 app 中核心类、核心方法等用系统相似的名称进行取别名,然后在预处理阶段就被替换,来达到代码混淆的目的。

clang -E main.m

编译阶段

词法分析

预处理完成后就会进行词法分析,这里会把代码切成一个个 token,比如大小括号、等于号、还有字符串等,

clang -fmodules -fsyntax-only -Xclang -dump-tokens 源文件路径
语法分析

语法分析,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等,然后将所有节点组成抽象语法树(Abstract Syntax Tree AST),语法分析程序判断程序在结构上是否正确。

clang -fmodules -fsyntax-only -Xclang -ast-dump 源文件路径
  • FunctionDecl 函数
  • ParmVarDecl 参数
  • CallExpr 调用一个函数
  • BinaryOperator 运算符

生成中间代码IR

完成以上步骤后,就开始生成中间代码 IR 了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成 LLVM IR,可以通过下面命令可以生成 .ll 的文本文件,查看 IR 代码。

clang -S -fobjc-arc -emit-llvm 源文件路径 -o 输出文件路径

Objective-C 代码在这一步会进行 runtime 桥接,property 合成、ARC 处理等。

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

当然,IR 文件在 Objective-C 中是可以进行优化的,一般设置是在 target - Build Setting - Optimization Level(优化器等级)中设置。

Optimization Level 优化级别

LLVM 的优化级别分别是 -O0-O1 -O2 -O3 -Os,下面是带优化的生成中间代码 IR 的命令

clang -Os -S -fobjc-arc -emit-llvm 源文件路径 -o 输出文件路径
BitCode

xcode7 以后开启 bitcode ,苹果会做进一步优化,生成 .bc 的中间代码,我们通过优化后的 IR 代码生成 .bc代码

clang -emit-llvm -c .ll源文件 -o .bc文件

后端

LLVM 在后端主要是会通过一个个的 Pass 去优化,每个 Pass 做一些事情,最终生成汇编代码。我们通过最终的 .bc 或者 .ll 代码生成汇编代码:

 clang -S -fobjc-arc .bc 文件 -o .s 文件
 clang -S -fobjc-arc .ll 文件 -o .s 文件

生成汇编代码也可以进行优化

clang -Os -S -fobjc-arc .m 文件 -o .s 文件

生成目标文件

目标文件的生成,是汇编器以汇编代码作为插入,将汇编代码转换为机器代码,最后输出目标文件(object file)

clang -fmodules -c .s 文件 -o .o 文件

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

xcrun nm -nm .o 文件
  • undefined 表示在当前文件暂时找不到符号

  • external 表示这个符号是外部可以访问的

链接

链接主要是链接需要的动态库和静态库,生成可执行文件,其中静态库会和可执行文件合并,动态库是独立的。连接器把编译生成的 .o 文件和 .dyld.a 文件链接,生成一个 mach-o 文件

clang .o 文件 -o 输出文件

LLVM 工程编译

下载源码

git clone https://github.com/llvm/llvm-project.git

安装 cmake

brew install cmake

通过 xcode 编译 LLVM

  • cmake 编译成 Xcode 项目

    cd llvm-project
    mkdir build_xcode
    cd build_xcode
    cmake -G Xcode ../llvm
    
  • 打开 build_xcode/LLVM.xcodeproj 文件

  • 选择 Automatically Create Schemes (目前 xcode version 13.2.1)

  • 选择 ALL_BUILD Secheme 进行编译

通过 ninja 编译 LLVM

  • 安装 ninja

    brew install ninja
    
  • llvm-project 目录下创建文件夹 build_ninja 、 llvm-release

    cd llvm-project
    mkdir build_ninja
    mkdir llvm_release
    
  • cmake 编译

    cd build_ninja
    cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=../llvm_release
    
  • ninja 运行

     ninja install
    
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容