LLVM 初探

1,856

0x1. LLVM 架构简介

经典的三段式设计

LLVM,GCC, JIT(Java, Python) 等编译器都遵循经典的三段式设计

  • 前端 (Frontend) - 词法分析,语法分析, 生成抽象语法树,生成中间语言 (例如 java 的字节码,llvm 的 IR,GCC 的 GIMPLE Tuples)

  • 优化器 (Optimizer) - 分析中间语言,避免多余的计算,提高性能;

  • 后端 (Backend) - 根据中间语言,生成对应的 CPU 架构指令 例如 X86,ARM;

通过这种设计,增加新的语言,只需要实现新的 Frontend,Optimizer 和 Backend 可以重用;同理新增新的 CPU 架构时,也只需要实现新的 Backend。

LLVM 的优势

LLVM,GCC,JIT 都采用三段式设计,LLVM 的优势在哪里 ?

GCC,JIT 存在的问题:

  • GCC 的问题

    • 古老,模块化不够 (GCC 是个整体,无法独立使用某块功能)。
  • JIT 的问题

    • 强制 JIT 编译,垃圾回收以及使用非常特殊的对象模型;在编译与该模型不完全匹配的语言(例如C)时,性能欠佳。(我也不懂 😊)

LLVM 的优势:

0x2: 编译 LLVM

  1. 前置要求

    cmake, python, 可以通过 homebrew 来安装

    详细要求参考 Getting Started with the LLVM System - Requirements

  2. 检出 LLVM 工程

    git clone --depth=1 https://github.com/llvm/llvm-project.git
    
  3. 编译 LLVM 和 Clang

    cd llvm-project
    mkdir build_with_ninja
    cd build_with_ninja
    cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;compiler-rt" -G Ninja ../llvm
    ninja
    
    • -DCMAKE_BUILD_TYPE=Release
      • 默认 Debug,设置为 Release 可以减少硬盘空间的占用
    • -DLLVM_ENABLE_ASSERTIONS=ON
      • Debug 下默认 YES,其他 NO
    • -DCMAKE_INSTALL_PREFIX=directory
      • 不指定,默认安装在 build_with_ninja 的 bin 目录下
    • -G 参数说明:
      • Ninja - 推荐,编译速度更快
        • 安装方式- brew install ninja
      • Unix Makefiles - 通过 make 来编译
      • Visual Studio,Xcode - 方便调试
  4. 测试是否成功

    cd bin
    ./clang --help
    

0x3. Clang 命令

  1. 首先创建一个名为 "hello.c" 的 C 文件

    #include <stdio.h>
    int main() {
        printf("hello world\n");
        return 0;	
    }
    
  2. 预编译

    clang hello.c -E
    
  3. 词法分析,导出 token

    clang -fsyntax-only -Xclang -dump-tokens hello.c
    
  4. 语法分析, 导出抽象语法树

    clang -fsyntax-only -Xclang -ast-dump hello.c
    
  5. 编译为可执行文件

    clang hello.c -o hello
    
  6. 编译为 bitcode

    clang -o3 -emit-llvm hello.c -c -o hello.bc	
    

    -emit-llvm 搭配 -S 或者 -c 选项可以生成 LLVM .ll 或者 .bc 文件,.ll, .bc 都是 LLVM IR 格式,它们的区别是 .ll 是可读的,而 .bc 不可读。

  7. 运行程序, 输出 hello world

  8. ./hello
    

    运行 bitcode

    lli hello.bc
    
  9. 使用 llvm-dis 查看 .bc 文件

    llvm-dis < hello.bc | less
    
  10. 将 LLVM 中间文件 (.ll 或者 .bc) 编译为汇编文件

    llc hello.ll -o hello.s	
    

    或者

    llc hello.bc -o hello.s
    

    也可以使用 clang

    clang hello.bc -S hello.s
    
  11. 生成可执行文件

    clang hello.s -o hello
    
  12. 常见错误

    1. 找不到头文件
    fatal error: 'stdio.h' file not found
    #include <stdio.h>
             ^~~~~~~~~
    1 error generated.
    

    通过 -I 指定头文件路径

    clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/include ...
    

0x4. 上手 Pass

pass 是什么?

一系列优化 和 转换 LLVM IR 的 C++ 代码, 主要流程如下:

几个概念:

