LLVM 编译流程&clang插件开发

1,133 阅读8分钟

本文主要是理解LLVM编译流程以及clang插件的开发。

LLVM

LLVM是架构编译器的框架系统,以C++编写而成,用于优化任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)。对开发者保持开放,并兼容已有脚本。

传统编译器设计

源码 Source Code + 前端 Frontend + 优化器 Optimizer + 后端 Backend(代码生成器 CodeGenerator)+ 机器码 Machine Code,如下图所示

image.png 主要分为三部分:

1. 编译器前端(Frontend)

编译器前端的任务是解析源代码,它会进行词法分析语法分析语义分析,检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree,AST),LLVM的前端还会生成中间代码(intermediate representation, IR).

2. 优化器(Optimizer)

优化器负责进行各种优化,改善代码的运行时间,例如消除冗余计算。

3. 后端(Backend)/代码生成器(CodeGenerator)

代码映射到目标指令集。生成指定平台机器语言,并且进行机器相关的代码优化

ios的编译器架构

Objective C/C/C++ 使用的编译器前端是Clang,Swift是Swift,后端都是LLVM。

image.png

LLVM 的设计

LLVM最重要的地方就是支持多种源语言或多种硬件架构,通过通用的代码表示形式IR,类似于桥接模式,实现了前后端分离

image.png

Clang

LLVM项目中的一个子项目,负责C,C++,Object-C语言的编译器,在整个LLVM架构中,属于编译器前端。通过clang的学习,可以更好的应用到项目中,比如通过Clang插件,不仅能够检查代码规范,还能够进行无用代码分析自动埋点打桩线下测试分析方法名混淆等。

clang插件本身的编写和使用并不复杂,关键是如何更好的应用到工作中,

编译流程

我们通过一个简单例子来看下一下完整过程。 新建一个main.m 文件

#import <Foundation/Foundation.h>
#define DEFINEEight 8
int main(){
    @autoreleasepool {
        int eight = DEFINEEight;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:”starming”];
        int rank = eight + six;
        NSLog(@“%@ rank %d”, site, rank);
    }
    return 0;
}

终端输入命令: 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

image.png 主要有六个阶段

  1. 输入文件,找到源文件
  2. 预处理:进行宏的替换、头文件的导入,条件编译
  3. 编译:进行词法分析、语法分析、检测语法是否正确,生成IR
  4. 后端:LLVM会通过一个一个Pass去优化,最终生成汇编代码
  5. 汇编:汇编代码生成目标文件
  6. 链接链接动态库和静态库,生成可执行文件
  7. 绑定:通过不同的架构生成对应的可执行文件
预处理阶段

执行

clang -E main.m

执行完毕后就可以看到头文件的导入和宏的替换。 image.png

编译阶段

编译阶段主要是进行词法、语法等的分析和检查,然后生成中间代码IR

1. 词法分析

这里会把代码切成一个个token,比如大小括号、等于号还有字符串等。 可以通过如下命令查看。

  clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

如果头文件找不到,指定sdk.

clang -isysroot (自己SDK路径) -fmodules -fsyntax-only -Xclang -dump-tokens main.m

 clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -fmodules -fsyntax-only -Xclang -dump-tokens main.m
2. 语法分析

词法分析完成后就是语法分析,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等,然后将所有节点组成抽象语法树(Abstract Syntax Tree,AST),语法分析程序判断程序在结构上是否正确。 可以通过如下命令查看。

  clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

生成抽象语法树

image.png

3. 生成中间代码IR(intermediate representation)

完成以上步骤后,就开始生成中间代码IR了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR, 通过下面命令可以生成.ll的文本文件,查看IR代码。

  clang -S -fobjc-arc -emit-llvm main.m

image.png

OC代码在这一步会进行runtime桥接,:property合成、ARC处理等 IR 的基本语法

@ %局部符号 未命名

  • @ 全局符号
  • % 局部符号
  • alloca 开辟空间
  • align 内存对齐
  • i32 32个bit 4个字节
  • store 写入内存
  • load 读取数据
  • call 调用函数
  • ret 返回 IR的优化 LLVM优化级别分别是-O0 -O1 -O2 -O3 -Os
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

IR文件在OC中是可以进行优化的,一般设置是在target - Build Setting - Optimization Level(优化器等级)中设置。LLVM的优化级别分别是-O0 -O1 -O2 -O3 -Os(第一个是大写英文字母O),下面是带优化的生成中间代码IR的命令 bitCode

  • xcode7以后开启bitcode,苹果会做进一步优化,生成.bc的中间代码,我们通过优化后的IR代码生成.bc代码
clang -emit-llvm -c main.ll -o main.bc

生成汇编代码 我们通过最终的.bc或者.ll代码生成汇编代码

 clang -S -fobjc-arc main.bc -o main.s clang -S -fobjc-arc main.ll -o main.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

以下是main.o中的符号,其文件格式为 目标文件

image.png

  • undefined表示在当前文件暂时找不到符号
  • external表示这个符号是外部可以访问
链接

链接主要是链接需要的动态库和静态库,生成可执行文件,其中

  • 静态库会和可执行文件合并
  • 动态库是独立的
clang main.o -o main
复制代码

查看链接之后的符号

$xcrun nm -nm main
复制代码
绑定

