ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从源码构建到手写Pass:LLVM编译器基础设施实践指南

从源码构建到手写Pass:LLVM编译器基础设施实践指南 第一次把 llvm-project 这份仓库拉到本地时我盯着目录结构愣了几秒。这分明就是一个普普通通的 C 工程除了 llvm、clang、lld、mlir 这些顶层目录实在看不出跟编译器三个字有什么直接关系。但正是这份看起来普通的源码撑起了无数工具链和产品。它不是一个编译器而是编译器的基础设施是支撑起 Clang、Swift、Rust 后端、Android NDK 甚至一堆芯片厂商内部工具链的底层平台。我发现很多人对 LLVM 的认知停在一个开源的 C 语言编译器或者Clang 的另一个名字。但当你真正进入 llvm-project才会意识到它真正的核心是一套中间表示IR前端负责把源语言翻译成 IR优化器负责在 IR 上做变换后端负责把 IR 变成目标机器码。这一层抽象让每种语言配每个芯片的传统编译方式彻底变成了语言前端对接 IR、芯片后端对接 IR的组合游戏。这篇文章写给那些想真正动手研究 LLVM 的人。不管你是被编译课折磨的学生还是想在公司内部工具链上做文章的开发亦或是纯粹好奇编译器到底是怎么工作的我都建议你亲自把 llvm-project 跑起来亲手拉一个分支、改一份代码、编译一遍工具链。下面这些内容全部来自我实际折腾这个项目的过程有命令、有源码、有踩坑尽量做到能复制、能跑通、能让你少走弯路。1. 为什么说 LLVM 是编译器的编译器先说结论LLVM 最了不起的设计不是在优化算法上有多么领先而是它对编译过程这个老问题的结构性重构。传统编译器比如 GCC虽然不断演进但总体还是前端—中端—后端耦合在一个二进制工程里。你要为新的语言写前端得嵌入到 GCC 的框架里要为新的指令集写后端也得等整个项目愿意支持。这种 All-in-One 的模型发展到今天生成端的数量和语言的数量一旦爆炸开发和维护成本会直线上升。LLVM 的思路其实非常朴素把编译过程拆成三个独立的、干净的阶段。前端负责词法分析、语法分析、语义分析生成平台无关的 LLVM IR。中端基于 LLVM IR 做各种优化例如常量折叠、公共子表达式消除、循环不变量外提等同样是平台无关的。后端把优化后的 LLVM IR 转换成目标机器的指令选择、寄存器分配、指令调度最终落到具体架构的汇编或机器码。这套抽象的爆发点在于只要你的语言能编译成 LLVM IR你就能自动获得 X86、ARM、RISC-V 等所有 LLVM 已支持后端的输出能力反过来芯片厂商只要把后端对接好就能让所有 LLVM 所支持的语言跑在自己的芯片上。举几个具体的例子你就明白这项架构有多香了。Rust 语言早期后端直接用了 LLVMrustc 负责让 Rust 变得可编译LLVM 负责让它跑得飞快Swift 不仅编译器前端跑在 LLVM 之上它的 SILSwift Intermediate Language还跟 LLVM 的优化器有深度配合很多芯片创业公司在流片前第一时间要做的就是找编译器团队把 LLVM 的后端调通因为整个嵌入式生态都建立在 LLVM 之上。所以当我第一次把 llvm-project 当作一个源码阅读项目去对待时我最大的感受是这个项目本质上是一套编译器领域所有核心问题的参考实现。它既是一个能用的工具也是一个极佳的教学样本。它的代码库庞大到让你头皮发麻但它的架构清晰到几乎为所有想理解现代编译技术的开发者画好了一张地图。2. 本地构建 llvm-project先搞懂 CMake 配置再动手很多人拿到这个仓库的第一反应是直接用make开始编译然后等了一个小时之后后悔。实际上构建 LLVM 是一个需要提前规划的任务你的磁盘、内存、构建工具的选择都会直接影响后续的开发体验。我给出的方案不一定是最短的路径但一定是最稳妥的。2.1 获取源码前的机器评估LLVM 的构建对硬件是有真实需求的。我的建议是内存至少 16GB磁盘至少预留 100GB 可用空间CPU 核数越多越好。如果你只是构建 X86 单目标并只构建核心工具clang、opt、llc在 Release 模式下一台 8 核的机器大概需要 20~40 分钟。如果你想开 Debug 模式并把所有 target 都打开那时间会膨胀到数小时并且磁盘占用可能超过 100GB。这不是开玩笑我自己第一次构建时因为没规划好磁盘空间连续两次被撑爆分区最后不得不用du -sh *逐个目录找体积元凶。其中一个典型的元凶就是每个工具目录下的CMakeFilesDebug 静态库文件极大动辄几百 MB 到几个 GB。所以先检查你的磁盘和内存再开始。如果你用的是 WSL 环境请注意不要在/mnt/c这种跨文件系统的路径下进行构建等磁盘空间是小事那些文件在 Windows 分区上会产生大量小文件读写性能低到让人怀疑机器坏了。把整个 llvm-project 放在 Linux 文件系统里放在/home或者/opt下远离/mnt目录。2.2 获取源码我通常只做浅克隆来节省时间因为 llvm-project 的历史提交太多了全量克隆会花上不少时间。浅克隆的命令如下git clone --depth 1 https://github.com/llvm/llvm-project.git如果你需要开发特定分支比如release/19.x可以加--branch参数。浅克隆之后你仍然可以获得一个可正常构建、可修改、可提交 PR 的完整工作区。对于日常学习和开发来说这已经足够了。2.3 构建工具链的准备LLVM 项目的构建系统是 CMake Ninja。Ninja 是必选make不是不能用但在增量构建速度、并行度和错误信息可读性上Ninja 真的强太多了。安装命令各个系统不一样如果你是 Debian/Ubuntu可以这样装sudo apt update sudo apt install cmake ninja-build gcc g zlib1g-dev libtinfo-dev如果你的系统自带 CMake 版本过低建议手动装一个新版 CMake因为 LLVM 每个版本对 CMake 的版本要求都在逐步抬高。你可以在 cmake.org 下载预编译包解压后用也可以使用pip install cmake来安装更新的版本。这一点上不要偷懒版本过旧的话 CMake 会直接报错不支持。2.4 第一次配置的推荐命令项目源码下载好之后标准做法是建一个 build 目录。LLVM 官方推荐源码内和源码外的构建方式但无论如何都要构建到单独的 build 目录里。如果你把构建产物直接写在源码目录中后续git status会乱成一锅粥而且改了源码后构建和源码混在一起非常难排查问题。我建议的第一次配置命令如下cd llvm-project mkdir build cd build cmake -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_ENABLE_ASSERTIONSON \ ../llvm这条命令里每个选项都有它的理由-DCMAKE_BUILD_TYPERelease优化编译速度并缩减产物体积。除非你要调试编译器本身否则不要用 Debug那是给自己找罪受。但要提醒的是Release 模式下很多调试符号会被裁掉因此后续你如果想用 gdb 去追踪某个 IR 优化 pass 的内部变量会不太方便。折中方案是用RelWithDebInfo它在 Release 优化基础上保留-g调试信息。我的开发习惯就是RelWithDebInfo LLVM_ENABLE_ASSERTIONSON。-DLLVM_ENABLE_PROJECTSclang;lld指定要构建的顶层项目。llvm-project 的仓库里包含 clang、clang-tools-extra、lld、lldb、compiler-rt、mlir、flang、polly 等一大串项目全量打开会让构建时间成倍增加。如果你只是学习 IR 和优化clang 和 lld 基本够用。-DLLVM_TARGETS_TO_BUILDX86指定要生成哪些后端 target。默认情况下 LLVM 会构建所有能支持的 target 集合包括 X86、ARM、AArch64、RISC-V、PowerPC、WebAssembly 等。这样做的结果就是编译时间爆炸、生成的工具体积巨大。如果你只跑在 x86 机器上只构建 X86 目标就够了后期需要交叉编译时再重新配置。-DLLVM_ENABLE_ASSERTIONSON开启断言。LLVM 内部有大量assert用来检查 IR 合法性和 pass 写法的正确性比如指令的 operand 类型是否匹配、DominatorTree 是否被正确更新等。开断言后很多手误会在运行时立刻暴露出来而不是生成错误代码或直接崩溃。强烈建议一直开着。配置完成后直接运行ninja如果你只想构建特定工具可以指定 target例如只需要编译器前端、优化器、汇编生成器ninja clang opt llc llvm-dis这是一个好习惯。全量ninja会顺便构建 clang-format、clang-tidy、llvm-objcopy、llvm-nm 等等一系列工具有些你可能暂时用不到。2.5 常见的构建失败和解决办法我在第一次构建时遇到的几个问题几乎每个新手都会碰到。内存不足导致的 OOM如果你使用的是虚拟机且分配给 VM 的内存不足 8GB编译器很可能在链接阶段直接被杀掉。解决方案是限制并行度ninja -j 2或者干脆把 CMAKE_BUILD_TYPE 设为 ReleaseDebug OOM 概率更高。磁盘被撑爆前面提到的 100GB 预留空间真不是危言耸听。如果你开启了除 X86 外的其他 target或者在 Debug 模式下构建一个 build 目录就能吃掉 50GB 甚至更多。请务必在构建之前用df -h看一下剩余空间。旧版 CMake 或编译器版本过低LLVM 现在要求编译器支持 C17如果你用的是 Ubuntu 20.04 自带的 GCC 9 可能没问题但 Ubuntu 18.04 默认的 GCC 7 在编译最新 LLVM 时会报出一系列语法错误。建议直接装新版的 GCC 或 Clang。关于构建速度还有个加速手段ccache。在 CMake 配置时加入-DLLVM_CCACHE_BUILDON后续的增量构建会快不少。特别是当你只改了一个头文件却要触发大量重编的时候ccache 简直是救命稻草。sudo apt install ccache cmake -G Ninja -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_CCACHE_BUILDON \ ../llvm我自己的经验是第一次构建花了一个多小时开着 ccache 之后后面每次修改源码后增量编译基本控制在两分钟以内。这个收益非常可观建议一开始就配上。3. 从 Clang 到 llc一条命令里的编译器流水线构建完成之后你可以把整个 LLVM 工具链想象成一条流水线。Clang 把 C/C 源码变成 LLVM IRopt对 IR 进行优化llc把 IR 目标化并生成汇编。下面用一个最简单的例子把这条链路完整跑一遍。先写一段测试代码test.cint add(int a, int b) { int sum a b; return sum; } int main() { return add(1, 2); }3.1 从 C 源码到 LLVM IR首先生成 IRclang -S -emit-llvm test.c -o test.ll这段命令生成的test.ll是一个文本形式的 LLVM IR。打开它你会看到类似这样的内容; ModuleID test.c source_filename test.c target datalayout e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128 target triple x86_64-unknown-linux-gnu define dso_local i32 add(i32 noundef %0, i32 noundef %1) local_unnamed_addr { %3 add nsw i32 %1, %0 ret i32 %3 } define dso_local i32 main() local_unnamed_addr { %1 call i32 add(i32 1, i32 2) ret i32 %1 }不要被这些语法吓到。define dso_local i32 add就是在定义一个名为add的函数返回i32接受两个i32参数。%3 add nsw i32 %1, %0表示把%1和%0相加结果存到虚拟寄存器%3。这里的%0、%1分别是函数的第一个和第二个参数。有意思的是我没有指定-O0但 Clang 在默认优化级别下已经做了一些简化。如果加-O0IR 看起来会啰嗦得多会有alloca、load、store这些内存操作因为-O0下编译器倾向于忠实还原源代码的结构。你可以试试clang -S -emit-llvm -O0 test.c -o test_O0.ll对比后你就能直观感受到优化到底在 IR 层面做了哪些事情。3.2 用 opt 做 IR 级别优化IR 生成之后中间优化由opt完成。我经常用到的命令是这样opt -S -passesmem2reg,instcombine test_O0.ll -o test_opt.llmem2reg把allocaloadstore的内存操作提升成 SSA 形式的虚拟寄存器操作这是很多优化的基础instcombine做指令层面的合并和简化。跑完之后再把test_opt.ll和test_O0.ll对比你会看到 IR 明显变得清爽了。注意-passes是新的 Pass Manager 的参数形式。旧版 LLVM 命令写作opt -mem2reg -instcombine新版统一使用-passes。LLVM 15 之后旧式参数兼容性逐渐被移除所以直接用-passes最稳。3.3 用 llc 生成目标汇编IR 优化完之后llc负责把 IR 转换成目标汇编llc test_opt.ll -o test.s生成的test.s是针对你构建 LLVM 时指定的 target这里是 X86的汇编代码。如果你在构建时开了其他 target可以指定-marchllc test_opt.ll -marchaarch64 -o test_arm.s这就是 LLVM 架构神奇的地方同样的 IR 输入换一个后端参数就能输出完全不同的指令集汇编。七年前我第一次在 X86 机器上生成 ARM 代码时真的有啪一声通透了的感觉。3.4 bitcode 与 llvm-dis 和 llvm-astest.ll是人类可读的文本 IR但 LLVM 实际使用中经常把 IR 打包成二进制 bitcode 格式后缀是.bc。在配置的时候你可能听说过-flto这种编译选项它本质上就是让编译器生成 bitcode 而不是汇编链接阶段再由 LTO 优化器做跨模块优化。你可以手动试一下clang -emit-llvm -c test.c -o test.bc llvm-dis test.bc -o test_from_bc.llllvm-dis把 bitcode 反汇编成文本 IRllvm-as则做反向操作。这两个工具是日常查看 IR 的最佳伙伴。3.5 小工具串起来看LLVM 还提供了一堆实用小工具每个工具解决一个特定问题llvm-nm查看目标文件中符号表llvm-objdump反汇编目标文件配合-d参数查看指令llvm-readelf/llvm-readobj查看 ELF/Mach-O/COFF 头的结构llvm-dwarfdump查看 DWARF 调试信息做编译器调试的时候非常常用llvm-bcanalyzer分析 bitcode 文件内容。我经常把一段 C 代码用clang -S -emit-llvm -O2生成 IR再配上llc调到 ARM 汇编最后用llvm-objdump反过来看字节码整条链路下来编译器每个阶段到底做了什么就一目了然。4. 手写一个 LLVM Pass读懂 IR 才是理解整个项目的钥匙跑通工具链只是热身真正能让一个人理解 LLVM 核心架构的是亲手写一个 Pass。Pass 是 LLVM 中优化器的最小单元它会在 IR 上进行遍历和变换。这里我写一个打印每个函数指令数量的示例虽然功能简单但包含了一个现代 LLVM Pass 的完整模板你完全可以在此基础上做代码插桩、死代码删除、循环优化等实验。4.1 准备开发目录假设你在llvm-project之外建立独立目录my-pass结构如下my-pass/ ├── CMakeLists.txt └── inst_counter.cpp4.2 编写 Pass 源码以下代码基于 LLVM 17/18/19 的 New Pass Manager 骨架。#include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/IR/LegacyPassManager.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { class InstCounter : public PassInfoMixinInstCounter { public: PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { unsigned Count 0; for (auto BB : F) { for (auto I : BB) { (void)I; Count; } } errs() F.getName() : Count instructions\n; return PreservedAnalyses::all(); } }; } // namespace llvm::PassPluginLibraryInfo getInstCounterPluginInfo() { return { LLVM_PLUGIN_API_VERSION, InstCounter, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name inst-counter) { FPM.addPass(InstCounter()); return true; } return false; }); }}; } extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getInstCounterPluginInfo(); }几个关键点解释一下PassInfoMixinInstCounter是 CRTP 模板New Pass Manager 要求 Pass 类对外暴露一个run方法。这里的Function表示该 Pass 的作用域是函数级别Function PassFunctionAnalysisManager用来获取所需的分析结果。PreservedAnalyses::all()表示我们只是读取 IR不做任何修改。如果你真的修改了 IR比如把某些指令删掉了这里就应该返回PreservedAnalyses::none()或者精确地声明哪些分析保持有效。registerPipelineParsingCallback是让opt -passesinst-counter可以识别到你自定义 Pass 名的入口。它与-load-pass-plugin参数的配合是最常见的用法。llvmGetPassPluginInfo必须是外层 C 接口因为opt需要动态链接这个共享库。4.3 编写 CMake 配置在my-pass/CMakeLists.txt中加入cmake_minimum_required(VERSION 3.20) project(InstCounter) find_package(LLVM REQUIRED CONFIG) message(STATUS Found LLVM ${LLVM_PACKAGE_VERSION}) message(STATUS Using LLVMConfig.cmake in: ${LLVM_DIR}) include_directories(${LLVM_INCLUDE_DIRS}) separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS}) add_definitions(${LLVM_DEFINITIONS_LIST}) add_library(InstCounter MODULE inst_counter.cpp)这里的MODULE关键字意味着生成一个共享库后面用-load-pass-plugin加载时用的就是这个.so文件。如果你的 LLVM 是手动构建的find_package(LLVM REQUIRED CONFIG)会去找你构建目录里的lib/cmake/llvm/LLVMConfig.cmake所以配置时要用-DLLVM_DIR/path/to/your/build/lib/cmake/llvm指定位置。4.4 编译并加载运行cd my-pass mkdir build cd build cmake -G Ninja -DLLVM_DIR/path/to/llvm-build/lib/cmake/llvm .. ninja编译成功后在 build 目录里会生成libInstCounter.so。接下来拿前面生成的test.ll来试opt -load-pass-plugin./libInstCounter.so -passesinst-counter test.ll -S -o /dev/null如果你看到类似这样的输出说明插件生效了add: 2 instructions main: 3 instructions到这一步你已经亲手编写并加载了一个 LLVM Pass。说实话这个门槛迈过去之后整个优化器的源码对你来说就不再是一堵墙了。你可以尝试各种改动比如打印每条指令的 Opcode、统计某个 BasicBlock 的前驱数量、或者识别循环结构并打印循环内指令数。LLVM 提供了一套很完整的分析接口比如LoopInfo、DominatorTree等拿这些接口和 IR 文档对照着看是学习编译器优化的高效路径。5. 在 llvm-project 里读源码和调试的有效路径写 Pass 的过程中你必然会需要翻阅 LLVM 源码。llvm-project 的源码量很大直接打开文件从头看到尾是不可行的。我的经验是以问题为线索从工具的使用反推源码。5.1 源码目录到底怎么读llvm-project 的根目录有很明确的划分llvm/lib/IR核心 IR 数据结构比如Instruction、BasicBlock、Function、Module等类的定义llvm/lib/Transforms所有优化 pass 的源码比如LICM循环不变量外提、GVN全局值编号、InstCombine等llvm/lib/CodeGen代码生成的后端基础结构包括指令选择、寄存器分配、指令调度等llvm/lib/Target各个具体芯片后端的代码比如X86、AArch64clang/lib/Sema、clang/lib/CodeGenClang 前端的语义分析和 IR 生成代码。我通常从llvm/lib/Transforms开始。想搞清楚某个 pass 做了什么直接进去看源码比如llvm/lib/Transforms/Scalar/LICM.cpp。打开后你会看到完整的 Pass 优化流程以及调用到的分析接口。这种带着问题找答案的方式远比按目录顺序读一遍高效。5.2 调试方式先开断言再用 -debug-onlyLLVM 内部有一套自己的调试日志系统。在写 pass 时你可以在代码中这样调试LLVM_DEBUG(dbgs() Processing instruction: I \n);但这行输出默认是不打印的。需要在构建时加-DLLVM_ENABLE_ASSERTIONSON然后运行时用-debug-only指定模块名。LLVM_DEBUG 的dbgs()输出与-debug-only参数配合可以做到按模块过滤信息这在跟踪大规模优化的时候特别重要。例如要想看 LICM 这个 pass 的调试输出opt -passesfunction(licm) -debug-onlylicm test.ll -S -o /dev/null你可以在源码里找到DEBUG_TYPE然后在命令行中传入这个类型名作为过滤条件。需要注意的是-debug-only在 Release 模式下可能因为宏没有定义而不生效所以还是建议用RelWithDebInfo来构建。另外在写 pass 时如果开启了断言且你的 pass 破坏了 IR 的某些不变性质LLVM 会在触发assert的地方抛出错误堆栈。这些 assert 消息不仅不是负担反而是理解 IR 约束的最佳教材。很多 pass 的注释里都写有为什么必须保持这个不变性的说明做代码审查时你会发现它们在约束你的修改方式这些约束背后就是编译器理论的具体体现。5.3 用 git 历史学设计LLVM 的演进非常快API 变化频繁。如果你发现一个文档和源码不对最快的方式是去看 git 历史。比如你查一个接口是在哪个版本引入、为什么要这么改git log -p --follow file能给你完整线索。我也习惯把llvm-project的 GitHub 仓库直接 fork 一份方便直接在 PR 页面评论里看 maintainer 的讨论。很多设计取舍、为何不用另一种方案、将来会改成什么样在 review 讨论里讲得比源码注释还要透彻。这对理解为什么 LLVM 会发展成今天这个形态非常有价值。5.4 符号断点与反汇编调试编译器工具本身gdb或lldb是必须会的。比如我想看看opt在运行instcounter时具体访问了哪些指令可以这样gdb --args opt -load-pass-plugin./libInstCounter.so -passesinst-counter test.ll -S -o /dev/null (gdb) break InstCounter::run (gdb) run如果是RelWithDebInfo构建断点能命中的概率很大。你还能在函数内部打印F的内容、BB的内容。这种从项目外观察行为到进入源码内部跟踪数据流的转换是研究编译器的重要能力。6. 社区协作和代码规范别让风格问题挡住你的 PR如果你想把改写过的代码贡献回上游那你还要了解 LLVM 社区协作的基本规则。很多人辛辛苦苦写完一个 feature因为代码格式和 review 要求不符被打回好几次甚至整个方案被反复 review 到失去热情。提前了解规范能省很多事。6.1 代码风格LLVM Coding Standards 和大多数开源项目不同它使用两个空格缩进不用 tab函数名和类型名使用大驼峰PascalCase变量名使用小驼峰camelCase成员变量的前缀可以是尾随下划线。比如class BasicBlock { Instruction *FirstNonPHI; // 成员变量没有前缀下划线命名风格统一 public: bool isEntryBlock() const; // 方法名首字母大写 };这种风格和 Google C Style Guide、Linux Kernel 风格都不同。你写代码之前直接跑clang-format即可。LLVM 项目根目录下有个.clang-format文件格式规则都在里面。用内置的clang-format工具clang-format -i my_file.cpp然后再 check 一下是否满足要求git diff --check6.2 提交 PR 与 DCOLLVM 的协作模式经历过多次变化从 Phabricator 迁移到 GitHub PR 之后参与门槛已经降低了很多。你 fork 一份代码改完以后提交 PR然后有维护者对代码做 review指出问题、讨论方案循环几轮后合入主干。需要注意的是LLVM 要求每个 commit 都包含 Signed-off-by这是 Developer Certificate of OriginDCO的机制。你在提交时用git commit -s这样会在 commit message 末尾自动加上一行类似Signed-off-by: Your Name youremail.com的内容。没有这行署名很多 CI 流程会直接失败。另外LLVM 的项目治理流程里一个较大的功能往往要求先有设计文档RFC并在 Discourse 邮件列表中讨论获得共识后再开始编码。如果你是第一次贡献可以从修 bug、补测试用例、更新文档这类小改动开始先熟悉流程再挑战大功能。6.3 测试与验证LLVM 的测试体系走 LITLLVM Integrated Tester测试文件通常是.ll文件里面用RUN:指令描述命令行应该怎么执行。比如llvm/test/Transforms/LICM下的测试文件就是个非常好的学习样本。; RUN: opt %s -passeslicm -S | FileCheck %s ; CHECK-LABEL: define void test ; CHECK: br label %loop你要提交新 pass需要在llvm/test/Transforms/YourPass目录下补充 RUN 和 CHECK 用例。这个习惯极其重要LLVM 的维护者极度看重复现性和回归测试一个没有测试的 pass 几乎不可能合入上游。7. 给新手的最后几条建议如果你正打算把 llvm-project 当作长期学习的项目我有几条实在的经验之谈。第一不要企图一次看懂所有东西。我见过太多人打开llvm/lib/CodeGen/SelectionDAG的源码两天后彻底放弃。正确的路线是先跑通工具链再写一个 hello world 级别的 Pass逐步扩大到修改优化行为然后才适合去读后端核心代码。每一步都建立在动手验证之上不要只做看的功夫。第二读懂 IR 就等于拿到了整个编译器的钥匙。所有 pass 的本质都是 IR 到 IR 的变换。遇到不熟悉的getElementPtr指令先把LangRef文档读透遇到不熟悉的 metadata 类型先查它在哪个 pass 里被生成。IR 文档虽然长但你值得花时间从头到尾扫一遍。第三敢于破坏代码再修好。我学到的最多的内容往往来自故意在一个 pass 里改错逻辑然后观察 LLVM 抛出什么 invariants are broken错误。这些错误信息其实就是在告诉你 IR 的规则和边界。调试过程是痛苦且耗时的但正是这种痛苦让你真正理解了编译器这个复杂系统。最后分享一个我在实际开发中的习惯在构建目录和源码目录旁边我始终保留一个干净的playground目录里面放着自己写的测试.c文件和.ll文件。不管是在开发新 pass 还是在调试既有问题这个目录里的例子能让我随时用最小样本验证想法。编译器从来不是一个黑盒只要你愿意走到源码里面去它反而比很多应用级代码更透明、更讲道理。动手之后你会发现这个大项目没有想象中那么难啃。
返回列表