何为LLVM
在LLVM的官网(https://llvm.org/)中写到The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.翻译过来的意思是“LLVM项目是模块化、可重用的编译器以及工具链技术的集合。”
在理解LLVM时,我们可以认为它包括了一个广义的LLVM和一个狭义的LLVM。广义的LLVM就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及很多的模块;而狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库。
LLVM背景
目前常见的编译器有以下两种:
- GCC
- LLVM
LLVM的作者是Chris Lattner,此人既是LLVM之父也是Swift之父同时还是Clang的主要贡献者。在使用LLVM之前,Apple公司一直使用GCC作为编译器,但是Apple对Objective-C新增的特性,GCC并未配合给予实现,Apple自己开发的GCC模块又很难得到GCC委员会的合并,因此Apple在Chris Lattner毕业时,把他招入靡下开发自己的编译器架构,即LLVM。
Xcode版本 | 应用编译器 |
---|---|
<Xcode3 | GCC |
Xcode3 | GCC+LLVM |
Xcode4.2 | 默认LLVM-Clang |
>Xcode5 | 废弃GCC |
传统的编译器架构和LLVM架构
传统编译器架构.jpgLLVM架构.jpg
- 经典的编译器如GCC在设计上都是提供一条龙服务的: 你不需要知道它使用的IR是什么样的,它也不会暴露中间接口来给你操作它的IR。 换句话说,从前端到后端,这些编译器的大量代码都是强耦合的。这样做的好处是,因为不需要暴露中间过程的接口,它可以在内部做任何想做的平台相关的优化。 而坏处是,假如有N种语言(C、OC、C++、Swift...)的前端,同时也有M个架构(模拟器、arm64、x86...)的target,就需要N*M个编译器。
- LLVM架构不同的前端后端使用统一的中间代码LLVM IR, 那么每当新增加一种语言,就只要添加一个这个语言到IR的前端; 每当新增加一种目标平台,就只要添加一个IR到这个目标平台的后端。 如果有M种语言、N种目标平台,那么最优情况下只要实现 M+N 个前后端。优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改。
Clang和LLVM
LLVM-Clang.jpg因为LLVM只是一个编译器框架,所以还需要一个前端来支撑整个系统,所以Apple又拨款拨人一起研发了Clang(http://clang.llvm.org/),作为整个编译器的前端,Clang是LLVM项目的一个子项目,用来编译C、C++和Objective-C。
相比于GCC,Clang具有如下优点:
- 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
- 占用内存小:Clang生成的AST所占用的内存是GCC的五分之一左右
- 模块化设计:Clang采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
- 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
-
设计清晰简单,容易理解,易于扩展增强
Clang和LLVM.jpg
通过上面两张图可以清晰的描述Clang和LLVM之间的关系。
OC源文件的编译过程
对于iOS开发者来说,整个编译流程可以简要概括为 Clang对代码进行处理形成中间层作为输出,llvm把CLang的输出作为输入生成机器码
Clang的执行过程包含以下几步:
- 宏替换,头文件导入
- 语法分析,代码切割为token
- 组成AST(抽象语法树)
- 生成中间码(IR)
下面我们以一个CommandLine工程来看一下编译的过程:测试代码如下
#import <stdio.h>
#define height 5
int main(int argc, const char * argv[]) {
int a = 2;
int b = 3;
int c = a + b + height;
return 0;
}
- cd到测试工程目录下,命令行查看编译的过程:
clang -ccc-print-phases main.m
clang -ccc-print-phases main.jpg - 查看preprocessor(预处理)的结果:
clang -E main.m
可以看到,括号,符号,关键字等等都被切割成token。
宏替换.jpg
词法分析
- 词法分析,生成Token:
clang -fmodules -E -Xclang -dump-tokens main.m
词法分析.jpg
新增测试代码:
void test(int a, int b) {
int c = a + b - 3;
}
语法分析
- 语法分析,生成语法树-AST:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
语法分析.jpg
如果上面的图表现得还不够清晰,那下面这张图就非常明显的表达了语法树。
语法树AST.jpg
LLVM IR
LLVM IR有三种表示,一种是便于阅读的文本格式,类似于汇编代码,但其实它介于高等语言和汇编之间,这种表示就是给人看的,磁盘文件后缀为.ll;第二种是不可读的二进制IR,被称作位码(bitcode),磁盘文件后缀为.bc;第三种表示是一种内存格式,只保存在内存中,所以谈不上文件格式和文件后缀,这种格式是LLVM之所以编译快的一个原因,它不像gcc,每个阶段结束会生成一些中间过程文件,它编译的中间数据都是这第三种表示的IR。三种格式是完全等价的,我们可以在Clang/LLVM工具的参数中指定生成这些文件,可以通过llvm-as和llvm-dis来在前两种文件之间做转换。
clang -c -emit-llvm main.m 编译产生字节码
clang -S -emit-llvm main.m 编译产生可视化字节码
llvm-dis main.bc main.ll bc字节码转为可视化字节码ll
llvm-as main.ll main.bc 可视化字节码转为字节码bc
源码下载
git clone https://git.llvm.org/git/llvm.git/
下载clang
cd llvm/tools
git clone https://git.llvm.org/git/clang.git/
源码编译
安装cmake和ninja(使用ninja编译LLVM比较快,大约10几分钟,否则需要一个多小时。先安装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=LLVM的安装路径
更多cmake相关选项,可以参考: https://llvm.org/docs/CMake.html
依次执行编译、安装指令
ninja
编译完毕后, 【llvm_build】目录大概 20多G
ninja install
安装完毕后,安装目录大概 10多G
应用与实践
安装完毕,下面就可以开发一个Clang插件来练练手啦。
-
在【clang/tools】源码目录下新建一个插件目录,例如我的叫【WJBPlugin】
创建插件目录.jpg -
在【clang/tools/CMakeLists.txt】仿照最后一行加入内容: add_clang_subdirectory(WJBPlugin),小括号里是插件目录名
CMakeLists添加插件目录名.jpg -
在【WJBPlugin】目录下touch【WJBPlugin.cpp】,再新建一个【CMakeLists.txt】,文件内容是:add_llvm_loadable_module(WJBPlugin WJBPlugin.cpp)
WJBPlugin是插件名,WJBPlugin.cpp是源代码文件
CMakeLists.jpg
WJBPlugin.jpg - 在llvm同级目录下新建一个【llvm_xcode】目录
cd llvm_xcode
cmake -G Xcode ../llvm
会生成LLVM的Xcode项目,打开项目,在【Loadable modules】目录下可以找到我们的插件【WJBPlugin】,这样就可以直接在Xcode里编写插件代码以及编译插件了。
将下面这段c++代码拷入WJBPlugin.cpp
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
namespace WJBPlugin {
class WJBHandler : public MatchFinder::MatchCallback {
private:
CompilerInstance &ci;
public:
WJBHandler(CompilerInstance &ci) :ci(ci) {}
void run(const MatchFinder::MatchResult &Result) {
if (const ObjCInterfaceDecl *decl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
size_t pos = decl->getName().find('_');
if (pos != StringRef::npos) {
DiagnosticsEngine &D = ci.getDiagnostics();
SourceLocation loc = decl->getLocation().getLocWithOffset(pos);
D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Error, "报错:类名中不能带有下划线"));
}
}
}
};
class WJBASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
WJBHandler handler;
public:
WJBASTConsumer(CompilerInstance &ci) :handler(ci) {
matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
}
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
class WJBASTAction: public PluginASTAction {
public:
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {
return unique_ptr<WJBASTConsumer> (new WJBASTConsumer(ci));
}
bool ParseArgs(const CompilerInstance &ci, const vector<string> &args) {
return true;
}
};
}
static FrontendPluginRegistry::Add<WJBPlugin::WJBASTAction>
X("WJBPlugin", "The WJBPlugin is my first clang-plugin.");
选择WJBPlugin这个target进行编译,编译完会在Products生成一个动态库文件【WJBPlugin.dylib】
测试插件
-
Xcode新建一个测试工程,在项目中指定加载插件动态库:Build Settings > Other C Flags。
-Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称 加载插件.jpg
-
首先 hack Xcode,才能使 Xcode 指向我们自己编译的 Clang:
下载XcodeHacking.zip并解压,里面有 HackedBuildSystem.xcspec 和 HackedClang.xcplugin 两个文件,这里可能需要修改一下 HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec 文件,将 ExecPath 的值修改为你编译出来的 Clang 的目录: hackXcode.jpg
- 然后 cd 到解压的 XcodeHacking 目录,将这两个文件用命令行移动到对应的目录下:
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,点击 Target 的 Build Settings搜索c++,修改 Compiler for C/C++/Objective-C 项为 Clang LLVM Trunk(不进行 hack Xcode 操作的话是不会有这个选项的)
compiler.jpg - 先clean一下,然后编译项目,就会在编译日志看到WJBPlugin插件的警告信息了,大功告成。
总结
文章不长,这看似简单的过程也花费了我很多的时间,主要是LLVM过于强大,以至于我不得不在标题前加上了“浅谈”二字,本文属实只是介绍了些皮毛和入门知识,更深层的汪洋大海还需慢慢探索。
参考文献:
LLVM框架/LLVM编译流程/Clang前端/LLVM IR/LLVM应用与实践
LLVM基本概念入门
初探 Clang
LLVM编译原理和使用
网友评论