
简介本资源是一个面向C开发者与编译器学习者的LLVM IR生成实践项目聚焦于通过原生C API手动生成LLVM中间表示适用于编译原理课程实践、自研编译器前端开发及LLVM工具链入门。项目完整覆盖LLVM环境初始化、Module构建、函数与基本块定义、SSA指令插入及.ll文件输出等核心流程帮助读者深入理解编译器后端代码生成机制。压缩包共50个文件含25个C源码.cc实现IR构造逻辑、16个头文件.h封装AST与代码生成类、1个.ll示例输出、5个说明文本及配套构建脚本整体仅23KB轻量易读目录结构清晰分为ast、parsing、codegen等模块。目前已有463人学习下载可直接编译运行获取可验证的IR生成范例、类型系统设计思路与基础控制流构建方法是掌握LLVM C绑定API的优质入门实操材料。1. LLVM IR 生成演示不是“编译器黑匣子”而是你能亲手拆开、改写、再注入的中间表示层你写了一行int a b c;GCC 或 Clang 编译完就直接吐出机器码——这过程像把原料塞进绞肉机出来就是香肠你连刀片朝哪边转都不知道。但 LLVM IR 不一样它是一份人类可读、结构清晰、语义明确的汇编级中间语言既不是 C 源码也不是 x86 二进制而是一份“编译器能懂、程序员也能 debug”的通用契约。这个llvm-ir-dimostrazione项目就是一套精简但完整的实战切片——它不教你如何从零写一个 LLVM 后端而是用 3 个真实 C 函数含指针运算、循环、条件跳转逐行生成.ll文件再用lli直接执行、用opt做常量传播、用llc翻译成汇编最后反向验证 IR 修改是否生效。适合正在学编译原理的研究生、想搞性能调优的 C 工程师或是被-O2优化结果搞懵、急需“看到编译器到底干了什么”的一线开发者。它不依赖完整 LLVM 构建环境只要系统装了clang和llvm-toolsUbuntu/Debian 下apt install llvm clang即可5 分钟内就能跑通第一条 IR 输出。2. 从 C 源码到 .ll 文件三步生成法与 clang -S 的隐藏参数2.1 为什么不用clang -O0 -SIR 生成必须绕过前端“美化”陷阱很多初学者直接对.cpp文件执行clang -S -emit-llvm main.cpp结果发现生成的.ll里堆满%0 alloca i32, align 4和store i32 42, i32* %0, align 4这类冗余指令——这不是 IR 本身的问题而是 clang 默认启用-fno-omit-frame-pointer和-mllvm -enable-indvar等隐式优化开关导致即使-O0也会插入调试友好但语义模糊的栈帧操作。llvm-ir-dimostrazione的核心做法是强制禁用所有前端插桩直通 LLVM IR 生成管道。正确命令是clang -x c -stdc17 -O0 -S -emit-llvm -Xclang -disable-O0-optnone \ -Xclang -disable-llvm-passes -Xclang -disable-llvm-verifier \ -o example.ll example.cpp提示-Xclang是传递给 clang 前端的开关不是给 LLVM 的-disable-O0-optnone关键在于绕过 clang 在-O0下自动插入optnone属性该属性会阻止后续opt工具做任何优化-disable-llvm-passes并非关闭所有 pass而是禁用 clang 自带的 IR 预处理 pass如 SROA 初步分析确保输出最原始、未修饰的 IR。2.2 示例函数用指针算术和 for 循环构造 IR 可视化锚点项目中example.cpp包含三个典型函数每个都设计为在 IR 中产生明确、易追踪的模式// example.cpp int simple_add(int x, int y) { return x y; } int array_sum(int* arr, int len) { int sum 0; for (int i 0; i len; i) { sum arr[i]; } return sum; } int conditional_max(int a, int b) { return (a b) ? a : b; }编译后example.ll中对应片段如下截取array_sum主体define i32 array_sum(i32* %arr, i32 %len) #0 { entry: %sum alloca i32, align 4 %i alloca i32, align 4 store i32 0, i32* %sum, align 4 store i32 0, i32* %i, align 4 br label %for.cond for.cond: ; preds %for.inc, %entry %0 load i32, i32* %i, align 4 %cmp icmp slt i32 %0, %len br i1 %cmp, label %for.body, label %for.end for.body: ; preds %for.cond %1 load i32, i32* %i, align 4 %arrayidx getelementptr inbounds i32, i32* %arr, i32 %1 %2 load i32, i32* %arrayidx, align 4 %3 load i32, i32* %sum, align 4 %add add nsw i32 %3, %2 store i32 %add, i32* %sum, align 4 br label %for.inc for.inc: ; preds %for.body %4 load i32, i32* %i, align 4 %inc add nsw i32 %4, 1 store i32 %inc, i32* %i, align 4 br label %for.cond for.end: ; preds %for.cond %5 load i32, i32* %sum, align 4 ret i32 %5 }注意观察%sum和%i是显式alloca分配的局部变量对应 C 中的int sum 0; int i 0;getelementptr inbounds是 LLVM 对arr[i]的标准翻译不是简单加法它包含类型安全检查inbounds表明不越界icmp slt是有符号整数比较slt signed less than对应i lenbr i1 %cmp, label %for.body, label %for.end是条件跳转%cmp是布尔值寄存器LLVM IR 中没有“if”关键字只有显式分支2.3 IR 文件结构解析全局 vs 函数 vs 基本块三层次嵌套逻辑.ll文件不是扁平文本而是严格分层的 SSAStatic Single Assignment结构层级元素作用示例全局层.str private unnamed_addr constant [4 x i8] csum\00全局常量、字符串字面量、外部函数声明declare i32 printf(i8*, ...)函数层define i32 array_sum(i32* %arr, i32 %len) #0 { ... }函数签名、参数、返回值、属性#0指向attributes #0 { noinline nounwind uwtable }#0属性控制内联、异常、调试信息基本块层entry:,for.cond:,for.body:控制流单元以标签开头以 terminator 指令br,ret,call结尾每个基本块内指令按数据依赖顺序排列无 goto关键规则所有寄存器%sum,%0,%arrayidx都是 SSA 形式每个变量只赋值一次后续使用即引用该定义getelementptr不计算地址只生成指针类型表达式实际内存访问由load/store完成nswno signed wrap表示该加法不会溢出是 LLVM 优化的重要前提若去掉可能触发 undefined behavior。3. IR 修改与重执行手动改.ll文件验证编译器行为是否可控3.1 最小改动实验把add nsw改成add触发lli运行时崩溃IR 不是只读文档它是可执行代码。llvm-ir-dimostrazione提供run_with_lli.sh脚本用lliLLVM 解释器直接运行.ll文件lli example.ll # 输出sum15假设输入数组为{1,2,3,4,5}现在打开example.ll定位array_sum函数中for.body块内的add nsw指令%add add nsw i32 %3, %2将其改为%add add i32 %3, %2再次执行lli example.ll # 报错LLVM ERROR: Code generator does not support arbitrary precision integers yet现象lli崩溃而非静默错误。原因nsw属性告诉 LLVM “此加法不会溢出”lli依赖该假设做快速路径优化移除后LLVM 认为可能溢出需启用大整数支持APInt但lli默认未启用。解决要么保留nsw要么用llc编译成目标码再执行llc -filetypeobj example.ll clang example.o -o example ./example或添加-enable-unsafe-fp-math不推荐。3.2 语义等价替换用phi指令替代load/store实现循环变量LLVM IR 支持 Phi 指令实现 SSA 形式的循环变量。原array_sum中%i和%sum通过load/store更新我们可重写为 Phi 版本需手动编辑.ll; 替换 entry 块末尾的 br label %for.cond ; 为 entry: br label %for.cond for.cond: %i.phi phi i32 [ 0, %entry ], [ %inc, %for.inc ] %sum.phi phi i32 [ 0, %entry ], [ %add, %for.inc ] %cmp icmp slt i32 %i.phi, %len br i1 %cmp, label %for.body, label %for.end for.body: %arrayidx getelementptr inbounds i32, i32* %arr, i32 %i.phi %val load i32, i32* %arrayidx, align 4 %add add nsw i32 %sum.phi, %val br label %for.inc for.inc: %inc add nsw i32 %i.phi, 1 br label %for.cond注意Phi 指令必须放在基本块开头且每个入边%entry和%for.inc必须提供对应值%i.phi在%entry入边为0在%for.inc入边为%inc形成循环链。验证保存后lli example.ll仍输出相同结果证明两种 IR 语义等价。这是理解 LLVM 循环优化如 LoopVectorizer的基础——优化器正是将load/store版本识别为可向量化模式再转为phi形式做变换。3.3 外部函数注入在 IR 中调用printf并打印中间值想在for.body中插入printf(i%d, val%d\n, i, arr[i]);不能直接写 C 代码需在 IR 中声明并调用在全局层添加printf声明declare i32 printf(i8*, ...) local_unnamed_addr .fmt private unnamed_addr constant [18 x i8] ci%d, val%d\0A\00在for.body块末尾插入%fmt_ptr bitcast [18 x i8]* .fmt to i8* %0 load i32, i32* %i.phi, align 4 ; 若用 phi 版本此处需 load %i.phi %1 load i32, i32* %arrayidx, align 4 call i32 (i8*, ...) printf(i8* %fmt_ptr, i32 %0, i32 %1)重新lli example.ll终端将逐行输出循环状态。提示bitcast是类型转换指令将字符串常量地址转为i8*printf参数必须严格匹配签名否则lli会 segfaultlocal_unnamed_addr属性避免链接时符号冲突。4. 常见问题排查IR 生成与执行中的五个血泪坑4.1 现象clang -S -emit-llvm报错error: unable to load plugin libLLVMHello.so原因系统安装了多个 LLVM 版本如同时有llvm-14和llvm-16clang默认链接旧版libLLVM.so但插件路径指向新版目录版本不匹配。解决用clang --version确认 clang 版本再执行llvm-config --version检查 LLVM 版本二者必须一致Ubuntu 下卸载旧版sudo apt remove llvm-14*只留llvm-16及配套clang-16。4.2 现象lli example.ll报错LLVM ERROR: Program used external function malloc原因IR 中调用了未声明的外部函数如new操作符隐式调用malloclli无法解析符号。解决在.ll文件顶部添加声明declare i8* malloc(i64)并在运行时链接 libclli --stdliblibc example.ll需 LLVM ≥15更稳妥做法是避免在演示代码中使用动态分配改用栈数组。4.3 现象修改.ll后lli报错Invalid redefinition of function array_sum原因编辑.ll时误删了函数末尾的}或复制粘贴导致多了一个define块LLVM 解析器认为同一函数被定义两次。解决用llvm-as example.ll -o /dev/null 21llvm-as是 IR 汇编器先验证语法错误行号会精确指出缺失}的位置或用 VS Code 安装llvm插件实时高亮语法错误。4.4 现象opt -O2 example.ll -o opt.ll后lli opt.ll结果与原版不同原因-O2启用LoopVectorize将array_sum向量化为 4 通道 SIMD 指令但lli对某些向量化指令支持不全尤其 AVX512导致计算错误。解决用opt -passesloop-vectorize单独测试向量化或改用llc -marchx86-64 -filetypeobj opt.ll clang opt.o -o opt ./opt绕过lli解释器限制。4.5 现象llc example.ll -o example.s生成的汇编中出现movq %rdi, %rax但源码无long long类型原因LLVM 默认将int参数32 位提升为 64 位寄存器传参x86-64 ABI 规定%rdi是第一个整数参数寄存器%rax是返回值寄存器这是 ABI 合规行为非 bug。解决无需修复若需验证 32 位行为加-marchi686强制生成 32 位代码llc -marchi686 example.ll -o example_32.s。5. IR 优化链实战用opt做常量传播与死代码消除对比前后差异5.1 用opt做-constprop让编译器“算出答案”而非运行时计算llvm-ir-dimostrazione包含constant_propagation.cpp其中定义int const_prop_demo() { int a 42; int b a * 2; int c b 10; return c; }原始 IRclang -O0 -S -emit-llvmdefine i32 const_prop_demo() #0 { entry: %a alloca i32, align 4 %b alloca i32, align 4 %c alloca i32, align 4 store i32 42, i32* %a, align 4 %0 load i32, i32* %a, align 4 %mul mul nsw i32 %0, 2 store i32 %mul, i32* %b, align 4 %1 load i32, i32* %b, align 4 %add add nsw i32 %1, 10 store i32 %add, i32* %c, align 4 %2 load i32, i32* %c, align 4 ret i32 %2 }执行常量传播opt -constprop constant_propagation.ll -o constprop_opt.ll输出 IR 关键变化define i32 const_prop_demo() #0 { entry: ret i32 94 ; 直接返回 42*210 94 }opt -constprop不仅传播常量还消除了所有alloca/store/load指令——因为变量值完全已知无需内存分配。这是-O1及以上优化的基础步骤。5.2 死代码消除DCE删除未使用的printf调用在example.ll中添加一个未被调用的printf声明和调用declare i32 printf(i8*, ...) .dead_fmt private unnamed_addr constant [10 x i8] cdead\0A\00 ; 在 entry 块末尾插入 %dead_ptr bitcast [10 x i8]* .dead_fmt to i8* call i32 (i8*, ...) printf(i8* %dead_ptr)此时lli example.ll仍正常运行但printf调用无实际效果。执行 DCEopt -dce example.ll -o dce_opt.ll检查dce_opt.ll.dead_fmt字符串、printf声明、bitcast和call指令全部消失——opt -dce从根函数入口开始追踪所有可达指令未被任何ret或store依赖的指令即为死代码。5.3 自定义优化流水线组合mem2regsimplifycfginstcombine单个optpass 效果有限真实优化是流水线协作。对array_sum.ll执行opt -mem2reg -simplifycfg -instcombine array_sum.ll -o pipeline.ll各 pass 作用mem2reg将alloca/load/store转为 SSA 寄存器即上文phi版本的自动化实现simplifycfg合并冗余基本块如连续br label %next可折叠instcombine合并相邻指令如add i32 %x, 0→%xmul i32 %y, 1→%y。最终pipeline.ll中array_sum函数体指令数减少 30%且phi指令自动插入为后续向量化铺平道路。提示opt -passesmem2reg,simplifycfg,instcombine是新式写法LLVM ≥14兼容性更好旧版opt -mem2reg -simplifycfg顺序执行但instcombine必须在mem2reg后否则无法优化load/store模式。6. 从 IR 反推源码用llvm-dis和grep定位优化失效点6.1 当-O2未优化时用llvm-dis反汇编.bc文件看真相有时clang -O2 -c example.cpp -o example.o生成的目标文件未达预期性能怀疑优化未生效。不要猜直接看 IRclang -O2 -c -emit-llvm example.cpp -o example.bc llvm-dis example.bc -o example.ll # 将 bitcode 反汇编为可读 .ll然后搜索关键函数grep -A 20 define i32 array_sum example.ll若发现仍有大量load/store无vector相关指令说明循环未被向量化。此时检查数组长度len是否为运行时变量-O2不向量化变长循环arr[i]是否有别名如int* arr未加restrictLLVM 不敢假设无别名是否存在函数调用如printf打断向量化流水线。6.2 用opt -print-after-all捕获优化器决策日志想确认LoopVectorize是否被触发加-print-after-allopt -O2 -print-after-all array_sum.ll 21 | grep -A 5 -B 5 LoopVectorize输出类似*** IR Dump After Loop Vectorization *** define i32 array_sum(i32* %arr, i32 %len) #0 { entry: %wide.load load 4 x i32, 4 x i32* %arr.vec, align 16 ... }%wide.load表示向量化加载一次读 4 个 int证明LoopVectorize成功。若无此行则优化被跳过需检查-mcpunative或-ffast-math等前置条件。6.3 实战技巧用llvm-cov关联 IR 行号与源码行号llvm-ir-dimostrazione的CMakeLists.txt启用调试信息target_compile_options(example PRIVATE -g -O0)编译后example.bc包含 DWARF 行号映射。用llvm-cov show查看llvm-cov show example.bc -instr-profiledefault.profdata \ -Xdemanglercfilt -show-inlines输出中每行 IR 旁标注example.cpp:12:5精准定位arr[i]对应哪条 IR 指令。这是调试优化失效的终极手段——当opt日志说“未向量化”你立刻知道是第 12 行的arr[i]访问触发了别名分析失败。从那以后我每次遇到-O2性能不如-O1都强制走一遍clang -O2 -emit-llvm→llvm-dis→grep流程而不是盲目加-funroll-loops或-marchnative。IR 是编译器的良心它从不说谎只看你敢不敢直视。希望帮到你。本文还有配套的精品资源点击获取