
简介北航计算机学院2017级编译原理课程设计项目实现C0语言到MIPS汇编的编译器面向计算机专业本科阶段学习编译原理的学生可作为课程设计或实验教学的完整参考。资源共33个文件包括19个cpp源文件、8个h头文件、3个txt文本、2个docx文档和1个md说明压缩包仅81KB代码结构清晰涵盖词法分析、语法分析、语义分析、中间代码生成、优化及MIPS目标代码生成等核心模块。输入文件testfile.txt存放C0源代码输出mips.txt为生成的目标汇编并附赠2019年文法、说明文档及额外教学素材。已有61人学习适合有C语言基础、希望深入理解编译器各阶段实现细节的读者。通过阅读源码与运行调试可直观掌握从高级语言到汇编的完整转换流程也能为后续编译优化、寄存器分配等进阶内容提供实践基础。1. 编译原理课设这份 C0 到 MIPS 的完整编译器源码能省你大半周编译原理课程设计最劝退的地方在于它不是一道算法题而是一条必须端到端跑通的流水线。北航计算机学院 2017 级这套项目把 C0 语言到 MIPS 汇编的编译器完整落地成了源码输入 testfile.txt输出 mips.txt词法分析、语法分析、语义分析、中间代码、优化、目标代码生成一个不少。对正在做同类课设的人来说它最大的价值不是能抄而是每个阶段的文件边界都划得清清楚楚你能看到一份合格教学编译器该有的模块长什么样。它能解决的具体问题很实在词法分析器状态机怎么组织、四元式怎么设计、寄存器分配在课设层面做到什么程度算够源码里都有对应实现。适合三类人卡在某个编译阶段的学生、需要给实验课找参考实现的助教、想快速看懂一个编译器整体结构的自学者。下面按拆文件→跑起来→看优化→避坑→改造的顺序过一遍顺带记下我重编它时踩过的坑。2. 把源码包拆开看文件职责、编译流水线与两份 MIPS 生成器2.1 模块边界先从文件名读出来六个阶段对应哪些文件拿到源码先别急着编译把文件名当目录看。这个包的文件命名是教学项目里少见的规整基本按照编译器经典阶段一对一命名我把关键文件按职责列在下面。文件阶段/职责word_analyze.cpp词法分析识别关键字、标识符、常数、运算符并输出 token 流grammar_analyze.cpp语法分析按文法做递归下降生成语法结构并做语义检查symbolTable.h / symbol.cpp符号表变量与函数的作用域登记、类型和种类管理error.h / error.cpp错误处理统一错误码与错误信息输出tempCode.h / tempCode.cpp中间代码生成四元式序列regenerate_quas.cpp四元式回填处理跳转目标的回落地址blockdivide.cpp基本块划分为局部优化做准备dag.cppDAG 优化公共子表达式消除active_analyze.cpp活跃变量分析算每个变量在每条指令处的活跃区间temp_reg.cpp / register_allocate.cpp临时寄存器分配与正式寄存器分配constoptimize.cpp / optimize.cpp / optimize.h常量折叠与优化总控mips_generate.cpp基础版 MIPS 目标代码生成optimized_mips_generate.cpp优化版 MIPS 目标代码生成type.h / type.cpp / gtype.h / heads.h类型定义、全局类型与公共头文件这样拆分的逻辑和教材章节是严格对应的词法、语法在前中间代码居中优化和目标代码收尾。你如果自己写课设卡在我该建哪些文件照着这个清单规划工程结构至少不会在中期检查时被问倒。heads.h 和 gtype.h 这种公共头通常放全局常量、枚举定义和跨文件可见的函数声明先看这两个文件能快速建立全局认知。2.2 四元式是中间枢纽tempCode 的数据结构决定后面所有阶段整个工程里最值得先读的是 tempCode.h因为从语法分析结束之后词法、语法阶段的产物都会收敛成中间代码后面优化、寄存器分配、MIPS 生成全都在这个结构上做文章。// tempCode.h 中四元式结构的典型形态 struct Quaternion { int op; // 操作码, -, *, /, , jmp, jz, call, ret... int arg1, arg2, result; // 三个操作数用符号表编号或临时变量编号表示 };这里的 op 是一个操作码枚举arg1、arg2、result 不直接存变量名而是存符号表里的编号这样在优化阶段做活跃变量分析、寄存器分配时只需要比较整数编号不需要反复做字符串匹配。这也是教学编译器里最常见的四元式设计把名字留在符号表里中间代码只碰编号换名不改四元式。regenerate_quas.cpp 的存在说明这套工程处理了跳转地址回填。if、while 这类语句在递归下降时条件分支的目标地址是后知后觉的——语法树没走完你并不知道 else 或循环体结束在哪。常规做法是先留一个空位等收尾时统一回填regenerate_quas 就是干这个的。调试时如果发现 MIPS 输出里 label 乱跳问题大概率出在这一步。2.3 两份 MIPS 生成器一份兜底、一份冲优化分包里同时有 mips_generate.cpp 和 optimized_mips_generate.cpp这是很多课设拿高分的标准姿势先写一个能把四元式一对一翻译成 MIPS 的朴素版本兜底保证评测能过再写一个结合寄存器分配、减少访存指令的优化版冲分。两个版本的区别从输出就能看出来。对比项基础版优化版变量访问每个变量都从栈/内存 load、store活跃变量尽量驻留寄存器临时值每算一步就 spill 到栈用临时寄存器暂存减少访存目标代码量较长、冗余明显明显更短风险正确性直观好调试寄存器分配边界容易出错还有一个文件值得单独说optimized_mips_generatearm.cpp。它把同一套优化生成逻辑移植到了 ARM 指令集上。MIPS 是 RISCARM 也是 RISC两者的寄存器约定、访存指令有差异但结构相似这个文件等于给了你一份同一算法跨指令集移植的活样例。如果你课设要求不限定 MIPS或者想演示自己对指令集的理解直接拿它对比 MIPS 版和 ARM 版的翻译差异就行。3. 把工程跑起来编译命令、main 的执行链与输入输出约定3.1 编译工程本身一条 g 命令把全部 .cpp 链接成可执行文件这套源码是声明与定义分离的写法头文件只放声明实现在各个 .cpp 里所以不能单编 main.cpp必须让所有实现参与链接。# 在 Compiler-master 目录下把全部 .cpp 一次性编译链接 g -stdc11 -O0 -o compiler *.cpp # 如果编译器不支持通配符展开用显式列出的方式 g -stdc11 -O0 -o compiler \ main.cpp word_analyze.cpp grammar_analyze.cpp symbol.cpp error.cpp \ tempCode.cpp regenerate_quas.cpp blockdivide.cpp dag.cpp \ active_analyze.cpp register_allocate.cpp temp_reg.cpp \ constoptimize.cpp optimize.cpp mips_generate.cpp func_insert.cpp注意我特意加了 -O0。编译这个编译器工程时不建议开 -O2因为教学代码里偶尔会有依赖未定义行为的地方优化器一介入可能改变你排查问题的路径。想要确认编译是否成功先看有没有未声明的标识符和重复定义两类报错前者说明某个 .cpp 没参与编译后者说明 optimize.cpp 和 constoptimize.cpp 里存在同名全局函数需要去 optimize.h 统一声明。链接产物名为 compiler运行时不带参数它按固定文件名读写。3.2 main.cpp 的执行链testfile.txt 到 mips.txt 的每一站main.cpp 是整个工程的调度中心流程几乎是教科书六阶段的直线实现。int main() { // 第 1 步读取 testfile.txt按行切分源码 vectorstring src readFile(testfile.txt); // 第 2 步词法分析得到 token 流 auto tokens wordAnalyze(src); // 第 3 步语法分析 语义检查过程中填符号表 auto tree grammarAnalyze(tokens); // 第 4 步从语法结构生成四元式中间代码 auto quats generateTempCode(tree); // 第 5 步优化阶段按开关决定是否走 dag / 常量折叠 if (optimizeSwitchOn) { optimizeQuats(quats); } // 第 6 步目标代码生成 string mips generateMips(quats); // 第 7 步写出 mips.txt writeFile(mips.txt, mips); return 0; }每一站的输入输出都很清楚词法分析消费源码行产出 token语法分析消费 token产出语法树并顺带维护符号表中间代码生成消费语法树产出四元式目标代码生成消费四元式产出 MIPS 汇编文本。调试时你可以在第 5 步前后各加一个 dump 函数把四元式序列打到标准输出这一招能解决后面一半的疑难杂症——编译器是个黑匣子但中间代码就是它的内部日志。3.3 输入输出约定与 2019 文法文档评测脚本只认这两个文件名课设项目最大的隐性规则是文件名固定源代码输入必须是 testfile.txt目标代码输出必须是 mips.txt评测脚本按这个约定抓文件。自己玩的时候随意提交评测前一定要确认这两个名字一字不差大小写也别错否则分直接白给。至于 2019年文法.docx 和说明文件.txt它们是这个工程的上层契约。文法文档定义了 C0 语言的教学子集函数定义、变量声明、赋值、if、while、return、算术与比较表达式是核心内容具体边界以文档里的产生式为准。我的建议是先把文法文档里每一条产生式和 grammar_analyze.cpp 里的函数逐一对应看一遍比如看到if-statement :: if ( expr ) statement就去代码里找对应的递归下降函数。这样既能把抽象文法落到具体实现也能在答辩时准确说出我的语法分析支持文法文档中第几条产生式。4. 优化链路怎么做基本块划分、DAG、活跃变量与寄存器分配4.1 blockdivide dag先把局部优化做扎实优化不是一步到位的大改造而是分层的。最底层是局部优化它只在基本块内部做文章。blockdivide.cpp 做的就是切基本块遇到函数入口、跳转指令、跳转目标标号处切一刀把四元式序列切成一段段内部顺序执行、外部只有首尾相连的基本块。// blockdivide.cpp 的基本切块逻辑示意 vectorBlock divide(QuatList quats) { vectorBlock blocks; Block cur; for (int i 0; i quats.size(); i) { // 把当前指令加入正在累积的基本块 cur.push(quats[i]); // 当前指令是跳转类jmp/jz基本块到此结束 if (isBranch(quats[i])) { blocks.push_back(cur); cur.clear(); } // 当前指令是跳转目标标号说明上一块已结束新块从这里开始 else if (isLabel(quats[i])) { blocks.push_back(cur); cur.clear(); cur.push(quats[i]); } } return blocks; }切分规则就两条跳转指令是一条块的结尾标号是一条块的开头。切完之后dag.cpp 在每个基本块内部构建 DAG 节点——叶子节点是常量和变量内部节点是运算。DAG 的意义在于公共子表达式消除如果块内两个 ab 用的是同样的操作数DAG 里只会有一个加法节点第二次引用直接复用结果。这在 C0 这类教学语言里未必能省很多指令但它是后续优化概念的入口答辩时讲清楚 DAG 怎么建、怎么消重比堆一堆花哨优化更有说服力。4.2 active_analyze register_allocate活跃变量驱动寄存器分配寄存器分配是优化链路里最容易翻车的环节。active_analyze.cpp 做的事情是算出每条指令执行前哪些变量是活跃的——所谓活跃就是在这个执行点之后还会被读取。一个变量一旦不再活跃它占的寄存器就可以被释放给别的变量这就是寄存器分配的底层依据。// active_analyze.cpp 中活跃变量计算的抽象流程 // 从函数末尾倒着装填活跃信息 for (int i quats.size() - 1; i 0; i--) { // 当前指令 result 被定义从活跃集移除 liveOut liveIn; // 当前指令 arg1、arg2 被引用加入活跃集 liveIn.remove(quats[i].result); liveIn.add(quats[i].arg1); liveIn.add(quats[i].arg2); }注意活跃变量分析是逆向的从函数结尾往前算。register_allocate.cpp 拿到活跃区间后给每个变量分配寄存器核心约束是活跃区间重叠的变量不能共用同一个寄存器。temp_reg.cpp 负责管理那些计算过程中临时产生的、不需要跨语句存活的值它们用完即弃是优化版输出能大幅减少访存指令的关键。如果你的优化版生成结果在函数调用附近出错九成是活跃变量分析没有正确处理调用指令覆盖 caller-saved 寄存器的问题这一点在后面避坑章节会细说。4.3 constoptimize optimize常量折叠与优化总控constoptimize.cpp 做的是常量折叠在编译期把能算的算掉别拖到运行时。比如四元式里有一条 a 2 * 3直接替换成 a 6省一条乘法指令。教学语言里常量表达式不多但这类优化实现简单、正确性容易验证是课设报告里性价比很高的一个点。optimize.h 是优化总控开关我一般建议把它设计成编译期可配置的选项。课设答辩时老师很可能问你的优化开没开、开哪些、为什么。如果你能现场演示同一条 testfile.txt 在基础版和优化版下的不同输出并解释差异来自基本块划分、DAG 还是活跃变量驱动的寄存器分配这比写满一页纸的优化名词更能证明你真正跑通了全链路。另外优化开关做得越细越容易做回归测试——每个优化功能单独开关出问题能快速定位到具体阶段。5. 避坑排查教学编译器最常见的五个翻车现场5.1 编译工程与输入输出先让工程本身跑起来现象g 编译源码包时报 error: xxx was not declared或者链接阶段一堆重复定义单独编译 main.cpp 必挂。原因这套工程是声明和实现分离的写法头文件里只有声明实现散落在各 .cpp。单编一个文件看不到全部实现全量编译时如果 optimize.cpp 和 constoptimize.cpp 里各自定义了同名全局函数链接就会冲突。解决固定用g -stdc11 -O0 -o compiler *.cpp全量编译。如果出现重复定义去 optimize.h 看函数声明确认实现只保留一份另一个文件改为调用。工程跑不通之前不要碰任何优化功能的调试这一步不稳后面全是连锁问题。现象运行后 mips.txt 为空或者只有 .data 段没有 .text 段更典型的是提示未包含 main 类型。原因C0 是教学子集评测时源文件必须含 main 函数作为入口。语法分析阶段如果没有对函数名做特殊处理把 main 当普通函数登记后续目标代码生成就找不到入口地址。解决先确认 testfile.txt 里确实写了 main再看 grammar_analyze.cpp 对函数定义的处理给符号表条目加一个 isMain 标记目标代码生成时以它作为 .text 段的起点。别小看这个问题做课设时它是最先拦路的坎。5.2 语法、语义与符号表最容易埋雷的三层现象局部变量和全局变量同名函数内改局部变量全局变量跟着变或者反过来函数内读到的全是全局变量的值。原因符号表作用域没有分层。教学编译器最常见的偷懒写法是拿一个线性表存所有符号同名变量直接覆盖旧条目作用域一嵌套就错乱。解决检查 symbolTable.h按作用域栈来组织进入函数压一层退出函数弹一层查符号从顶层往下逐层找。这是语义分析的基础也是一份课设能否过中期检查的分界线。做完这步再测同名变量错误会立刻消失。现象带 if、while 的 testfile 跑出来 label 重复或者跳转目标整体对不上MIPS 输出在模拟器里乱跳。原因条件跳转和循环跳转的目标地址是后知后觉的语法树没走完不知道跳哪。regenerate_quas.cpp 负责回填回填时机不对就会把 label 打错位置甚至重复。解决在中间代码生成之后加一个 dump打印全部四元式对照 2019 文法文档里 if、while 的产生式查跳转编号。常见的正确姿势是每个跳转四元式先留一个占位用 label 队列记录待回填的指令下标收尾时统一填充实际目标。打印中间代码这一步是解决这类问题最快的路径。5.3 优化与目标代码开优化后结果不一致现象同一份 testfile.txt基础版 mips_generate.cpp 的输出在 SPIM 里能跑对optimized_mips_generate.cpp 的输出结果却是乱数。原因寄存器分配在函数调用边界没有保存 caller-saved 寄存器。MIPS 约定调用者负责保存 $t0-$t9 这类寄存器函数调用发生时这些寄存器里的值可能被被调函数写坏。活跃变量分析如果没把调用指令当作杀死这些寄存器的边界分配器就会让跨越调用点的变量继续住在这些寄存器里结果自然错乱。解决查 active_analyze.cpp 对 call 指令的处理遇到函数调用时把 caller-saved 寄存器对应的活跃变量全部标记为不活跃或强制 spill。你的调试习惯应该是先把基础版输出和优化版输出 diff 一下定位到第一个不一致的四元式然后顺着寄存器分配过程看那个操作数被分到了哪个寄存器、跨没跨函数调用边界。这类问题光看代码很难一眼看穿但 diff 一缩范围往往几分钟就能锁定。6. 把它改造成你自己的课设MIPS 验证方法与语法扩展链路6.1 用 SPIM 或 MARS 验证 mips.txt 能不能真跑生成 mips.txt 只是第一步课设最怕的是编译器输出看起来像 MIPS实际一跑就崩。验证方法我用两条命令# SPIM 命令行加载并运行 spim -file mips.txt # MARS 无界面模式跑完全程序 java -jar mars.jar nc mips.txtMARS 的几个关键参数nc 表示无图形界面适合脚本化批量验证db 进入调试器可以单步看寄存器变化a 表示显示指令地址。我的验证习惯是先跑无界面模式确认程序不崩再用调试器单步跟踪第一次出现异常的指令。对照 C0 源码检查 $v0 寄存器的最终值以及 lw/sw 访存指令的偏移量是否落在数据段范围内这是最快发现目标代码逻辑错误的方法。从头到尾跑通一遍的样例至少要覆盖三类赋值与表达式、if/while 分支、函数调用光有 hello world 级别的 testfile 说明不了问题。6.2 从 2019 文法文档加一个新语法点的完整链路最后说怎么把这份资源变成你自己的课设。扩展新语法点的流程是固定的六步先在 2019 文法文档里确认新语法对应的产生式再在 word_analyze.cpp 加关键字和运算符的词法识别然后在 grammar_analyze.cpp 加对应产生式的递归下降处理接着在 tempCode 里加新的四元式操作码随后在 mips_generate.cpp 和 optimized_mips_generate.cpp 里分别加目标代码翻译最后写一个最小 testfile 做回归。拿加 for 循环举例文法产生式是 for (初始化; 条件; 步进) 语句。词法阶段加 for 关键字语法阶段按普通循环处理生成三块四元式——初始化块、条件判断块、步进块步进块末尾无条件跳回条件判断块的开头。MIPS 端用 addi 实现步进、beq/bne 实现条件判断、j 实现回跳。加完之后基础版和优化版都要用同一个 testfile 验证防止优化链路只改了一半。从那以后我每拿到一份课设源码第一件事不是通读代码而是先把输入输出两个文件建好、把工程跑通、再打印中间四元式。这三件事做完编译器的黑匣子基本就拆开了一半。这套 C0 到 MIPS 的源码把六个阶段完整串了下来你把代码、文法文档、两份生成器对照着过一遍编译原理就不再是背书而是看得见摸得着的系统。希望帮你少走点弯路也希望你改出来的版本能在答辩时讲得比这份源码更透。本文还有配套的精品资源点击获取