乐于分享
好东西不私藏

LLVM二次开发(2)——源码文件类型、工程建议、调试方法

LLVM二次开发(2)——源码文件类型、工程建议、调试方法

源码文件类型

和其他大型代码工程一样,LLVM代码工程整体包含多种类型的代码文件,包括:

  • C++代码文件C++源码.cpp.h源文件是LLVM工具链的实现主体。LLVMclang目录下的C++源码都在数百万行量级。

  • CMake代码文件CMakeLLVM的元构建系统。CMake的构建脚本CMakeLists.txtxx.cmake分布在LLVM代码树的各级目录下。CMake元构建脚本支持通过-G选项选择生成Unix MakefileNinjaVisual Studio等直接构建脚本。

  • TableGen代码文件LLVM自带的TableGen语言,用于表达LLVM和其他子项目中适合模板化或表格化的数据或代码。TableGen的源代码文件后缀是.td。典型的例子如前端的AST语法节点和后端中的机器指令,一般都用TableGen语言定义。

  • def源文件对于能够归在同一范畴,但在不同场合下用法不同的多个元素,LLVM常将它们以宏的形式罗列在.def文件中,供其他.cpp.h源文件以自己内部定义的宏来引用。例如,前端的TokenKinds.def罗列了C系列语言的词素,而后端x86_64.defAArch64.defSparc.def这些架构特定def文件,则罗列了该架构后端链接衔接涉及的重定位类型。

  • inc中间代码文件LLVM构建过程中,TableGen会将.td编译为符合C++语法的.inc文件,供.cpp.h包含引用。对TableGen表达感到抽象的读者,可以查看这些.inc中间文件来确切知道.td源码的C++表达效果。

在二次开发的过程中,开发者应当逐步熟悉各类源码文件在LLVM整体框架和具体流程中的作用。上面几种文件中,.td.def文件中枚举的元素或表项,往往在编译的某个子过程占据重要角色。所以,在对某个工具的代码子目录作分析时,可以留意先搜索该目录是否有.td.def文件,还可以进一步搜索.td.def中定义的元素在哪些.cpp.h中得以使用。反之,在对LLVMC++代码进行分析时,可能会遇到某些符号无法从.cpp.h中直接找到定义源的情况,这时可以试试扩大搜索范围至代码树中的.td.def.inc等代码文件,或者在代码树根目录下全局搜索。

LLVM IRTableGen源码的vim语法高亮

