LLVM入门指南:从架构解析到源码构建与Pass编写
如果你最近下载了llvm-project打开目录却不知道从哪开始其实这很正常。我接触LLVM项目也有好几年了坦白讲这个仓库是我见过的开源项目里结构最庞杂、学习曲线最陡峭的一个但一旦你摸清了它的主脉络很多工作都会变得顺理成章。这篇文章不是官方文档的翻译而是我总结的一份从理解架构、完成构建到动手写Pass的实操笔记适合刚接触LLVM的开发者、想做语言前端的同学以及需要在交付里用LLVM做性能优化或代码分析的工程师。llvm-project覆盖的不只是一个编译器而是一整套编译器基础设施。你可以在里面获得词法语法分析、中间表示优化、目标代码生成、链接器、调试器组件、C/C标准库实现甚至用于构建AI编译器框架MLIR的全部源码。正因为它包含的东西太多很多人的第一反应反而是“我该从哪里看起”。这篇文章会从整体架构讲到源码构建再到亲手写一个能跑的LLVM Pass覆盖一条完整的学习主线希望能帮你少走一些弯路。1. LLVM项目到底是什么不只是编译器1.1 从名字到架构LLVM的设计哲学LLVM这个名字最开始是Low Level Virtual Machine的缩写但今天它已经不再是一个虚拟机的概念而是一个模块化的编译基础设施。它的核心设计其实可以总结成一句话前端、中端、后端解耦用统一的中间表示IR串起整个编译过程。这种设计与传统的GCC那种单一大仓库、前后端强耦合的方式截然不同。我经常用一个比较好懂的比喻来解释这套架构把LLVM想象成一个大型跨国翻译公司。前端Frontend是“方言翻译员”负责把C/C、Rust、Swift等不同语言翻译成公司内部通用的标准语言中端Middle-end是“审校编辑”对翻译后的标准语言做润色和精简删掉重复的废话替换成更高效的表达后端Backend是“目标语言母语者”再把标准语言翻译成不同国家的人能听懂的地方方言也就是x86、ARM、RISC-V这些机器指令。这套解耦设计带来的最大优势是你只需要写一个新的前端就能复用整个中端的优化能力和所有后端架构支持。这就是为什么那么多新语言比如Rust、Swift都选择基于LLVM来做编译器它们不需要重新发明优化器和代码生成器只需要专注于自己语言本身的语法和语义。1.2 你最终能从LLVM项目里得到什么llvm-project的价值绝不仅仅是“可以用clang编译C程序”。对开发者和研究者来说它提供的核心能力可以拆成几个层面第一层是编译器工具链。clang是C/C/Objective-C的前端它负责把源码编译成IRlld是高性能链接器compiler-rt提供运行时支持库libc和libcabi是C标准库实现。这些组合起来你就能拿到一套完整的、可以用于生产环境的编译工具链很多商业项目、嵌入式SDK、甚至游戏主机开发工具都是基于这套东西构建的。第二层是优化与代码分析基础设施。LLVM的Pass框架允许你在IR层面插入自定义的优化、静态分析、插桩逻辑。你可以写一个Pass统计函数调用次数可以写一个Pass做死代码消除也可以写一个Pass检测空指针解引用。这是安全研究、性能工程、程序分析领域极其肥沃的土壤。第三层是代码生成与JIT能力。LLVM后端可以生成多种架构的机器码也支持在运行时实时编译生成机器码。数据库引擎、图像渲染器、动态语言运行时都有大量使用LLVM JIT做性能加速的实例。如果你对“写一个解释器再把热点函数JIT编译成机器码”这类方向感兴趣LLVM会是最合适的底座。第四层是专项框架比如MLIR和Polly。MLIR是编译器领域的“乐高积木”可以用来构建面向AI芯片、数据流计算等特定领域的编译器Polly是做循环优化和多面体模型计算的项目。这些都属于LLVM生态里比较前沿的分支难度较高但天花板也高。在我接触过的项目里大多数人的需求其实落在一二三层。这篇文章也主要围绕这些层次展开先把主脉络打通再去碰专项方向会轻松很多。2. 源码目录拆解llvm-project里到底有什么2.1 核心模块与周边子项目当你在GitHub下载完整仓库或者执行完第一次clone之后llvm-project目录下的内容常常让新手眼花缭乱。我列一个表格把几个重要子项目的作用说清楚你按需去翻阅就行目录名作用学习优先级llvmLLVM核心库、IR定义、Pass框架、代码生成、opt/llc等工具源码最高clangC/C/Objective-C前端把源码解析成AST再降级到IR最高clang-tools-extraclang-tidy、clang-format等C辅助工具中lld高性能链接器支持ELF/COFF/Mach-O中compiler-rt运行时库sanitizer、builtin、profile等中libcC标准库实现低libcabiC ABI与异常处理支持库低libcC标准库实现较新加入低polly基于多面体模型的循环变换与优化低mlir多级中间表示框架面向AI与自定义编译器按需flangFortran前端按需openmpOpenMP运行时与编译支持按需这里我特别想强调两点。第一llvm和clang是两个独立又有很强关联的代码库它们目录相邻但构建是分开的。你在使用命令行编译时感觉不出区别但读源码时一定要知道“语法分析相关的代码在clang里IR优化相关的代码在llvm里”否则很容易迷路。第二根目录下还有一些脚本和配置比如llvm/utils里有很多辅助脚本llvm/cmake里有一系列构建模块这些不是顶层内容但偶尔在排查构建问题时很有用。2.2 源码构建的前置准备与CMake参数选择先泼一盆冷水llvm-project的完整构建对机器性能是有要求的。我早年在一台4核8G内存的机器上尝试全量编译连libc和compiler-rt一起编结果编译到一半系统直接OOM。所以前置准备不能马虎。依赖方面Linux下需要gcc或clang、cmake、ninja、python3、zlib等基础工具macOS下需要Xcode Command Line Tools。如果你用Windows官方推荐用Visual Studio 2019以上或者用Visual Studio Code配合ninja工具链但Windows的构建体验相对Linux和macOS来说更折腾一些新手前期尽量用Linux环境会顺畅很多。CMake参数选择是整个构建流程里最值得花时间理解的环节。很多新手一上来就执行默认构建结果等了七八个小时还没编完实际上是因为用了错误的配置。我给出一个比较常用的组合再逐个解释每个参数的含义cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_USE_LINKERlld \ -DLLVM_PARALLEL_COMPILE_JOBS8 \ -DLLVM_PARALLEL_LINK_JOBS1-G Ninja使用ninja替代默认的Unix Makefiles并行构建速度和输出可读性都远优于make。正常情况下构建LLVM这类大型C项目Ninja能帮我节省至少三分之一的时间。-DCMAKE_BUILD_TYPERelease编译产出release版本关闭debug符号和断言编译速度更快生成的编译器运行效率更高。代价是后续如果你想调试LLVM源码或者自己写的Pass就得改用Debug版本这在下文会展开说。-DLLVM_ENABLE_PROJECTSclang指定除了核心llvm之外还要同时构建哪些项目。这里只选了clang目的是控制构建规模。如果你还需要lld可以写成clang;lld但每一项都会显著增加编译时间。-DLLVM_TARGETS_TO_BUILDX86只生成X86后端。LLVM默认会生成X86、ARM、AArch64、RISC-V等一大堆后端大部分人是根本用不到的。这一步能省下大量编译时间和磁盘空间。如果你的目标是嵌入式开发可以改成ARM;AArch64。-DLLVM_USE_LINKERlld用lld作为链接器。前提是你系统里已经有一个可用的LLVM/lld版本或者构建环境中带了系统clang。这一步能大幅缩短二进制链接时间我第一次把这个开关打开时链接clang的速度从五六分钟直接降到一分多钟。-DLLVM_PARALLEL_COMPILE_JOBS8和-DLLVM_PARALLEL_LINK_JOBS1控制并行编译和并行链接的任务数。链接阶段极度吃内存并行链接多路经常会爆内存所以我在链接任务上只开1个job编译任务则根据CPU核数来设实测比默认策略稳定得多。如果你只想快速跑通一个最小版本完全可以只编llvm核心而不编clang但那样你后续无法用clang生成IR做试验。以我日常经验来看作为入门阶段只构建llvmclangX86后端是一个性价比最高的组合既能完整跑通开发流程又不会把电脑拖垮。3. 从零构建一次完整的llvm-project编译实操3.1 第一步克隆源码与版本选择提到获取llvm-project我建议直接走官方GitHub仓库git clone --depth 1 --branch llvmorg-18.1.8 https://github.com/llvm/llvm-project.git这里有两个关键点。第一--depth 1表示浅克隆只拉取最新一次提交记录可以省掉大量历史提交数据缩短下载时间。第二--branch指定一个具体的release标签。很多新手习惯直接clone默认分支也就是main分支这样做但LLVM的main分支每天都在大改API今天能过的代码可能下周就编译不过了。对绝大多数人来说选择一个稳定的release版本才是正确操作。我一般会选当前社区维护周期内比较新的版本比如18.x既有较新的特性又有大量教程资料可以参考不容易在API上踩到莫名其妙的老坑。下载完成后进入目录cd llvm-project可以看到根目录下已经包含前面说的那些子项目目录。接下来就可以开始配置和构建了。3.2 第二步使用CMake配置构建目录构建llvm-project强烈建议采用“源码目录和构建目录分离”的做法也就是out-of-source构建。千万不要直接在里面新建build目录然后疯狂make这样以后想换一个配置重编的时候会特别痛苦。进入项目根目录后执行cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_USE_LINKERlld \ -DLLVM_PARALLEL_COMPILE_JOBS8 \ -DLLVM_PARALLEL_LINK_JOBS1CMake配置过程中会输出一堆检查信息如果某个依赖缺失一般会在这里直接报错。一个比较典型的错误是Could NOT find ZLIB解决方式在Ubuntu/Debian上是sudo apt install zlib1g-devmacOS上brew install zlib即可。配置完成之后正式构建cmake --build build --target clang如果没有意外编译会开始跑起来。具体耗时取决于CPU核数、内存和磁盘。我自己在8核16G内存的MacBook Pro上构建大概需要20到40分钟如果是4核8G内存的老机器可能得一两个小时。编译期间风扇狂转是很正常的只要没报错就耐心等待。注意不要在编译过程中直接把机器跑死机。如果内存只有8G建议把LLVM_PARALLEL_LINK_JOBS保持为1编译任务数也不要超过CPU核数否则系统会疯狂swap速度反而更慢。3.3 第三步验证编译结果构建完成后构建产物在build/bin目录下。先检查版本build/bin/clang --version输出里会显示clang版本号以及它基于的LLVM版本。我第一次自己编译出clang时最激动的事情就是用这个“亲手做的编译器”去编一段HelloWorld。echo int main() { return 0; } hello.c build/bin/clang hello.c -o hello ./hello这里能正常生成可执行文件就说明整套工具链已经跑通了。接下来可以更进阶一点生成IR来观察编译器的中间产物build/bin/clang -emit-llvm -S hello.c -o hello.ll打开hello.ll看一下你会看到类似define i32 main()这样的内容。让IR第一次出现在你眼前是理解LLVM整个设计哲学非常重要的瞬间后面我们就是和这些东西打交道。3.4 构建提速的实践经验编译LLVM项目是一个漫长的过程尤其是反复清理重编的时候速度会直接影响开发和学习的节奏。我自己有几次实际很管用的提速经验顺手分享给大家。第一尽量用ccache做编译缓存。在CMake配置时加上-DLLVM_CCACHE_BUILDON或者直接设置环境变量让ccache生效。这样当你切分支、改CMake参数、清理重编时大部分目标文件都会命中缓存第二次构建能快到一个新的量级。ccache对后续反复改代码改配置的体验提升特别明显。第二不要在Debug模式下做全量验证。Debug版本编译慢、运行也慢但缺点是产出文件巨大动辄几十GB。我建议日常开发用Release构建只有在需要深入调试Pass逻辑、打印详细堆栈时才配置单独的Debug构建目录。两个构建目录共存没问题但别同时开着两个ninja进程执行构建否则会互相抢资源。第三关闭不需要的组件。LLVM_ENABLE_PROJECTS只保留你要的LLVM_TARGETS_TO_BUILD只保留你要的后端这两个开关我在前面已经说过每次配置时都要检查一遍。很多人的电脑明明配置不差编译却特别慢查到最后往往发现自己默认编译了六七个后端白白浪费了几十分钟。4. 核心实操手写一个简单的LLVM Pass4.1 LLVM Pass机制简介Pass是LLVM中用于“遍历和变换IR”的基本单位。你可以把一个Pass理解成流水线上的一个工人它接收IR对IR做某种检查或修改然后把处理过的IR交给下一个工人。编译器里的优化、分析、插桩全部都是Pass的实例。LLVM的Pass框架演进过好几次。老版本用的是legacy PassManager通过注册llvm::FunctionPass这种类来工作而新版本更推荐使用new PassManager也就是基于PassInfoMixin的写法。新PassManager更干净支持更细粒度的分析依赖很多老Pass也在陆续迁移过来。2022年之后的LLVM普遍默认采用new PassManager早期的教程很多已经过时了所以大家在看别人写的旧代码时如果看到一个Pass还在使用initializeXXXPass这样的老接口大概率就是legacy写法考虑换成新写法会省心很多。在动手之前有两点基础要打牢一是知道LLVM IR的基本结构函数、基本块、指令二是知道整个Pass框架的挂载方式。下面我会用new PassManager的写法带大家写一个最简单的Pass。4.2 从零编写一个FunctionPass我建一个项目文件夹hello-pass里面放两个文件hello_pass.cpp和CMakeLists.txt。先看代码hello_pass.cpp#include llvm/IR/Function.h #include llvm/IR/PassManager.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { struct HelloPass : public PassInfoMixinHelloPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { errs() Hello from LLVM Pass: F.getName() \n; return PreservedAnalyses::all(); } }; } // namespace extern C ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, HelloPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name hello) { FPM.addPass(HelloPass()); return true; } return false; }); }}; }这里面的每个部分都是有讲究的PassInfoMixinHelloPass是CRTP模板基类new PassManager用它来完成类型擦除。换句话说只要你继承这个模板框架就知道怎么管理你Pass的生命周期。run(Function F, FunctionAnalysisManager AM)是这个Pass的核心入口。每当PassManager遍历到IR里的一个函数就会调用一次这个run函数。F是当前正在处理的函数通过F.getName()可以拿到函数名。PreservedAnalyses::all()表示“我没有修改任何IR所以所有分析和变换的结果都可以保持有效”。如果你的Pass真的改了IR就必须根据实际情况声明“哪些分析被破坏了”。如果拿不准最保守的写法是返回PreservedAnalyses::none()让框架重新计算所有分析代价是增加编译耗时。llvmGetPassPluginInfo是这个Pass作为动态库被opt加载时的入口。它把Pass插件的信息暴露给LLVM里面的回调函数registerPipelineParsingCallback会负责把命令行里你写的-passeshello和这个HelloPass连接起来。配套的CMakeLists.txt这样写cmake_minimum_required(VERSION 3.20) project(HelloPass) 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(HelloPass MODULE hello_pass.cpp) target_link_libraries(HelloPass PRIVATE LLVMCore) set_target_properties(HelloPass PROPERTIES PREFIX OUTPUT_NAME hello-pass)这里面的关键点MODULE表示编译成动态插件库而不是可执行文件LLVM_INCLUDE_DIRS需要指向你之前构建LLVM时对应的include目录LLVMCore保证我们能链接到PassBuilder和PassPlugin这些库。配置方式假设你的LLVM构建目录在../llvm-project/buildcd hello-pass cmake -S . -B build -DLLVM_DIR$PWD/../llvm-project/build/lib/cmake/llvm cmake --build build编译结束后你会在build目录下看到一个hello-pass.so文件。4.3 用opt工具运行你的Passopt是LLVM自带的IR优化工具就像IR的“独立实验台”。你不需要启动真实编译器就能在IR文件上执行任意Pass组合。先准备一个测试用的C文件int add(int a, int b) { return a b; } int main() { return add(1, 2); }用我们编译出的clang把它编译成IR字节码clang -emit-llvm -c test.c -o test.bc然后用opt加载Pass插件并执行opt -load-pass-plugin./hello-pass/build/hello-pass.so -passeshello test.bc -o test.hello.bc如果一切正常终端会打印Hello from LLVM Pass: add Hello from LLVM Pass: main注意这里需要加-o参数否则opt会因为没有指定输出文件而报错。输出文件test.hello.bc看起来和原始test.bc几乎没有区别因为我们这个Pass只做日志输出不改任何IR。但这已经足够证明整个Pass开发与运行链路是通的。如果你想把优化后的IR转成可读文本执行llvm-dis test.hello.bc -o -就能通过标准输出看到IR全貌。如果看不到任何变化也不用担心这是正确的我们的HelloPass只是打个招呼真正要修改IR的Pass会在下一节介绍。4.4 将Pass集成到Clang编译流程有的人可能觉得每次都要手动先编译成bc再用opt去跑流程太麻烦。其实Pass还可以直接挂载到clang在编译源码时自动执行这样更像“真实编译器里做优化”的用法。new PassManager下clang提供了-fpass-plugin参数clang -fpass-plugin./hello-pass/build/hello-pass.so test.c -o test运行这个命令时只要编译流程里处理到函数你的Pass就会触发。实际输出会混在编译过程里你可以看到那些Hello from LLVM Pass: xxx的日志。这里有一个特别容易踩的坑如果Pass的动态库是用旧版LLVM编译的而clang是新版LLVM就会因为IR结构不兼容直接导致崩溃或加载失败。所以第一次做Pass实验时一定要确保Pass是用和clang同一个LLVM构建目录编译出来的。等到你经验多了再考虑跨版本兼容的问题也不迟。如果大家用的是老版本的clang尤其是12.x之前可能-fpass-plugin参数不存在传统做法是用-Xclang -load -Xclang yourpass.so但这种用法在新版本里已经不建议了。最简单的判断方法就是看clang --version然后选择对应时代的用法别拿10.x的教程硬套18.x的代码。5. LLVM调试与工具链从IR到机器码的观察5.1 学会读LLVM IR编译器的中间表示是理解LLVM最重要的一环很多人在这一步选择跳过直接去写Pass结果发现改一个指令都无从下手。建议还是稍微花点时间把IR的基本格式看懂。前面我们已经生成过test.bc现在用可读方式看看IRbuild/bin/llvm-dis test.bc -o test.ll cat test.ll你会看到类似下面这样的内容define i32 add(i32 %a, i32 %b) { entry: %add add nsw i32 %a, %b ret i32 %add }我用大白话解释一下define i32 add(i32 %a, i32 %b)表示定义一个返回32位整数、接收两个32位整数参数的函数add。在entry这个基本块里%add是一个虚拟寄存器存储了%a和%b相加的结果ret则负责返回它。nsw表示这段加法默认不会发生带符号溢出优化器可以更积极地做变换。IR里最核心的要素是类型、SSA、基本块和指令。类型用i32、i8*、void等表示每个值都有明确的类型SSA意味着每个变量只被赋值一次这为许多优化提供了极大便利但也让新手一开始不适应“怎么不能多改几次变量”基本块是一段顺序执行的指令序列以label开头以跳转或返回指令结尾。你可以把IR理解成“带类型的伪代码”而后端就像是翻译官把它翻译成目标机器的“当地方言”。5.2 用llvm-dis、llc、lli完成分析LLVM提供了好几个配套工具是平时最常打交道的llvm-dis把二进制格式的IR.bc转成可读文本.ll。前面已经演示过这是理解Pass到底改了什么的最直观途径。llc把IR转换成目标汇编代码。它不执行整个C语言编译流程只做“IR到汇编”这一步。lli直接解释执行IR。它可以用解释模式模拟运行IR也可以参与JIT流程是验证IR逻辑正确性的高效工具。举个例子把test.ll编译成汇编build/bin/llc test.ll -o test.s此时打开test.s你会看到x86汇编。可能很多人第一次看汇编会头大但有个比较实用的方法是直接用lli跑一下IR确保逻辑正确再回头逐一对照汇编。比如build/bin/lli test.ll echo $?这个命令会运行IR里的main函数并返回退出码。如果main返回0你就能确认这个IR文件的所有逻辑都是正确的。调试JIT相关问题时这种“解释执行IR汇编比对”的组合非常有用。5.3 在VS Code里调试LLVM源码当我们深入一点需要调试Pass本身的逻辑看某一个变量在IR优化过程中发生了什么变化这时候就需要用到gdb或lldb。首要前提是你用Debug构建了LLVM。在CMake配置里把CMAKE_BUILD_TYPE设为Debug同时给CMake传入-DLLVM_ENABLE_ASSERTIONSONDebug默认开启断言然后再构建一遍。这一步会消耗大量内存和磁盘空间但为了调试是值得的。在VS Code里调试Pass我的做法是配置launch.json让gdb启动opt并传入加载插件的参数。大致配置如下{ name: opt run hello pass, type: cppdbg, request: launch, program: /path/to/build/bin/opt, args: [ -load-pass-plugin/path/to/hello-pass/build/hello-pass.so, -passeshello, test.bc, -o, test.hello.bc ], cwd: /path/to/your/workdir }然后在hello_pass.cpp的run函数里打上断点按下F5你就能看到Pass执行的完整流程。这里有个经验Debug构建和Release构建不要在同一个build目录里混着来否则CMake缓存会相互干扰。我一般是建build-release和build-debug两个目录各编各的平时用release调试时用debug。6. 常见问题与排查技巧实录6.1 构建阶段高频问题llvm-project构建过程中的坑很多都集中在资源不足和版本不匹配两个方向上。先看构建阶段错误/现象常见原因解决建议编译过程中内存耗尽进程被杀死并行编译/链接任务太多或Debug构建过于吃内存减少LLVM_PARALLEL_COMPILE_JOBS把链接JOB固定为1改用Release构建CMake报找不到LLVMConfig.cmake只编译了库没有生成完整的cmake配置或LLVM_DIR路径不对确认构建LLVM时没关闭LLVM_INSTALL_TOOLCHAIN_ONLY配置Pass项目时正确指定LLVM_DIR报错找不到zlib、libxml2等依赖系统缺少开发包Ubuntu安装zlib1g-dev、libxml2-devmacOS使用brew install zlib libxml2磁盘空间不足全量Debug构建可达80GB以上Release也在20GB左右清理其他项目用LLVM_TARGETS_TO_BUILD控制范围考虑ccache缓存量编译很慢但程序本身没问题实际是编了多余的后端或多余项目检查CMakeCache中的LLVM_TARGETS_TO_BUILD和LLVM_ENABLE_PROJECTS我特别想强调一下“链接时OOM”这个问题。很多次新手在8G内存机器上编译到某个大组件时系统突然卡死然后进程被杀。这是因为链接确实是LLVM构建中最吃内存的阶段几个大so文件同时链接能轻松吃满16G内存。保守的做法是始终把LLVM_PARALLEL_LINK_JOBS设为1虽然链路会慢一点但能避免绝大多数内存崩溃。6.2 运行时段错误与IR版本兼容构建成功不代表后续开发顺畅运行opt或clang时踩坑的案例也不少。我整理一个开发期高频问题的速查表错误/现象常见原因解决建议opt加载Pass插件时报错“Plugin does not match ABI”Pass插件编译时使用的LLVM版本与opt运行时不匹配用同一个LLVM构建产物来编插件和跑opt-passeshello报“unknown pass name”Pass注册回调没生效或者回调返回false检查llvmGetPassPluginInfo里注册的Name是否与命令参数完全一致重新编译插件clang使用-fpass-plugin时报错参数不存在编译器版本太老new pass manager的插件参数未实现升级LLVM版本或改用老接入方式-Xclang -load -Xclang xxx.sorun函数里修改了IR但返回PreservedAnalyses::all()导致下游结果异常没有正确声明的分析结果如果修改了IR尽量返回PreservedAnalyses::none()或者使用getAnalysis后明确标记被改变的分析手写IR文件不合法llc/opt直接报错IR里的类型、指令格式有误先用clang生成基准IR对照按LLVM LangRef逐行修正Debug版和Release版混用运行时行为不一致Debug带断言Release会触发不同优化路径遇到诡异问题优先统一所有组件为同一个构建版本在LLVM开发里版本一致性至关重要。我见过太多人从网上下了一个编译好的旧版Pass插件再配一个新版LLVM结果跑什么错什么。在做Pass开发之前请先确认你的opt、clang、插件都来自同一个构建目录这是成本最低的排错手段。6.3 学习资源与下一步方向如果你已经跟着上面跑通了构建和Pass开发说明LLVM的基础回路已经建立起来下一步可以分方向深入。我推荐几个比较靠谱的资源官方文档llvm.org/docs下的LangRef和WritingAnLLVMNewPMPass是我反复翻阅的两份文档尤其是LangRef里定义了IR的所有细节遇到不确定的指令含义时直接查它比问搜索引擎快。源码阅读LLVM源码本身就是最好的教材。llvm/lib/Transforms目录下有大量Pass的完整实现比如InstCombine、EarlyCSE读它们的代码能很快提升你对Pass框架的理解。Compiler Explorer在线机器码查看工具支持在多个编译器版本和多个目标架构之间切换。当你改了IR或C代码想快速看它对不同后端的影响这个工具非常顺手。邮件列表和论坛遇到疑难问题可以搜索llvm-dev邮件列表和相关论坛很多老坑都有现成答案提问前先搜索能帮你省下不少时间。如果你已经有较扎实的C和编译原理基础后续可以进一步研究MLIR框架这是LLVM生态里非常活跃的方向也是连接传统编译技术和AI编译器的桥梁。不过我的建议是先把这一篇文章里的基础链路走完再考虑那些更宏大、更前沿的模块基础不打牢直接硬啃MLIR很容易被庞大的抽象层级劝退。写到这里回到我自己的经验。最初我下载llvm-project的时候也被它的体量吓了一跳编译一次动辄几十分钟打开源码全是模板和层层抽象说实话有过很强的挫败感。但当我第一次写完一个只有几十行的Pass看到它真的在编译器里跑起来时那种“原来编译器内部是这么运转的”的感觉是平时写普通业务代码很难获得的。现在如果让我给第一次接触LLVM的人提建议我会说不要试图把它当成本百科全书先搭好一个能用的环境写一个最小的Pass读一段最简单的IR把“编译、分析和代码生成”这条主线走通你对LLVM项目的恐惧就会减少一大半。后面再遇到API变更、新概念、新框架都是在这条主线之上不断延伸的事情。这条路的门槛不低但对喜欢编译器底层技术的人来说每一步都值得。