在上一篇中介绍了LLVM/Clang的编译器插件可以用来进行一些简单的编译器功能扩展,从这篇开始就一步步来实现这个编译器插件。本篇主要介绍了如何建立LLVM/Clang编译器插件的开发环境及开发目录配置,如何使用CMake编译系统组织起整个工程项目来,以及使用最基本的LLVM趟管理器遍历输出函数信息等。
环境配置
安装LLVM/Clang的开发包
在进行LLVM插件开发时,有两种模式可以选择,LLVM官方网站推荐的是下载整个LLVM源代码并在LLVM源代码目录中进行开发。这样的好处是可以利用LLVM提供的一整套编译工具及测试套件,但是却要与整个庞大的LLVM源码目录打交道。这里,我们选择独立构建,即 build out of source tree 的方式来进行构建。这里以 Archlinux 为开发环境,目前源里的 LLVM 版本为 7.0.1。
由于 Archlinux 的打包系统中不拆分 dev 包,如果是其它如 debian 系或 rpm 系的 Linux 发行版,还需要同时安装开发包,因为我们需要引用 LLVM 的头文件及它的 CMake 相关配置。
建立源码目录
在项目目录中建立以下目录及文件:
CMake 配置
编写 CMakeLists.txt,写入以下内容:
这是一个编译为共享库的 CMake 配置,使用 add_definitions 和 include(AddLLVM) 引入了 LLVM 的 CMake 配置,除此以外,都是典型的 CMake 基本配置,细节不再赘述。
插件源码
在 src/main.cpp 中编写以下内容:
首先我们在 22 行创建了一个自己的类叫做 DivisionCheckPass,这也就是我们这样模块的核心功能实现的地方。它继承于 LLVM 的 FunctionPass,在编译器的运行过程中,会对源代码、中间代码进行不止一次的扫描,每次扫描都会使得编译进程往前推进一步。而这每一次扫描,称为一“趟”,即一个 Pass。而这里的 FunctionPass 就是指这是专门针对函数进行分析的一个趟模块。
在 LLVM 中,有一个趟管理器即 PassManager。它管理了所有的趟模块,并依据不同趟模块之间的依赖关系、作用、及返回值等信息对所有的趟进行排列。例如一些针对特定平台机器码进行优化的趟就需要在比较后期的编译过程中再运行。在 52~54 行中,我们就把这个趟模块添加到了 PassManager 中,并指定它的加载方式为 EarlyAsPossible,即尽可能早的加载。
然后看到 36 行的构造函数中,所有的 LLVM 趟以一个 ID 来进行标识,以区分不同的趟。这里需要说明的是,一个趟模块从理论上来讲,应该是只有一个有意义的实例的,即在一次编译过程中,多次插入一个趟模块是没有什么意义的(但一个趟是有可能被执行多次的)。所以这里在区别不同的趟模块时,只使用这个 ID 变量的地址加以区分,并不区分同一个类的不同实例。
在把我们的 DivisionCheckPass 添加到 PassManager 中之后,由于我们是一个 FunctionPass,所以在编译过程中一旦遇到函数类型的语法子树,就会由 PassManager 调用我们的 runOnFunction 函数,交由我们模块进行处理。这个函数返回的布尔值代表我们是否修改了这个函数,假设在我们这个模块之前有一个优化模块对指令进行了优化,而我们的模块之后又修改了这个函数中的指令,那可能就破坏了之前的优化效果,或者是带来了新的优化机会,那么 LLVM 编译系统就会依照设定重复调用其他模块再次进行优化。现在,我们不对函数做修改,仅仅输出函数的名称,并返回 false。
编译
为了不污染源码目录,我们在项目目录中新建 build 目录进行构建:
编译完成后,在 build 目录下,我们的编译器插件 libdiv-check.so 就生成成功了,接下来使用系统中的 clang 编译器加载这个插件编译我们的测试代码:
可以看到,在编译过程中加载了我们的 DivisionCheckPass,并输出了二进制中包含的每个函数的名称,其中包括源码中的 dangerous_division_function 及 safe_division_function 函数和一些引入的函数及编译器添加生成的函数。由于 C++ 为了实现函数重载,所以它们的符号名称是经过了名字粉碎的。
下一篇,将进一步对中间代码、汇编代码进行分析,以确定我们该如何进行指令修改以实现除法保护的功能。