LLVM

460 阅读9分钟

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可以给我们带来很多好处。

编译流程

通过命令可以打印源码的编译阶段

  • 新建一个main.m文件,里面写上下面的代码
#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

生成汇编代码

  • 我们通过.bc或.ll代码生成汇编代码
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
  • 通过nm命令,查看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 // 将目标文件转成可执行文件
  • 查看文件file 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
    
    • 在clang的tools下安装extra工具
    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安装:
brew install cmake

编译llvm

  • cmake编译成Xcode项目
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文件,可以选择手动编译,或者自动编译
  • 手动编译,我们选择clangclangTooling
  • 自动编译

使用ninjia编译

  • 安装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;
            //非xcode中的源码都认为是用户的
            if (fileName.find("/Applications/Xcode.app/") == 0) return false;
            return true;
        }
        
        //判断是否应该用copy修饰
        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) {
            //通过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();
                //判断应该使用copy但是没有使用copy
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
//                    cout<<typeStr<<"应该使用;copy修饰!但是你没有!"<<endl;
                    //诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    //Report 报告
                    diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐使用copy!!"))<<typeStr;
                }
            }
        }
    };
    
    //自定义的XQConsumer
    class XQConsumer: public ASTConsumer{
    private:
        //AST节点的查找过滤器!
        MatchFinder matcher;
        XQMatchCallback callback;
    public:
        XQConsumer(CompilerInstance &CI):callback(CI){
            //添加一个MatchFinder去匹配objcPropertyDecl节点
            //回调在XQMatchCallback里面run方法!
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        //解析完一个顶级的声明就回调一次!
        bool HandleTopLevelDecl(DeclGroupRef D) {
//            cout<<"正在解析...."<<endl;
            return true;
        }
        //整个文件都解析完成的回调!
        void HandleTranslationUnit(ASTContext &Ctx) {
//            cout<<"文件解析完毕!"<<endl;
            matcher.matchAST(Ctx);
        }
    };
    
    //继承PluginASTAction 实现我们自定义的Action
    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项目中编译就会有如下警告了。说明你的插件成功导入并生效了。