LLVM 初探

黑客技术

点击右侧关注,了解黑客的世界!

Java开发进阶

点击右侧关注,掌握进阶之路!

Python开发

点击右侧关注,探讨技术话题!

作者丨TWalker

来源丨知识小集(zsxjtip)

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 的优势:

• 良好的模块化

• 严格定义语义的中间语言 IR

• 更多请参考 https://www.kanxue.com/book-37-410.html

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. bitcode

./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. 常见错误

找不到头文件

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, 全局变量等

可以遍历 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 中:

• 注册到 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

• 注册到标准编译流程中,默认会执行 HelloPass,例如通过 clang 调用

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

移植到 LLVM 源码目录

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

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

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

通过 Xcode 调试 Pass

请参考 Xcode调试一个LLVM Pass

参考

• 了解 LLVM 是什么,基本架构模块,IR 是什么,pass 的作用

• 阅读 LLVM Overview  了解 LLVM 的模块划分以及各模块基本功能。
• 阅读 The Architecture of Open Source Applications 了解 LLVM 的架构

• 编译

• 阅读 get_started 了解如何编译 LLVM
• 阅读 llvm cmake 参数 了解编译时 cmake 参数的含义
• 阅读 CMake 简介 了解 CMake 语法

• 上手 pass,IR

• 根据 writing an LLVM pass 上手尝试,了解 pass 的使用流程
• ProgrammersManual 阅读开发手册了解编写 pass 可能涉及到的 API
• IR 手册 略读 IR 手册,了解 IR 的基本特征

• 其他参考资料

• LLVM 中文网
• Xcode调试一个LLVM Pass
• LeadroyaL's llvm 系列文章

• 日常查阅

• llvm cmake 参数
• IR 手册
• LLVM command guide llvm-as, llvm-dis, opt 等命令的使用说明

 推荐↓↓↓ 

????16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

万水千山总是情,点个 “在看” 行不行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值