编译器王者-LLVM

iOS | LLVM | Objective-C

LLVM 是什么?

LLVM-logo

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
官网:https://llvm.org/

LLVM 项目是模块化、可重用的编译器以及工具链技术的集合,美国计算机协会(ACM)将其2012年软件系统奖项颁给了LLVM,之前曾获得此奖项的软件和技术包括:Java、Apache、Mosaic、the World Wide Web、Smalltalk、UNIX、Eclipse等

创始人: Chris Lattner,亦是Swift之父

  • 有些文章把 LLVM 当做 Low Level Virtual Machine(低级虚拟机)的缩写简称,官方描述如下:
  • The name “LLVM” itself is not an acronym; it is the full name of the project.
    “LLVM” 不是首字母缩略词,它是项目的全称

传统的编译器架构

传统编译器架构
传统编译器架构:

  • Frontend:前端(词法分析、语法分析、语义分析、生成中间代码)
  • Optimizer:优化器(中间代码优化)
  • Backend:后端(生成对应平台、设备、架构机器码)

LLVM架构

传统编译器架构
不同的前端后端使用统一的中间代码 LLVM Intermediate Representation(LLVM IR)

统一中间代码的优点:
1> 如果需要支持一种新的编程语言,只需要实现一个新的前端
2> 如果需要支持一种新的硬件设备,只需要实现一个新的后端
3> 优化阶段是一个通用的阶段,它针对的是统一的 LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改

相比之下,GCC 的前端和后端没有分的太开,前端后端耦合在一起。所以 GCC 为了支持一门新的语言,或者支持一个新的目标平台,就变得非常困难

LLVM 现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、JAVA、.NET、Python、Ruby、Scheme、Haskell等)

Clang

1> 什么是 Clang ?
Clang 是 LLVM 项目的一个子项目,基于 LLVM 架构 C/C++/Objective-C 的编译器前端

官网:http://clang.llvm.org/

2> 相比于 GCC,Clang 具有如下有点:
编译速度快:在某些平台上,Clang 的编译速度显著的快过 GCC(Debug 模式下编译 OC 速度比 GCC 快 3 倍)
占用内存小:Clang 生成的 AST(语法树) 所占用的内存是 GCC 的五分之一左右
模块化设计:Clang 采用基于库的模块化设计,易于 IDE 继承及其他用途的作用
诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据(metadata),有利于调试和错误定位
设计清晰简单,容易理解,易于扩展增强

Clang 与 LLVM

传统编译器架构
广义的 LLVM:整个 LLVM 架构
狭义的 LLVM:LLVM 后端(代码优化、目标代码生成等)

传统编译器架构

OC源文件的编译过程

命令行查看编译的过程:$ clang -ccc-print-phases main.m
OC源文件编译器过程1
输入 main.m => 预处理器,预处理(include、import、宏定义…),输出 cpp 文件 => compiler 编译成中间代码 ir => 后端,目标代码 => 链接动态库等 => 适合 ** 架构的代码

查看preprocessor(预处理)的结果:$ clang -E main.m

1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define AGE 30
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
int c = a + b + AGE;
return 0;
}

OC源文件编译过程2
执行上面的命令行之后,发现预处理器将 <stdio.h> 中代码引了进来,而且将 AGE 宏 也替换成了 30

词法分析