LLVM的日常分析和开发过程,需要频繁地TableGen后缀.tdLLVM IR(后缀.ll代码进行分析和修改。在Linux环境下,常用的vim编辑器虽然默认支持CC++Java等常见编程语言的语法高亮,但对TableGenIR语言的高亮支持并内置。不过LLVM代码中已经包含了相关语法高亮插件,位于llvm/utils/vim目录。用户只需按照以下步骤操作,将这些插件复制并安装到自己的~/.vim目录下即可

$ cd llvm/utils/vim$ cp ftdetect ftplugin indent syntax ~/.vim/ -r

安装完插件后,重新打开一个LLVM IR源码文件,看到语法高亮已经生效,如19所示。

19LLVM IR代码高亮效果

工程建议

无论是针对编译器这一特定业务模型,还是基于其他的大型代码工程作二次开发,一些通用的工程经验都是可参考的

  • 用好工程级的代码分析工具。读者根据自己的喜好,可以使用Source Insightvim+cscope/ctags或是VSCode作代码分析C++的建模粒度是class,那么对于本次开发涉及的关键class间的继承、使用、组合等关系的分析对软件开发是有价值的。

  • 使用gitSCMSource Code Management工具SCM工具带来的好处包括但不限于代码历史版本追溯、差异比对、多人协作、降低人为错误影响。

  • 用好LLVM自带的集成测试工具llvm-lit,尽可能实现测试集对增量代码功能的覆盖,并确保测试结果的快速获得。原则上,对于每次增量提交的代码,开发者不能假定它不会影响本模块乃至其他模块的功能和性能。

对于增量较大、参与人员较多或是长周期的代码开发,可以进一步使用CI持续集成工具,或是实践CI的理念来确保软件工程的架构健康程度和输出质量保持在可接受水平。显式的使用CI持续集成工具,或是隐式的实践CI理念,其效果是可以相近的,因为工具和方法论之间存在诸多映射。

调试方法

如果是基于LLVM SDK工具链做适配开发,就需要足够的调试手段来获得工具链运行的中间信息。除了加打印、gdb这种基本方式外,LLVM还内置提供了一些为特定编译流程定制的调试信息,例如前端AST语法树的打印、后端SelectionDAG指令选择图的打印。以下一一介绍。

gdb方法

gdb是应对复杂运行问题时有效的调试工具。LLVM工具链也可以用gdb调试,但需要在工具链构建阶段配置为Debug模式:

$ cmake -DCMAKE_BUILD_TYPE=Debug -G "Unix Makefiles" ../llvm$ make

具体调试起来,工具链的调试跟其他程序并无二致,但由于Debug模式生成的工具往往体积较大,gdb加载时间长,例如clangllc的加载时间能达到1分钟。下面是gdb抓取llc后端编译函数栈的操作和过程打印:

$ gdb llcGNU gdb (Ubuntu 9.2-0ubuntu1~20.04.19.2Copyright (C) 2020 Free Software Foundation, Inc.. . .For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from llc...(gdb) b SparcDAGToDAGISel::SelectBreakpoint 1 at 0x3145458: file llvm/lib/Target/Sparc/SparcISelDAGToDAG.cpp, line 321.(gdb) r -mtriple=sparcv9 --filetype=obj func1.llStarting program: build/bin/llc -mtriple=sparcv9 --filetype=obj func1.ll[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Breakpoint 1, SparcDAGToDAGISel::Select () at llvm/lib/Target/Sparc/SparcISelDAGToDAG.cpp:321321void SparcDAGToDAGISel::Select(SDNode *N) {(gdb) bt#0 SparcDAGToDAGISel::Select() at llvm/lib/Target/Sparc/SparcISelDAGToDAG.cpp#1 llvm::SelectionDAGISel::DoInstructionSelection() at llvm/lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp#2 llvm::SelectionDAGISel::CodeGenAndEmitDAG() at llvm/lib/CodeGen/SelectionDAG/SelectionDAGISel.cpp. . .

这里gdb的调试打印中作了适当简化,去除了一些次要信息。

STATISTICS统计接口

LLVM或其他大型软件的开发中,有一类调试需求,是需要针对编译过程中的各个特定事件,统计各个事件发生的次数。例如在中端阶段执行了某种优化的次数,或是在链接阶段获得某类输入section的个数。LLVM为这类统计需求专门设计了STATISTIC统计机制,以便于在代码中为各类事件作计数。在工具执行时加上-stats选项可以把运行中非0次数事件打印出来,看一个中端优化opt的例子:

$ opt -stats -O2 -S func1.ll -o func1.ll===--------------------------------------------------------------------===... Statistics Collected ...===--------------------------------------------------------------------===2 globalsmodref-aa - Number of functions that do not access memory2 globalsmodref-aa - Number of functions that only read memory8 instcombine - Number of instruction combining iterations performed1 reassociate - Number of insts reassociated

打印出的4行统计信息,分别是访问内存的函数的个数、只对内存作读操作的次数、指令合并迭代的执行次数、指令结合调整的次数。

在后端中,llc加上-stats选项可以打印出指令选择、指令发射、寄存器分配、指令打印等各阶段的统计信息:

$ llc -stats -mtriple=sparcv9 --filetype=obj func1.ll===--------------------------------------------------------------------===... Statistics Collected ...===--------------------------------------------------------------------===4 asm-printer       - Number of machine instrs printed2 assembler         - Number of emitted assembler fragments - align4 assembler         - Number of emitted assembler fragments - data. . .

LLVM对这个STATISTIC机制做的很完善,开发者如果要增加某项统计信息,在代码中用STATISTIC宏声明例化这个统计项,然后在代码中就可以适时修改更新这项统计数据:

// llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp#define DEBUG_TYPE "asm-printer"...STATISTIC(EmittedInsts, "Number of machine instrs printed");...EmittedInsts += NumInstsInFunction;

STATISTIC宏传入的第二个参数“Number of machine instrs printed”,是命令行运行时打印出该统计项的文字说明。