绑定主要是通过不同的架构,生成对应的mach-o格式可执行文件

LLVM 编译

我已经整理好了对应的下载通过xcode脚本

#!/bin/bash
LLVMPath=`pwd`
# 1. 下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git
# 2. 下载Clang
cd llvm/tools/
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
# 3. 下载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
# 4. 安装extra工具
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
# 5. 安装cmake
if cmake  >/dev/null 2>&1
then
    echo "cmake已经安装"
else
    echo "cmake未安装"
    echo "cmake执行安装"
    brew install cmake >> /dev/null
    if test $? -eq
    then
        echo "安装cmake成功"
    else
        echo "安装cmake失败"
    fi
fi

# 6. 通过xcode编译
echo "通过xcode编译"
cd $LLVMPath
mkdir llvm_build
cd llvm_build
cmake -G Xcode ../llvm

运行成功之后就会进入llvm_build,就是我们通过xcode方式编译好的工程。

Clang插件

创建CLPlugin插件 在这个路径下创建一个CLPlugin文件夹, image.png 创建一个CLPlugin.cpp 文件和 CMakeLists.txt文件 CMakeList.txt文件添加

add_llvm_library( CLPlugin MODULE BUILDTREE_ONLY CLPlugin.cpp) image.png 进入llvm_build路径下重新编译一下 cmake -G Xcode ../llvm

编写插件代码

CLPlugin目录下的CLPlugin.cpp文件中,加入以下代码

#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 CLPlugin {
    class CLMatchCallback: 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:
        //3.、自定义回调类,继承自MatchCallback扫描完毕的回调函数
        CLMatchCallback(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();
                //拿到节点的描述信息
                ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                
                // 判断是否应该使用copy,但是没有使用copy
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {//应该使用copy但是没有使用Copy
                    //诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    //Report 报告
                    diag.Report(propertyDecl->getLocation(),diag.getCustomDiagID(DiagnosticsEngine::Error, "这个地方应该用Copy"));
//                    cout<<typeStr<<"应该使用copy修饰但是没有用!发出警告!!"<<endl;
                }
      
            }
        }
    };
    //2. 自定义的CLConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器
    class CLConsumer:public ASTConsumer{
    private:
        //MatchFinder  AST 节点的过滤器
        MatchFinder matcher;
        CLMatchCallback callback;
    public:
        CLConsumer(CompilerInstance &CI):callback(CI){
            //添加一个MatchFinder去匹配ObjCPropertyDecl节点
            //回调!
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        //解析完毕一个顶级的声明就回调一次
       bool HandleTopLevelDecl(DeclGroupRef D){
//            cout<<"正在解析...."<<endl;
            return true;
        }
        
        //当整个文件都解析完成后回调!!
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完毕!!"<<endl;
            matcher.matchAST(Ctx);
        }
        
    };

    //1. 定义一个类  继承PluginASTAction,实现我们自定义的Action,自定义AST语法书树行为
    class CLASTAction:public PluginASTAction{
    public:
        bool ParseArgs(const CompilerInstance &CI, const vector<string> &arg) {
            return  true;
        } 
        std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
            return unique_ptr<CLConsumer> (new CLConsumer(CI));
        }
    };
}
//4. 注册插件!
static FrontendPluginRegistry::Add<CLPlugin::CLASTAction> X("CLPlugin","this is the description");

简单汇总下编写过程:

  • 第一步,编写 PluginASTAction 代码处理入口参数。
  • 第二步,通过 ASTConsumer访问所有 AST 节点,获取想要的内容。
  • 第三步,编写 MatchCallback回调函数。
  • 第四步,注册 Clang 插件,提供外部使用。

测试一下 编译成功后生成clang文件路径,CLPlugin路径在lib下面 image.png

//命令格式
自己编译的clang文件路径  -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.1.sdk/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径

修改为自己sdk路径 image.png

Xcode集成插件

使用 Clang 插件可以通过 -load 命令行选项加载包含插件注册表的动态库

-load 命令行会加载已经注册了的所有 Clang 插件。使用 -plugin 选项选择要运行的 Clang 插件。Clang 插件的其他参数通过 -plugin-arg-来传递。

cc1 进程类似一种预处理,这种预处理会发生在编译之前。cc1 和 Clang driver 是两个单独的实体,cc1 负责前端预处理,Clang driver 则主要负责管理编译任务调度,每个编译任务都会接受 cc1 前端预处理的参数,然后进行调整。

有两个方法可以让 -load 和 -plugin 等选项到 Clang 的 cc1 进程中:

一种是,直接使用 -cc1 选项,缺点是要在命令行上指定完整的系统路径配置;

另一种是,使用 -Xclang 来为 cc1 进程添加这些选项。-Xclang 参数只运行预处理器,直接将后面参数传递给 cc1 进程,而不影响 clang driver 的工作。

image.png

  • Build Settings栏目中新增两项用户定义的设置,分别是CCCXX
  • CC 对应的是自己编译的clang的绝对路径
  • CXX 对应的是自己编译的clang++的绝对路径
  • 接下来在Build Settings中搜索index,将Enable Index-Wihle-Building FunctionalityDefault改为NO image.png 最后重新编译项目,我们的插件就开始代码检查了。

image.png