词法分析,生成 Token:$ clang -fmodules -E -Xclang -dump-tokens main.m
OC源文件编译过程2
将代码中每个部分生成 token,’int’、’main’、’(‘、’int’、’argc’ …,一遍后面分析,Loc=main.m:13:9表示 main.m 13 行第 9 个字符

语法树 - AST

语法分析,生成语法树(AST,Abstract Syntax Tree):$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

词法分析之后将 Token 拼接、联系起来,生成语法树:
OC源文件编译过程2

LLVM IR

LLVM IR 有3中表示形式(但本质是等价的,就好比水可以有气体、液体、固体3中形态)

1>text:便于阅读的文本格式,类似于汇编语言,扩展名 .ll,$ clang -S -emit-llvm main.m
2>memory:内存格式
3>bitcode:二进制格式,扩展名 .bc,$ clang -c -emit-llvm main.m

1
2
3
void test(int a, int b) {
int c = a + b - 3;
}

上面代码生成 text 格式中间代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
define void @test(i32, i32) #0 {
// 定义一个局部变量 int a
%3 = alloca i32, align 4
// 定义一个局部变量 int b
%4 = alloca i32, align 4
// 定义一个局部变量 int c
%5 = alloca i32, align 4
// 将 0(第一个参数)的值赋给 3,即 a
store i32 %0, i32* %3, align 4
// 将 1(第二个参数)的值赋给 4,即 b
store i32 %1, i32* %4, align 4
// 读取 3 的值给 6,即 a
%6 = load i32, i32* %3, align 4
// 读取 4 的值给 7,即 b
%7 = load i32, i32* %4, align 4
// 6 + 7,即 a + b,给 8
%8 = add nsw i32 %6, %7
// 8 - 3,即 a + b - 3
%9 = sub nsw i32 %8, 3
// 将 a + b - 3 的结果给 5,即 c,即 c = a + b - 3
store i32 %9, i32* %5, align 4
ret void
}

IR 基本语法:
1> 注释以分号 ; 开头
2> 全局标识符以@开头,局部标识符以%开头
3> alloca,在当前函数栈帧中分配内存
4> i32,32bit,4个字节的意思
5> align,内存对齐
6> store,写入数据
7> load,读取数据

官方语法参考:
https://llvm.org/docs/LangRef.html


上面带大家简单熟悉了下 LLVM,感兴趣的童鞋可以进入官网自行深入研究。光了解是不是有点无趣呢,接下来,我们来开发一个我们自己的编译器插件

LLVM、Clang 源码下载

下载LLVM
\$ https://git.llvm.org/git/llvm.git/
大小 661.3 M,仅供参考

下载clang
\$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/
大小 242.9 M,仅供参考

源码编译

我们在终端可以敲 clang 命令,是因为在 Xcode …/XcodeDefault.xctoolchain/user/bin目录下已经安装了Clang,并且MAC 默认用的是 Xcode 内置的 Clang。既然我们要开发自己的编译器插件,所以在源码下载完成之后,我们需要编译我们自己的 Clang

安装 cmake 和 ninja(先安装brew,https://brew.sh
1> $ brew install cmake
2> $ brew install ninja

:ninja 如果安装失败,可以直接从 github 获取 release 版放入 /usr/local/bin中,https://github.com/ninja-build/ninja/releases


在LLVM源码统计目录下新建一个 llvm_build目录(最终会在 llvm_build 目录下生成 build.ninja

/LLVM_ALL/llvm
/LLVM_ALL/llvm_build
/LLVM_ALL/llvm_release
/LLVM_ALL/llvm_xcode

$ cd llvm_build
$ cmake -G Ninja ../llvm(指定 llvm 源码位置,会将 llvm 源码在llvm_build 目录下生成 ninja 的模板,注:llvm 路径需根据自己的 llvm 目录相对于 llvm_build 的位置) 成功的标识:llvm_build 目录下生成 build.ninja 文件

指定LLVM编译目标路径:
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安装路径
(eg.) $ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=/users/username/Desktop/项目/LLVM_ALL/llvm_release/

更多 cmake 相关选项,可以参考:https://llvm.org/docs/CMake.html


依次执行编译、安装指令
$ ninja
编译完毕后, llvm_build 目录大概 21.61 G(仅供参考)
$ ninja install
安装完毕后,安装目录大概 12.21 G(仅供参考)


生成 Xcode 模板工程,使用 Xcode 进行编译
$ cd llvm_xcode
$ cmake -G Xcode ../llvm

应用与实践

1> libclang、libTooling
官方参考:https://clang.llvm.org/docs/Tooling.html
应用:语法树分析语言转换

2> Clang 插件开发
官方参考:
https://clang.llvm.org/docs/
https://clang.llvm.org/docs/ClangPlugins.html
https://clang.llvm.org/docs/ExternalClangExamples.html
https://clang.llvm.org/docs/RAVFrontendAction.html
应用:代码检查(命名规范、代码规范)

3> Pass开发
官方参考:http://llvm.org/docs/WritingAnLLVMPass.html
应用:代码优化代码混淆

4> 开发新的编程语言
https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/latest