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

- 编译器前端(Frontend)编译器的前端任务是解析源代码。 会进行词法分析、语法分析、语义分析。检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM前端还会生成中间代码(intermediate representation, IR)
- 优化器(Optimizer)优化器负责各种优化。改善代码的运行时间,如消除冗余计算等
- 后端(Backkend)/ 代码生成器(CodeGenerator) 将代码映射到目标指令集,生成机器语言,并进行机器相关的代码优化 (目标指不同操作系统)
- iOS的编译器架构:Objective-C/C/C++使用的编译器前端是Clang,Swift是swift,后端都是LLVM。

LLVM的设计
- 当编译器决定支持多种源语言或多种架构时,LLVM最重要的地方就来了。其他编译器如GCC是一个非常成功的编译器,但由于它作为整体应用程序设计的,用途受到了限制。
- LLVM最重要的地方:支持多种语言或多种硬件架构。使用通用代码表示形式:IR(用来在编译器中表示代码的形式)。
- LLVM可以为任何编程语言独立编写前端,也可以为任何硬件架构独立编写后端。
- 所以LLVM不是一个简单的编译器,而是架构编译器,可以兼容所有前端和后端。

clang
- Clang是LLVM项目的一个子项目。基于LLVM架构的轻量级编辑器,诞生之初就是为了替代GCC,提供更快的编译速度。 他是负责编译C、C++、Objective-C语言的编译器,它属于整个LLVM架构中的编译器前端。
- 对于开发者而言,研究Clang可以给我们带来很多好处。
编译流程
通过命令可以打印源码的编译阶段
#import <stdio.h>
#define C 10
int add(int a, int b) {
return a + b + C;
}
int main(int argc, const char * argv[]) {
int d = add(1,2);
printf("%d",d);
return 0;
}
- 在终端中进入到main.m文件的路径,执行
clang -ccc-print-phases main.m
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
- 编译流程主要为下面7步
- 0: input, "main.m", objective-c:输入文件:找到源文件
- 1: preprocessor, {0}, objective-c-cpp-output:预处理:宏的展开,头文件的导入
- 2: compiler, {1}, ir:编译:词法、语法、语义分析,最终生成IR
- 3: backend, {2}, assembler ():汇编:LLVM通过一个个的Pass去优化,每个Pass做一些事,最后生成汇编代码
- 4: assembler, {3}, object:目标文件
- 5: linker, {4}, image:链接: 链接需要的动态库和静态库,生成可执行文件
- 6: bind-arch, "x86_64", {5}, image:架构可执行文件:通过不同架构,生成对应的可执行文件
预处理阶段
- 执行命令
clang -E main.m >> main2.m生成main2.m文件,查看main2.m文件
...
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, va_list);
# 408 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 2 3 4
# 2 "main.m" 2
int add(int a, int b) {
return a + b + 10;
}
int main(int argc, const char * argv[]) {
int d = add(1,2);
printf("%d",d);
return 0;
}
编译阶段
词法分析
- 词法分析
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

- 这里会把代码切成一个个的Token,比如小号括号,等于号还有字符串等。
语法分析
- 词法分析之后就是语法分析,它的任务验证语法是否正确。在词法分析的基础上,将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree,AST)。 语法分析程序判断源程序在结构上是否正确。

- 如果导入的头文件找不到,我们可以指定SDK
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
生成中间代码IR(Intermediate representation)
- 完成词法、语法分析后,就开始生成中间代码IR,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM的IR。
IR基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用数据
ret 返回
- 通过下面命令
clang -S -fobjc-arc -emit-llvm main.m生成.ll文本文件,查看IR代码:

IR优化
- LLVM的优化级别分为 -O0、 -O1、 -O2、 -O3、-Os(第一个字母是Optimization的O)。
- 我们可以在Xcode的Build Settings中搜索Optimization,可以看到优化级别。(Debug模式默认None [O0]无优化,Release模式默认Fastest,Smallest [Os]最快最小)