Module, Function, BasicBlock, Instruction, Value

  • Module: 包含 Function, 全局变量等

    可以遍历 Module 得到 Function

    for (Module::iterator iter = M.begin(); iter != M.end(); iter++) {
      Function *F = &(*iter);
    
  • Function:包含若干 BasicBlock

  • BasicBlock:包含若干 Instruction

  • Instruction: 指令,包含操作,Value

    add 是操作,其他为 Value;

    %1 = add i32 %a, %b
    
  • Value:大部分对象都可以看成 Value,包括常量,参数,指令,函数

在 LLVM 源码目录外开发 Pass

为方便表述,简称 “外部 pass”,外部 pass 比较灵活,不用修改 LLVM 源码配置;

尝试制作一个打印函数名的简单 Pass,步骤如下:

  1. 新建 outpasses

    # llvm-project 同级目录
    cd ../..
    mkdir outpasses
    
  2. 创建如下目录

    outpasses/
        |
        CMakeLists.txt
        PrintFunctions/
            |
            CMakeLists.txt
            PrintFunctions.cpp
            ...
    
  3. outpasses / CMakeLists.txt 内容如下:

    配置 LLVM_DIR - LLVM_DIR 为 LLVM 编译时的安装目录

    cmake_minimum_required(VERSION 3.4)
    
    set(ENV{LLVM_DIR} ~/llvm/llvm-project/build_with_ninja/lib/cmake/llvm)
    
    find_package(LLVM REQUIRED CONFIG)
    add_definitions(${LLVM_DEFINITIONS})
    include_directories(${LLVM_INCLUDE_DIRS})
    link_directories(${LLVM_LIBRARY_DIRS})
    
    # add c++ 14 to solve "error: unknown type name 'constexpr'"
    add_compile_options(-std=c++14)
    add_subdirectory(PrintFunctions)  # Use your pass name here.
    
  4. PrintFunctions/CMakeLists.txt 内容如下:

    add_library(PrintFunctions MODULE
      # List your source files here.
      printFunctions.cpp
    )
    
    # LLVM is (typically) built with no C++ RTTI. We need to match that;
    # otherwise, we'll get linker errors about missing RTTI data.
    set_target_properties(PrintFunctions PROPERTIES
      COMPILE_FLAGS "-fno-rtti"
    )
    
    # Get proper shared-library behavior (where symbols are not necessarily
    # resolved when the shared library is linked) on OS X.
    if(APPLE)
      set_target_properties(PrintFunctions PROPERTIES
        LINK_FLAGS "-undefined dynamic_lookup"
      )
    endif(APPLE)
    
  5. PrintFunctions/printFunctions.cpp 内容如下:

    #include "llvm/Pass.h"
    #include "llvm/IR/Function.h"
    #include "llvm/Support/raw_ostream.h"
    #include "llvm/IR/LegacyPassManager.h"
    #include "llvm/Transforms/IPO/PassManagerBuilder.h"
    using namespace llvm;
    
    namespace {
        struct Hello : public FunctionPass {
            static char ID;
            Hello() : FunctionPass(ID) {}
    
            virtual bool runOnFunction(Function &F) {
                errs() << "I saw a function called " << F.getName() << "!\n";
                return false;
            }
        };
    }
    
    // Automatically enable the pass.
    char Hello::ID = 0;
    static RegisterPass<Hello> X("hello", "Hello World Pass",
                                false /* Only looks at CFG */,
                                false /* Analysis Pass */);
    static RegisterStandardPasses Y(
        PassManagerBuilder::EP_EarlyAsPossible,
        [](const PassManagerBuilder &Builder,
            legacy::PassManagerBase &PM) { PM.add(new Hello()); });
    
  6. 编译 pass 得到 libPrintFunctions.so

    cd outpasses
    cmake .
    make
    

    libPrintFunctions.so 在 PrintFunctions 文件夹中

  7. 通过 clang 加载 pass

    cd llvm-project
    ./build_with_ninja/bin/clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/include  -Xclang -load -Xclang ../outpasses/PrintFunctions/libPrintFunctions.so ../llvmtest/test.c
    

    test.c 内容如下:

    #include <stdio.h>
    int main() {
        int a = 0;
        if ( a = 1) {
            printf("123");
        }
        return 0;
    }
    

    输出:

    I saw a function called main!
    
  8. 也可以通过 opt 来加载 pass

    生成 bitcode

    ./build_with_ninja/bin/clang  -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/include -o3 -emit-llvm test.c -c -o test.bc	
    

    opt -hello 开启 pass

    ./build_with_ninja/bin/opt -load ../outpasses/PrintFunctions/libPrintFunctions.so -hello < ../llvmtest/test.bc
    

Pass 和 PassManager 的关系

PassManager 管理 Pass,解决多个 Pass 依赖,传值等问题,例如 PassA 需要等待 PassB 执行完成后才执行;

Pass 需要注册到 PassManager 中:

  1. 注册到 opt 中, hello 命令行可选参数,“Hello World Pass” 帮助说明

    # 注册到 opt 中,
    static RegisterPass<Hello> X("hello", "Hello World Pass",
                                 false /* Only looks at CFG */,
                                 false /* Analysis Pass */);
    

    通过 opt -hello 来使用 HelloPass

     opt -load lib/LLVMHello.so -hello < hello.bc > /dev/null
    
  2. 注册到标准编译流程中,默认会执行 HelloPass,例如通过 clang 调用

    static llvm::RegisterStandardPasses Y(
        llvm::PassManagerBuilder::EP_EarlyAsPossible,
        [](const llvm::PassManagerBuilder &Builder,
           llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });
    

移植到 LLVM 源码目录

  1. 拷贝 PrintFunctions 目录到 LLVM 源码目录的 lib/Transform 目录下

  2. 在 lib/Transform/CMakeLists.txt 中新增 add_subdirectory(PrintFunctions)

  3. 重新编译,可以得到 libPrintFunctions.so

通过 Xcode 调试 Pass

请参考 Xcode调试一个LLVM Pass

0x5. 参考