编译器之王LLVM
引言
- 做iOS开发我们都知道,Xcode内置有编译器
- 打开项目-target->build setting ->搜索compiler(编译器)->能看见编译器就是LLVM
- 早期的Xcode(<4)内置的编译器是GCC,后来的就是LLVM
LLVM简介
- 什么是LLVM
- 官网:https://llvm.org/
- The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
- 译: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”这个名称本身不是首字母缩略词; 它是项目的全名
- 学习LLVM编译器有什么用呢?
- 我们知道,Xcode内置的是LLVM编译器
- 我们写完OC代码后,通过LLVM编译成汇编,然后在编译成二进制可执行文件
- 我们知道在编译时,如果我们的代码有明显的语法或者其他非正常的编写,编译器就会提示红色报错,或者黄色警告,这些都是编译器来控制操作的
- 因此如果我们学习了编译器语法,那么我们就可以给编译器编写一些插件,让我们的OC代码在编译时按照我们自己的想法编译
- 比如:写一个编译器插件用于检查我们写的类命名是否有下划线或者其他非正常字符,如果有,编译时就报错。
传统的编译器架构
- 传统编译器架构通常分为3段
- Frontend:前端
- 词法分析、语法分析、语义分析、生成中间代码
- 我们编写的源码,首先会经过编译器前端(上面操作)生成中间代码
- Optimizer:优化器
- 中间代码优化
- Backend:后端
- 生成机器码
- Frontend:前端
LLVM架构
- 不同语言他的编译前端不一样,比如C语言用Clang
- 不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
- 如果需要支持一种新的编程语言,那么只需要实现一个新的前端
- 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端
- 优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改
- 相比之下,GCC的前端和后端没分得太开,前端后端耦合在了一起。所以GCC为了支持一门新的语言,或者为了支持一个新的目标平台,就变得特别困难
- LLVM现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
Clang
- 什么是Clang?
- LLVM项目的一个子项目
- 基于LLVM架构的C/C++/Objective-C编译器前端
- 官网:http://clang.llvm.org/
- 相比于GCC,Clang具有如下优点
- 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
- 占用内存小:Clang生成的AST(语法树)所占用的内存是GCC的五分之一左右
- 模块化设计:Clang采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
- 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
- 设计清晰简单,容易理解,易于扩展增强
-
Clang与LLVM
- 广义的LLVM
- 整个LLVM架构
- 狭义的LLVM
- LLVM后端(代码优化、目标代码生成等)
- 举例:
- 广义的LLVM
OC源文件的编译过程
-
命令行查看编译的过程:
$ 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
- 从上面可以看出主要经历了6个过程
- input:找到源文件“main.m”
- preprocessor: 预处理器,预处理阶段,主要将“include、import、宏定义”替换为实际代码
- compiler:编译阶段,产生ir
- backend:后端,生成汇编assembler
- assembler:汇编生成目标代码object
- linker: 连接,动态库、静态库等
- bind-arch:生成对应的架构
- 从上面可以看出主要经历了6个过程
-
查看preprocessor(预处理)的结果:
$ clang -E main.m
词法分析
- 就是编译器前端要先对源码进行词法分析
- 词法分析,生成Token:
$ clang -fmodules -E -Xclang -dump-tokens main.m
- 词法分析,生成Token:
语法分析
- 语法树-AST
- 语法分析,生成语法树(AST,Abstract Syntax Tree):
$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
下面代码,生成语法树如下:(这就是把这段代码一点一点的拆分出来)
//源码: void test(int a, int b){ int c = a + b - 3; } //生成的语法树 void 'void' [StartOfLine] Loc=<main.m:19:1> identifier 'test' [LeadingSpace] Loc=<main.m:19:6> l_paren '(' Loc=<main.m:19:10> int 'int' Loc=<main.m:19:11> identifier 'a' [LeadingSpace] Loc=<main.m:19:15> comma ',' Loc=<main.m:19:16> int 'int' [LeadingSpace] Loc=<main.m:19:18> identifier 'b' [LeadingSpace] Loc=<main.m:19:22> r_paren ')' Loc=<main.m:19:23> l_brace '{' Loc=<main.m:19:24> int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:20:5> identifier 'c' [LeadingSpace] Loc=<main.m:20:9> equal '=' [LeadingSpace] Loc=<main.m:20:11> identifier 'a' [LeadingSpace] Loc=<main.m:20:13> plus '+' [LeadingSpace] Loc=<main.m:20:15> identifier 'b' [LeadingSpace] Loc=<main.m:20:17> minus '-' [LeadingSpace] Loc=<main.m:20:19> numeric_constant '3' [LeadingSpace] Loc=<main.m:20:21> semi ';' Loc=<main.m:20:22> r_brace '}' [StartOfLine] Loc=<main.m:21:1> eof '' Loc=<main.m:21:2>
- 语法分析,生成语法树(AST,Abstract Syntax Tree):
LLVM IR
- LLVM IR有3种表示形式(但本质是等价的,就好比水可以有气体、液体、固体3种形态)
- text:便于阅读的文本格式,类似于汇编语言,拓展名.ll,
$ clang -S -emit-llvm main.m
- memory:内存格式
- bitcode:二进制格式,拓展名.bc,
$ clang -c -emit-llvm main.m
- text:便于阅读的文本格式,类似于汇编语言,拓展名.ll,
- IR基本语法
- 注释以分号 ; 开头
- 全局标识符以@开头,局部标识符以%开头
- alloca,在当前函数栈帧中分配内存
- i32,32bit,4个字节的意思
- align,内存对齐
- store,写入数据
- load,读取数据
- 官方语法参考
- https://llvm.org/docs/LangRef.html
- 将下面的函数,生成中间代码:
-
原函数
void test(int a, int b){ int c = a + b - 3; }
-
中间代码IR如下:
-
插件的开发
- 要开发插件必须要经过以下步骤
源码下载
- 下载LLVM
git clone https://git.llvm.org/git/llvm.git/
- 下载clang
- 因为clang是LLVM的子项目,所以他跟LLVM的源码是分开的。
cd llvm/tools
llvm/tools
是第一步下载LLVM的源码文件夹
git clone https://git.llvm.org/git/clang.git/
源码编译
- 为什么要编译LLVM呢?
- 因为要编译起来,我们才能使用LLVM命令行
- 按照下面的步骤编译 ,否则将会编译很长很长时间
- 安装cmake和ninja(先安装brew,https://brew.sh/)
brew install cmake
brew install ninja
- ninja如果安装失败,可以直接从github获取release版放入【/usr/local/bin】中
- https://github.com/ninja-build/ninja/releases
- 在LLVM源码同级目录下新建一个【llvm_build】目录(最终会在【llvm_build】目录下生成【build.ninja】)
cd llvm_build
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=编译好的安装路径(比如:/Users/mac/Desktop/Test)
- 更多cmake相关选项,可以参考: https://llvm.org/docs/CMake.html
- 目录如下图
- 依次执行编译、安装指令
- 编译
ninja
- 编译完毕后, 【llvm_build】目录大概 21.05 G(仅供参考)
- 安装
ninja install
- 安装完毕后,安装目录大概 11.92 G(仅供参考)
- 这个安装位置就是在上面选择的那个路径(3-2 编译好的安装路径)
- 编译
- 安装cmake和ninja(先安装brew,https://brew.sh/)
- 也可以生成Xcode项目再进行编译,但是速度很慢(可能需要1个多小时)
- 在llvm同级目录下新建一个【llvm_xcode】目录
cd llvm_xcode
cmake -G Xcode ../llvm
- 然后打开llvm_xcode文件,会看见一个Xcode项目,用Xcode打开
- 会有一个 autocreate schemes弹框,选择
automatically create schemes
- 然后会发现这个项目会有很多的target,然后选择ALL_BUILD 这个target即可
- 然后编译command +b
- 编译完后,会再products目录下面,show in finder,即可找到
- 会有一个 autocreate schemes弹框,选择
- 在llvm同级目录下新建一个【llvm_xcode】目录
-
安装完毕后的路径图
应用与实践
- libclang、libTooling
- 官方参考:https://clang.llvm.org/docs/Tooling.html
- 应用:语法树分析、语言转换等
- Clang插件开发
- 官方参考
- https://clang.llvm.org/docs/ClangPlugins.html
- https://clang.llvm.org/docs/ExternalClangExamples.html
- https://clang.llvm.org/docs/RAVFrontendAction.html
- 应用:代码检查(命名规范、代码规范)等
- 官方参考
- Pass开发
- 官方参考:https://llvm.org/docs/WritingAnLLVMPass.html
- 应用:代码优化、代码混淆等
- 开发新的编程语言
- https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
- https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/latest/
##clang插件开发
- 设置插件目录
- 开发插件在哪个位置呢?
- 在你下载llvm源码的位置
- 在【llvm/toos/clang/tools】源码目录下新建一个插件目录,假设叫做【zh-plugin】
- 在本例的目录就是:
LLVM学习/LLVM源码/llvm/tools/clang/tools/zh-plugin
- 在【clang/tools/CMakeLists.txt】最后加入内容:
add_clang_subdirectory(zh-plugin)
,小括号里是插件目录名
- 开发插件在哪个位置呢?
- 设置插件的必要文件
- 在【zh-plugin】目录下
- 新建一个【CMakeLists.txt】,文件内容是:
add_llvm_loadable_module(ZHPlugin ZHPlugin.cpp)
touch CMakeLists.txt
- ZHPlugin是插件名,ZHPlugin.cpp是源代码文件
- 新建一个ZHPlugin.cpp文件
touch ZHPlugin.cpp
- 目录如下:
- 新建一个【CMakeLists.txt】,文件内容是:
- 在【zh-plugin】目录下
- 编写插件源码
- 直接在上面那个目录下编写ZHPlugin.cpp中的C++代码,很显然不方便
- 那就生成Xcode模板,用Xcode来编写
- 在llvm同级目录下建一个文件夹名字为:
llvm_xcode
cd llvm_xcode
cmake -G Xcode ../llvm
-
如果这个时候出现一个错误,则执行下面命令:
错误:xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Deve 解决命令: sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
- 在llvm同级目录下建一个文件夹名字为:
- 打开Xcode模板
- 选择
automatically create schemes
- 在项目列表的
Loadable modules
文件夹下,就能找到我自己建的C++文件ZHPlugin.cpp - 就可以在cpp文件中编写代码了
- 编写完之后,要在target列表中选择这个(ZHPlugin)target,进行编译。
- 编译完之后,会生成一个动态库文件:ZHPlugin.dylib
- 在products文件夹下,找到这个动态库,然后show in finder,找到它
- 如下图:
- 选择
- 加载插件
- 在Xcode项目中指定加载插件动态库:BuildSettings > OTHER_CFLAGS
- -Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称
- 然后编译Xcode会发现报错
- 原因是因为Xcode使用的编译器是自带的编译器,不允许加载插件。
- 需要修改成我们之前自己编译的编译器(就是那11.92G)中的Clang
- 那就需要对Xcode进行Hack
- Hack Xcode
- 首先要对Xcode进行Hack,才能修改默认的编译器
- 下载【XcodeHacking.zip】,解压,修改【HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec】的内容,设置一下自己编译好的clang的路径
-
然后在XcodeHacking目录下进行命令行,将XcodeHacking的内容剪切到Xcode内部
sudo mv HackedClang.xcplugin `xcode-select -print- path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print- path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
- 修改Xcode的编译器
- 编译项目
- 编译项目后,会在编译日志看到ZHPlugin插件的打印信息(如果插件更新了,最好先Clean一下项目)
- 更多
- 想要实现更复杂的插件功能,就需要利用clang的API针对语法树(AST)进行相应的分析和处理
- 关于AST的资料
- https://clang.llvm.org/doxygen/namespaceclang.html
- https://clang.llvm.org/doxygen/classclang_1_1Decl.html
- https://clang.llvm.org/doxygen/classclang_1_1Stmt.html
- 在Xcode项目中指定加载插件动态库:BuildSettings > OTHER_CFLAGS