bitCode
- Xcode7之后,开启bitCode苹果会再进一步优化,生成.bc的中间代码。
clang -emit-llvm -c main.ll -o main.bc
生成汇编代码
clang -S -fobjc-arc main.ll -o mainll.s
clang -S -fobjc-arc main.bc -o mainbc.s
clang -Os -S -fobjc-arc main.m -o main.s
生成目标文件(机器代码)
- 生成汇编文件后,汇编器以汇编代码作为输入,将汇编代码转换为机器代码,输出目标文件(object file)
clang -fmodules -c main.s -o main.o
xcrun nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _add
000000000000000a (__TEXT,__text) external _main
- undefined: 表示当前文件暂时找不到符号。
- external:表示这个符号是外部可以访问的。(实现不在我这,在外部的某个地方)
生成可执行文件(链接)
- 通过链接器把编译产生的.o文件和.dylib、.a文件链接关联起来,生成真正的mach-o可执行文件
clang main.o -o main
main: Mach-O 64-bit executable x86_64
- 再查看main的符号
xcrun nm -nm main
xcrun nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f6d (__TEXT,__text) external _add
0000000100000f77 (__TEXT,__text) external _main
0000000100002008 (__DATA,__data) non-external __dyld_private
- 生成的main文件是一个executable可执行文件
- 虽然符号还有undefined,但是可执行文件中指定了该符号的来源库。文件在运行时,会从相应的库中取读取该符号(printf)的地址。
clang插件
- 我们通过实现一个clang插件来更好的理解LLVM和clang的工作流程
LLVM下载
- 由于网络原因,我们借助镜像下载LLVM的源码
- 下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
- 在LLVM的tools目录下下载Clang
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
- 在LLVM的projects目录下下载
compiler-rt libcxx libcxxabi
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
- Gitee上有已配置好关联库的源码,可直接下载: gitee.com/mirrors/LLV… 下载之后将部门文件安装上面的路径移动下
需要注意的一点,文件路径里面不能有中文、空格等特殊字符,会影响后续操作的成功
- 最终的文件路径

LLVM编译
- 最新的LLVM只支持cmake编译,需要使用Homebrew安装cmake。
安装cmake
- 查看brew列表,检查是否安装过cmake,如果有,就跳过此步骤
brew list
brew install cmake
编译llvm
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm
- 成功之后,可以看到生成的Xcode文件:

- 打开Xcode文件,可以选择手动编译,或者自动编译
- 手动编译,我们选择
clang和clangTooling

- 自动编译

使用ninjia编译
$ brew install ninja
- 在llvm同级目录下新建一个llvm_build目录,最终会在llvm_build目录下生成build.ninja。
- 在llvm同级目录下新建一个llvm_release目录,最终编译文件会在llvm_release文件夹路径下。
$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= /Users/xxx/xxx/LLVM/llvm_release
$ ninja
$ ninja install
创建插件
- 在/llvm/tools/clang/tools目录下新建插件
XQPlugin文件夹。

- 修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件,在后面新增add_clang_subdirectory(XQPlugin)。

- 在QTPlugin目录下新建一个名为CMakeLists.txt的文件和一个名为QTPlugin.cpp的文件,在CMakeLists.txt中写上
add_llvm_library( XQPlugin MODULE BUILDTREE_ONLY XQPlugin.cpp
)

- 目录文件创建完成之后,利用CMake重新生成一下Xcode项目。
$ cd llvm_xcode
$ cmake -G Xcode ../llvm
- 插件源代码在 Xcode 项目中的Loadable modules目录下可以找到,这样就可以直接在 Xcode 里编写插件代码。

编写插件代码
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
namespace XQPlugin {
class XQMatchCallback: public MatchFinder::MatchCallback{
private:
CompilerInstance &CI;
bool isUserSourceCode(const string fileName){
if (fileName.empty()) return false;
if (fileName.find("/Applications/Xcode.app/") == 0) return false;
return true;
}
bool isShouldUseCopy(const string typeStr){
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos) {
return true;
}
return false;
}
public:
XQMatchCallback(CompilerInstance &CI):CI(CI){}
void run(const MatchFinder::MatchResult &Result) {
const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if (propertyDecl && isUserSourceCode(fileName)) {
string typeStr = propertyDecl->getType().getAsString();
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐使用copy!!"))<<typeStr;
}
}
}
};
class XQConsumer: public ASTConsumer{
private:
MatchFinder matcher;
XQMatchCallback callback;
public:
XQConsumer(CompilerInstance &CI):callback(CI){
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
bool HandleTopLevelDecl(DeclGroupRef D) {
return true;
}
void HandleTranslationUnit(ASTContext &Ctx) {
matcher.matchAST(Ctx);
}
};
class XQASTAction:public PluginASTAction{
public:
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<XQConsumer>(new XQConsumer(CI));
}
};
}
static FrontendPluginRegistry::Add<XQPlugin::XQASTAction> XQ("XQPlugin","this is XQPlugin");
- 将插件添加编译,编译完成之后找到对应的
XQPlugin.dylib

- 测试插件
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.2.sdk -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名 -c 需要编译的源码路径(.m等)

Xcode集成插件
加载插件
- 打开需要加载插件的Xcode项目,在Build Settings栏目中的Other C Flags添加上如下内容:
-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang 插件名字(namespace 的名字,名字不对则无法使用插件)

设置编译器
- 由于Clang插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误,会出现如下图所示:

- 在Build Settings栏目中新增两项用户定义的设置

- 分别是CC和CXX,CC对应的是自己编译的clang的绝对路径,CXX对应的是自己编译的clang++的绝对路径。
- 继续编译,报错如下

- 接下来在Build Settings栏目中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO。
- 最后在新创建的Xcode项目中编译就会有如下警告了。说明你的插件成功导入并生效了。
