美文网首页
浅谈LLVM

浅谈LLVM

作者: 住册新账号 | 来源:发表于2021-06-24 20:57 被阅读0次

    何为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架构

    传统编译器架构.jpg
    LLVM架构.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编译原理和使用

    相关文章

      网友评论

          本文标题:浅谈LLVM

          本文链接:https://www.haomeiwen.com/subject/nzrzeltx.html