ARTICLE DETAIL

资讯详情

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

LLVM项目实战:从零构建到编写自定义Pass的完整指南

LLVM项目实战:从零构建到编写自定义Pass的完整指南 作为一个常年跟编译器和工具链打交道的开发者我想先聊聊第一次拉取llvm-project代码时的真实感受那个仓库的体积和编译耗时足以劝退一大半刚入门的同学。但如果你真的愿意花一个周末去上手它你会发现这个项目几乎是整个现代编程语言生态的“工业母机”。无论是你在用的 C/C 编译器 Clang还是 Rust 的底层后端甚至各种芯片厂商的专用工具链背后都有它的身影。这篇文章我不会做那种层层叠叠的架构图解读也不会把官方文档重新抄一遍而是以一个实际使用者的视角把llvm-project这个巨无霸到底怎么用、怎么学、怎么避坑一次性讲清楚。1. 为什么说 llvm-project 是编译器世界的“工业母机”1.1 一个仓库装下了整个编译工具链生态llvm-project是 LLVM 官方维护的 monorepo单仓库多项目代码库。简单说以前 LLVM 的核心代码、Clang 前端、LLD 链接器、libc 标准库实现等是分散在不同仓库里的开发者需要分别拉取、分别配置版本匹配关系非常痛苦。现在官方把所有核心子项目统一放进一个仓库版本同步管理拉下来就是一套完整可构建的工具链。这个仓库里包含的远不止“一个编译器”这么简单。核心有这几个大件LLVM Core提供中间表示LLVM IR、优化器、目标代码生成器是整个生态的发动机。ClangC/C/Objective-C 的前端把源代码解析成 LLVM IR也是大多数人直接用到的编译器命令。LLD一个高性能的链接器链接速度比系统默认的 GNU ld 快很多。libc / libcabiC 标准库实现很多现代 C 项目尤其是跨平台项目会优先选择它。compiler-rt提供运行时支持库比如内存消毒器ASan、线程消毒器TSan的底层实现。MLIR专门用来构建编译器的编译器框架最近几年在 AI 芯片编译领域非常火。Polly多面体优化器主要在循环嵌套优化上发挥作用。这么多子项目放在一起并不是简单堆砌。它们之间存在严格的分层关系和版本绑定。比如 Clang 依赖 LLVM Core 的 IR 定义和优化接口LLD 依赖 LLVM 的对象文件处理能力。把版本绑在一起虽然仓库体积大了但至少不会再出现“Clang 10 配 LLVM 11 结果链接时报一堆诡异错误”的人为兼容性问题。1.2 monorepo 设计到底解决了什么问题很多人不理解为什么 LLVM 官方宁愿让仓库体积膨胀到几个 GB也要坚持 monorepo 策略。我个人的理解是这背后核心解决的是“变更原子性”的问题。举一个实际场景假设你想给 Clang 增加一个新的内建函数标准的流程是你需要同时修改 Clang 前端代码和 LLVM Core 里对应的内建函数定义。如果是多仓库模式你需要先提交 LLVM 的改动等合并之后再提交 Clang 的依赖改动中间一旦版本对不上构建就会断裂。而在 monorepo 里一次提交就可以同时包含两侧的改动CI持续集成用同一个 commit 构建整套工具链任何不匹配都能在合并前被立刻发现。对普通使用者来说monorepo 带来最直接的便利就是构建方式的统一。配置好 CMake 之后一条 ninja 命令就能把整套工具链全部编出来。你不需要去分别安装依赖、手工设置PATH省掉了很多环境配置的琐事。1.3 什么样的开发者和团队应该关注这个项目我个人认为以下三类人最值得花时间深入研究llvm-project第一类是编程语言设计者。如果你正在琢磨自己发明一门语言或者想给现有语言增加一个后端LLVM 是绕不开的基础设施。你只需要写好前端生成 LLVM IR剩下的优化、寄存器分配、指令调度全都不用管。第二类是芯片和嵌入式工具链工程师。每出一个新的 CPU 架构第一步就是给 LLVM 写后端支持。有了 LLVM芯片一出样片编译器就能立刻跟上不用像以前那样从头造一套编译环境。第三类是追求极致的性能优化工程师。LLVM 的优化器是开源的你可以查看每一个 pass 的实现甚至修改优化策略来适配自己的业务场景。很多大厂在发布高性能计算库时都会顺带披露自己给 LLVM 打的补丁这就是深入研究优化器的最佳素材。如果你只是写写业务代码不一定需要精通 LLVM 的每个细节但理解它的基本工作流程对你的调试能力也会有明显帮助。2. 核心子项目逐个拆解它们各自解决了什么问题2.1 LLVM Core中间表示与优化器的力量LLVM Core 是整个项目的基石它定义了一套被称为 LLVM IR 的中间语言。为什么要做中间表示因为编译器本质上是一个翻译器把人类可读的高级语言翻译成机器可读的汇编指令。这个翻译过程如果一步到位会非常僵硬因为高级语言千差万别而 CPU 指令也各有各的奇葩限制。中间表示就是两者之间的“通用语”。LLVM IR 有三个重要特征静态单赋值SSA形式、类型系统严谨、人类可读性较好。SSA 意味着每个变量只能赋值一次这让数据流分析变得异常简单。举个例子在 IR 里你不能写x x 1而必须写x2 x1 1这样分析器就能清晰地追踪每个变量的来源做常量传播和死代码消除时非常高效。优化器是 LLVM Core 里另一块重头戏。默认的优化流水线包含几十个 pass优化步骤比如内联、循环展开、向量化、全局变量合并等等。每个 pass 的作用都不相同有的负责让代码跑得更快有的负责让编译产物更小有的则是为了后续 pass 能发挥更好的效果。这些 pass 之间是有依赖顺序的顺序调错了可能导致优化效果大打折扣但编译器不会出错——这也是编译器开发中一个很微妙的设计哲学优化是尽力而为正确性必须绝对保证。2.2 Clang把 C/C 变成 IR 的桥梁Clang 是整个项目里用户接触最多的部分。你执行clang main.c -o main时实际经历了这几个阶段预处理展开宏、处理头文件、词法分析把代码切成 token、语法分析构建抽象语法树、语义分析检查类型是否正确、生成 LLVM IR、交给 LLVM Core 做优化、最后生成汇编代码和对象文件。相比传统的 GCCClang 有几个特别讨喜的特点。第一是编译速度快GCC 在某些大型 C 项目上的编译效率确实让人着急Clang 在这方面体感明显快一截。第二是报错信息友好它不仅能告诉你在哪一行出错了还会用高亮标记出具体的问题字符甚至给出修改建议。第三是模块化设计Clang 提供了一套 libclang 库和 Clangd 语言服务很多 IDE 的代码补全和跳转功能就是基于它做的。我经常给新人的第一个建议是把 GCC 的编译命令无缝迁移到 Clang 上试试看同样的代码在 Clang 下能收获什么不一样的警告信息。Clang 有很多独有的诊断项比如-Wrange-loop-analysis能检测出循环里不必要的拷贝这种警告会在你写 C 的时候帮你避开很多性能坑。2.3 LLD链接速度快到让人上瘾链接器是编译流程里经常被忽略的一环但它的性能直接影响开发体验。LLD 的目标是“链接速度比别人快一个数量级”。它在链接 Chromium 这种超大项目时能做到比 GNU ld 快 5 到 10 倍内存占用也更低。LLD 的设计思路是通过并行处理和更高效的数据结构来压缩链接耗时。传统链接器往往是单线程逐个处理对象文件LLD 则会把很多可以并行的阶段拆分例如符号解析、节区合并、重定位生成都能利用多核优势。如果你用 CMake 构建 C/C 项目只需要在 CMakeLists 里加一行set(CMAKE_EXE_LINKER_FLAGS -fuse-ldlld)就能立刻感受到链接速度的提升。而且 LLD 和 Clang 是同一套代码库维护的对 Clang 生成的产物适配性最好基本不会出现兼容性问题。2.4 libc、compiler-rt标准库与运行时安全的守护者libc 是 LLVM 项目里实现 C 标准库的部分。它设计的出发点是轻量、干净、容易移植而且对 C 新标准的支持跟进得很快。很多跨平台 C 项目会刻意选择 libc 而不是 GCC 的 libstdc主要是为了在不同操作系统上保持一致的标准库行为。compiler-rt 想关注的是一系列运行时库。最出名的包括 AddressSanitizerASan、UndefinedBehaviorSanitizerUBSan、ThreadSanitizerTSan等。这些工具在平时不会生效但当你在编译时加上-fsanitizeaddress程序里就会插入一段内存检查逻辑一旦发生越界或释放后使用它会立刻报错输出堆栈。在我实际排查内存问题的时候ASan 基本上能解决 90% 的悬案强烈建议所有 C/C 开发者在测试阶段默认开启。3. 从零构建 llvm-project完整实操与避坑指南3.1 拉取源码与依赖准备在开始之前我先说一下硬性条件。首先你需要一台内存至少 16GB 的机器磁盘剩余空间建议预留 100GB 以上。仓库本身加上构建产物体积都不小如果磁盘吃紧很容易在最后阶段因为空间不足而失败。拉取代码推荐使用浅克隆加单分支策略git clone --depth1 https://github.com/llvm/llvm-project.git--depth1只拉取最新提交能显著减少下载时间。如果你需要参与开发或者切换到特定发布分支再把深度补全即可cd llvm-project git fetch --unshallow git checkout llvmorg-18.1.8依赖方面Linux 上你需要确保以下工具已安装CMake 3.20 以上Ninja 构建系统支持 C17 的 GCC 或 ClangPython 3.8 以上测试脚本依赖zlib 和 libxml2可选部分工具会用到Ubuntu 系系统可以直接用sudo apt update sudo apt install build-essential cmake ninja-build python3 python3-pip zlib1g-dev libxml2-dev3.2 第一次构建的建议配置我强烈不建议新手直接执行cmake默认配置后再make编译整个项目。默认构建会生成包括所有工具、测试、文档在内的全部内容耗时长且容易出问题。我更推荐你先构建一个最小但可用的子集cmake -G Ninja -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DCMAKE_INSTALL_PREFIX$HOME/llvm-install这些参数的含义我来解释一下-G Ninja指定使用 Ninja 作为构建系统。Ninja 比 make 并行度更高增量编译速度也更快。-DCMAKE_BUILD_TYPERelease编译 LLVM 工具本身时开启优化。这个很关键如果使用 Debug 类型工具链本身运行会慢很多整个测试周期会被拉得很长。-DLLVM_ENABLE_PROJECTS指定要额外构建的子项目。这里只选了 clang 和 lld如果你想用 libc 或 MLIR可以继续追加用分号分隔。-DLLVM_TARGETS_TO_BUILD指定目标架构。这里选 X86 即可如果你需要 ARM 或 RISC-V 后端再加进列表。目标架构越多编译占用时间和内存都越大。-DCMAKE_INSTALL_PREFIX安装路径后续ninja install会装到这个目录方便你随时把自己的工具链放到PATH里。配置完成后执行cmake --build build -j$(nproc)这个命令会自动探测机器的 CPU 核心数并并行编译。在我的机器上16 核 32GB 内存构建 Clang 和 LLD 大约需要 30 到 40 分钟。如果你的核心数少可能需要一两小时建议预留一个比较充裕的时间窗口不要在编译过程中频繁切换任务避免产生额外的内存压力。ninja -C build install安装完成后验证一下export PATH$HOME/llvm-install/bin:$PATH clang --version如果能看到类似clang version 18.1.8的输出那你的第一套自编译工具链就算成型了。3.3 常见构建参数与使用场景对照很多时候我们并不是要把所有组件一股脑全编出来而是按需组合。我整理了一张常见场景的参数对照表方便你按图索骥使用场景关键 CMake 配置说明只想用 Clang 编译普通程序-DLLVM_ENABLE_PROJECTSclang最小可用集合开启链接优化 LTO-DLLVM_ENABLE_PROJECTSclang;lld需要 LLD 完成 LTO 链接使用 sanitizer 做内存检测-DLLVM_ENABLE_PROJECTSclang;compiler-rt提供 ASan/TSan 运行时尝试 C 标准库 libc-DLLVM_ENABLE_PROJECTSclang;libcxx;libcxxabi额外构建 libc 与 ABI 库研究编译优化与 pass-DLLVM_ENABLE_PROJECTSclang或llvm单项目即可配合 opt 工具使用做 AI 芯片编译框架-DLLVM_ENABLE_PROJECTSclang;mlir额外构建 MLIR 相关工具这些配置不是互斥的你可以把多个项目通过分号一起列出来只要磁盘和内存扛得住。3.4 构建时的硬性注意事项这块我说几个自己踩过的坑每一条都是教训。第一绝对不要用 32 位系统构建现在的 LLVM 源码在 32 位环境会出现各种匪夷所思的问题直接放弃更省心。第二CMake 配置阶段完成后不要随意移动 build 目录CMake 会记录绝对路径移动后必须重新生成。第三如果你使用了 conda 或虚拟环境注意清除CMAKE_PREFIX_PATH等环境变量它们会影响 CMake 对依赖库的查找导致找到错误版本的 zlib 或 Python 库。另外如果你在 macOS 上用 Apple Clang 作为宿主编译器去构建 LLVM请确保 Xcode Command Line Tools 已经更新到最新版本并且显式指定-DCMAKE_C_COMPILERclang -DCMAKE_CXX_COMPILERclang否则 CMake 可能会选中错误的后端编译器导致构建过程中产生一堆晦涩的报错。3.5 让 LLVM 跑在 Docker 环境里的优化思路有些开发者喜欢在 Docker 里构建 LLVM这时候需要注意镜像体积和构建效率的平衡。我个人的做法是分两阶段构建构建阶段使用完全体镜像运行阶段只保留编译好的产物。FROM ubuntu:22.04 AS builder RUN apt-get update apt-get install -y \ build-essential cmake ninja-build python3 git WORKDIR /src RUN git clone --depth1 https://github.com/llvm/llvm-project.git RUN cmake -G Ninja -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DCMAKE_INSTALL_PREFIX/opt/llvm RUN ninja -C build install FROM ubuntu:22.04 COPY --frombuilder /opt/llvm /opt/llvm ENV PATH/opt/llvm/bin:${PATH}这样最终镜像里只保留运行所需文件体积会小很多。你还可以把常用头文件和库目录挂载到镜像外面进一步缩小单次构建的输入。4. 从“会用”到“能改”手写一个 LLVM Pass 的完整过程4.1 为什么理解 LLVM Pass 是入门的关键如果你只是把 Clang 当成一个编译器来用那 LLVM 的很多精髓你都不会接触到。真正让你和这个项目产生深层连接的是修改和编写自己的优化 pass。Pass 是 LLVM 优化流水线中的基本执行单元。每个 pass 对 LLVM IR 做一次遍历和分析然后实施某种变换。举个例子有一个 pass 专门做常量传播如果你在代码里写了int a 5; int b a 3;它会分析出b其实恒等于 8于是把b直接替换成8这就是一个典型的优化。官方文档对 pass 结构的介绍比较抽象我在这里用实际代码演示一个最简单的 pass。它做的事情非常小遍历函数的每一条指令如果发现add指令也就是 IR 里的加法操作就打印一条日志。这虽然不改变代码但能帮助你理解 pass 的执行机制。4.2 环境准备与基础代码骨架首先在 llvm-project 源码树之外建立一个简单目录比如~/my-pass然后在里面创建CMakeLists.txtcmake_minimum_required(VERSION 3.20) project(MyPass) find_package(LLVM REQUIRED CONFIG) find_package(Clang REQUIRED CONFIG) message(STATUS Found LLVM ${LLVM_PACKAGE_VERSION}) message(STATUS Using LLVM include dir: ${LLVM_INCLUDE_DIRS}) add_library(MyPass MODULE MyPass.cpp) target_link_libraries(MyPass PRIVATE LLVM)接着创建MyPass.cpp#include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/IR/LegacyPassManager.h #include llvm/Pass.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { struct MyPass : public PassInfoMixinMyPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { for (BasicBlock BB : F) { for (Instruction I : BB) { if (auto *AddInst dyn_castBinaryOperator(I)) { if (AddInst-getOpcode() Instruction::Add) { errs() Find add instruction: AddInst \n; } } } } return PreservedAnalyses::all(); } }; } // namespace llvm::PassPluginLibraryInfo getMyPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, MyPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name my-pass) { FPM.addPass(MyPass()); return true; } return false; }); }}; } extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getMyPassPluginInfo(); }这段代码我是照着新版 Pass 管理器的规范写的。是不是感觉有点复杂其实核心就三块第一块定义 pass 的实际逻辑run 函数第二块定义 pass 在 PassBuilder 上的注册方式第三块导出插件入口函数。理解了这个结构你写复杂 pass 时的思路也是一样的。4.3 编译并加载插件编译这个插件需要把 LLVM 的头文件路径和库路径传给编译器和链接器。最简单的方式是复用 CMake 配置cd ~/my-pass mkdir build cd build cmake -DLLVM_DIR$(llvm-config --cmakedir) .. make编译会生成一个libMyPass.so文件。现在准备一个简单的 C 文件来测试// test.c int add(int a, int b) { return a b; }先用 clang 生成 LLVM IRclang -emit-llvm -S test.c -o test.ll然后通过opt工具加载我们的插件运行 passopt -load-pass-plugin./libMyPass.so -passesmy-pass test.ll -o /dev/null如果一切正常你会看到类似输出Find add instruction: %add add i32 %a, %b这意味着你已经成功在 LLVM 的优化流水线里插入了自己的逻辑。以后你可以在这个框架基础上做更复杂的静态分析或者尝试修改 IR 生成更优的代码。4.4 Pass 开发的前沿话题New PM vs Legacy PM我上面给出的代码是基于新版 Pass 管理器New PM的写法。这也是 LLVM 17 之后的默认方式。New PM 相比旧版的主要改进是分析的缓存机制、更好的流水线可组合性以及更清晰的 pass 生命周期管理。如果你去网上搜 LLVM pass 教程还是会搜到很多旧版写法它们会用到ModulePass或FunctionPass类并实现runOnFunction之类的接口。这种旧版接口在当前发布版里依然能编译但已经被标记为-legacy并且从 LLVM 19 开始官方明确表示不会再维护 Legacy PM 的功能演进。所以新入门的朋友别再浪费时间学旧接口了直接用 New PM 起步这样你看到的官方示例、社区新讨论、ChatGPT 新生成的内容才能对得上号。4.5 如何调试一个不生效的 Pass很多人在自己写 pass 之后发现一个奇怪的现象明明代码逻辑看起来没错但编译优化之后结果没有任何变化。这里我分享一下排查思路。首先要区分你的 pass 是分析型还是变换型。分析型 pass 只读取 IR 信息不修改 IR变换型 pass 才会修改 IR。如果你只是打印信息IR 肯定不变这很正常。其次要检查 pass 在流水线里的执行顺序。LLVM 的优化流水线是有阶段划分的某些 pass 需要在特定 pass 之后运行才能获得更准确的信息。如果你把 pass 加在流水线最前面可能很多优化还没开始IR 里能观察到的表达式形态和你预想的不一致。最后是验证手段。你用opt加载插件时可以先不指定-O2等优化级别单纯跑自己的 pass 观察输出。然后再叠加默认优化流水线看看两者输出有什么差异。这样能帮你判断问题出在你的 pass 逻辑上还是流水线顺序上。5. 开发与使用中的高频问题排查实录5.1 编译慢、磁盘占用大、内存告急怎么办LLVM 的编译资源消耗是出了名的大这里给出几个实操性的优化手段。第一个是开启 ccache。ccache 是编译缓存工具第一次全量构建后再次构建时如果源码和编译选项没变它会直接使用缓存结果速度提升非常明显。配置方式是在 CMake 里指定-DLLVM_CCACHE_BUILDON第二个是用 ThinLTO 优化编译器自身的构建。这个听起来有点绕大意是用 LLVM 自己的链接优化技术来加速构建它自己。在 CMake 里加-DLLVM_ENABLE_LTOThin但注意开启 LTO 构建 LLVM 会显著增加链接期内存消耗如果你的机器只有 16GB 内存可能会更慢而非更快。第三个是减少目标架构。尽量把-DLLVM_TARGETS_TO_BUILD设置成你实际需要的最小集合。比如你只做 x86 开发就不要默认构建 ARM、AArch64、RISC-V 等后端这样能少编三分之一左右的代码。5.2 版本升级后接口变化导致编译错误LLVM 的 API 变化频率很高大版本之间经常有 breaking change。最典型的就是 Pass 接口的变来变去以及一些工具链参数的调整。如果你在编译一个第三方项目时报出一堆关于 LLVM 头文件的错误大概率是版本不匹配。排查方法很简单先用llvm-config --version确认当前环境版本再查看项目文档要求的 LLVM 版本。如果项目很老建议不要硬刚新版 LLVM直接找一个对应的旧发行版分支来用。这里是典型的大版本匹配建议项目要求推荐 LLVM 版本分支2023 年之前的旧项目llvmorg-14.0.0 或 15.0.02024 年代码库llvmorg-17.0.0 或 18.1.8当前最新llvmorg-19.1.x这里我还想强调一点GitHub 上的main分支是开发版接口每天都在变千万不要拿它作为稳定开发的基线。5.3 调试信息不生效或符号丢失有时候你用 Clang 编译程序后进 GDB 调试发现断点打不上或者变量显示成optimized out。这通常是因为你在编译命令里没加-g或者优化级别开得太高。我常用的调试版编译命令是clang -g -O0 -fno-omit-frame-pointer main.c -o main-O0会关闭所有优化方便单步调试-fno-omit-frame-pointer会保留栈帧指针让 GDB 的堆栈回溯更准确。如果你确实需要在开启优化的情况下调试至少把-g加进去并且考虑使用-fno-inline关闭内联否则你会在调试时遇到大量“代码被内联到别处”的困扰。5.4 链接时找不到符号的常见场景分析链接错误算是高频问题尤其是刚切换到 LLD 或 LTO 模式时。最常见的是undefined reference to这类错误多数原因是你要链接的库是用 GCC 的 ABI 编译的而当前使用的是 Clang 和 libc标准库的符号命名空间不一致。遇到这种情况优先确认你链接的库是不是同一套 ABI 编译的。可以在链接命令里加上-stdliblibstdc或-stdliblibc强制指定使用的标准库保持所有对象文件一致。另外如果出现大量的重复符号错误可以检查是否在编译多个源文件时都包含了同一个头文件的实现部分比如把模板特化写在了头文件里。LLD 对重复符号的处理比 GNU ld 更严格它会直接报错而 GNU ld 可能只是警告然后跳过。这并不是 LLD 的 bug而是设计如此反过来会逼着你把代码写得更干净。5.5 TableGen 报错的本质与应对TableGen 是 LLVM 里用来描述指令集、寄存器、调用约定等信息的领域特定语言。LLVM 后端开发中你改一个.td文件就会触发 TableGen 自动生成一堆 C 代码。如果你在构建时报错说tablegen失败通常不是 TableGen 工具本身出问题而是你的.td描述里有语法错误或者某个类的继承关系写错了。这时候报错信息一般会给出具体的文件和行号直接定位修改即可。有一点要注意的是如果你改了.td文件构建系统会自动重新运行 TableGen 并生成新代码但生成的代码存放在 build 目录里不在源码目录。所以当你搜索相关符号时要去 build 目录里的生成文件里找不要花时间去源码目录里翻否则会找不到。6. 学习路径建议从入门到能独立开发工具链前面讲了那么多很多人可能会有点不知所措不知道从哪里下手。根据我带过不少新人的经验我梳理出一条循序渐进的路线。第一步先当一个“用户”。用 Clang 编译自己的 C/C 项目尝试开启-O2、-O3、LTO观察性能变化使用-Wall -Wextra查看警告学会阅读 Clang 的诊断信息。这个阶段不需要碰 LLVM 源码只需对编译流程有一个感性认识。第二步做一个“读者”。浏览 LLVM 官方的llvm/examples目录里面有非常经典的可执行示例比如 BrainFuck、Fibonacci、Kaleidoscope 教程。Kaleidoscope 教程会带你实现一门完整的小语言从词法分析到代码生成特别适合理解 LLVM IR 的生成过程。我建议手抄一遍而不是直接拷贝代码能收获完全不同的理解深度。第三步尝试“写工具”。仿照官方 Example 里的模块写一个能读取 IR 文件、遍历函数并统计指令数的分析工具。这个阶段能让你熟练使用FunctionPass或 New PM 的插件机制把 IR 的数据结构摸清楚。第四步深入到 Clang 前端。试着写几个 Clang 插件或者使用 Clang 的-ast-dump参数查看抽象语法树。当你理解 AST 结构之后再去看 Clang 如何把 AST 转换成 IR你对整个编译管的认知就会形成闭环。最后一步才是做真正的后端开发或专项优化。这个阶段已经是专家级别的活通常需要结合具体的芯片架构或业务场景。我建议在这个阶段多关注 LLVM 的官方博客和邮件列表紧跟社区动态因为你遇到的问题很可能别人已经讨论过了。如果你是学生或者刚转行我特别建议多参加 LLVM 社区的一些开源实习项目比如 LLVM 的 GSoC 项目或者国内一些高校参与的编排优化比赛。这类活动通常会有经验丰富的导师指导能在短时间内极大提升你的编译器能力。就我个人经验而言llvm-project这个仓库就像一座巨大的矿山初看时觉得无从下手但一旦找到自己感兴趣的方向每一次挖下去都会有不小收获。它的代码风格干净、模块划分清晰、文档扎实即便你不打算成为编译器开发者单纯作为工程标杆来读也能学到非常多系统设计和性能优化的思路。希望这篇文章能帮你少踩一些我当年踩过的坑更顺利地走上 LLVM 的探索之路。
返回列表