免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于C语言的MIPS编译器实现:从lex/yacc到中间代码优化与Python后端

基于C语言的MIPS编译器实现:从lex/yacc到中间代码优化与Python后端 简介这份资源是一套面向计算机专业学生与编译原理学习者的C语言编译器课程设计完整实现围绕词法分析、语法分析、中间代码生成与优化、目标代码生成等核心环节展开适合正在做课程设计或希望深入理解编译流程的读者参考。压缩包共54个文件约5.1MB包含cpp与h源码、c与l词法文件、y语法文件、obj与pdb编译产物、asm汇编码、py脚本及txt说明文档等覆盖从源码到可执行文件的完整工程结构。项目借助lex与yacc完成词法分析与语法分析并生成语法树用C解析语法树、生成中间代码并实现错误检测与优化再通过Python处理中间代码生成MIPS汇编码最终可在PCSpim模拟器上运行。目前已有505人学习下载读者可从中获取编译器各阶段的实现思路、模块划分方式与调试排错经验对理解编译原理与完成同类课程设计具有较高参考价值。1. 从一份能跑通 MIPS 的 C 编译器说起它到底能解决什么很多人做编译原理课程设计卡在最后一步语法树建出来了中间代码也生成了但就是跑不起来或者跑起来结果不对查半天查不出问题在哪。这份基于 C 语言的编译器资源包恰好把整条链路走通了——从 lex 词法分析、yacc 语法分析生成语法树到 C 解析语法树生成中间代码中间还带错误检测再做中间代码优化最后用 Python 把中间代码翻译成 MIPS 汇编码能在 PCSpim 上直接跑出结果。它适合正在做编译原理课程设计的学生也适合想搞清楚「一个能跑的编译器到底由哪些模块拼起来」的开发者。资源包里既有 lex.yy.c、y.tab.c 这类工具生成文件也有 Praser.cpp、innerCode.cpp、codeOptimize.cpp 这些手写核心模块还有 compiler.exe 和 makefile.bat拿到手就能编译运行不用从零搭架子。2. 编译链路拆解lex/yacc 前端与 C 中端的衔接方式2.1 词法与语法分析lex.yy.c 和 y.tab.c 是怎么被组织进来的这份资源的编译前端走的是经典组合lex 负责词法yacc 负责语法。资源包里能看到 lex.yy.c、y.tab.c、y.tab.h、y.output、compiler.y 这几个文件说明作者是把 lex 和 yacc 的源文件都保留了下来而不是只丢一个生成结果。compiler.y 是 yacc 的语法规则文件里面定义了文法产生式和语义动作y.output 是 yacc 生成的冲突报告做课程设计时这个文件很关键能帮你定位移进/归约冲突。常见做法是先写 .l 文件描述词法规则用 flex 生成 lex.yy.c再写 .y 文件描述语法规则用 bison 生成 y.tab.c 和 y.tab.h。资源包里直接给了 lex.yy.c 和 y.tab.c说明作者已经把生成步骤跑完了你拿到手可以先用 makefile.bat 或 cCompiler.sln 直接编译再回头改 compiler.y 重新生成。# 如果你要重新生成词法分析器假设词法文件叫 compiler.l flex -o lex.yy.c compiler.l # 重新生成语法分析器 bison -d -o y.tab.c compiler.y # -d 表示同时生成 y.tab.h供其他模块引用 token 定义这两条命令的含义flex 把 .l 规则文件转成 C 源码 lex.yy.cbison 把 .y 文法文件转成 y.tab.c 和头文件 y.tab.h。参数 -d 不能省否则 y.tab.h 不会生成Praser.cpp 里引用 token 枚举时会报未定义。y.output 是加 -v 参数生成的用来查看文法冲突课程设计答辩时如果被问到「有没有冲突」这个文件就是证据。2.2 语法树与符号表tree.cpp、block.cpp 的职责划分资源包里有 tree.h、tree.cpp、block.h、block.cpp这几个文件负责语法树节点和作用域块的管理。tree.cpp 里通常定义 AST 节点的结构比如表达式节点、语句节点、声明节点block.cpp 负责作用域栈处理变量声明和查找。Praser.cpp 和 Praser.h 是语法树解析的主体把 yacc 归约出来的语法树进一步转换成中间代码。我一般会这样组织tree.cpp 只放节点构造和遍历接口block.cpp 管符号表的压栈弹栈Praser.cpp 调用两者完成语义检查。这样分工的好处是后面加类型检查或作用域检查时不用动语法树结构本身。// tree.h 里常见的节点定义方式示意 struct TreeNode { int nodeType; // 节点类型表达式、语句、声明 std::string value; // 标识符或字面量 TreeNode* left; TreeNode* right; TreeNode* next; // 语句链表用 };这段结构体定义里nodeType 用来区分节点种类value 存标识符名字或常量值left/right 挂子节点next 用于把多条语句串成链表。参数说明nodeType 一般用枚举而不是魔法数字方便调试时打印value 用 std::string 而不是 char*避免手动管理内存。如果你要扩展比如加数组下标可以在 TreeNode 里再加一个 index 字段。2.3 中间代码生成与错误检测innerCode.cpp 的核心逻辑innerCode.cpp 和 innerCode.h 负责把语法树转成中间代码资源包里还有 innerCode.txt 和 inter.txt应该是中间代码的输出样例。摘要里提到「生成中间代码的过程中实现了错误检测」这意味着在遍历语法树时遇到未声明变量、类型不匹配、重复定义等情况会报错并记录。常见做法是遍历 AST 时维护一个符号表遇到变量引用先查表查不到就报「未声明」遇到赋值检查左右类型是否一致遇到函数调用检查参数个数。错误信息可以输出到 innerCode.txt 或控制台方便定位。// 中间代码生成时的错误检测片段示意 void genInnerCode(TreeNode* node) { if (node-nodeType NODE_ID) { Symbol* sym lookupSymbol(node-value); if (sym nullptr) { errorList.push_back(未声明变量: node-value); return; } emit(LOAD, sym-name); } // 其他节点类型处理... }这段代码的逻辑遇到标识符节点时先去符号表查查不到就记录错误并返回避免后续生成错误代码。参数说明errorList 是一个全局错误列表最后统一输出emit 是生成中间代码指令的函数第一个参数是指令名后面是操作数。注意这里 return 之后不再处理子节点防止错误扩散。3. 中间代码优化与 MIPS 后端从 codeOptimize.cpp 到 objectcode.py3.1 优化模块codeOptimize.cpp 做了哪些事codeOptimize.cpp 和 codeOptimize.h 是优化模块。课程设计里常见的优化包括常量折叠、公共子表达式消除、死代码删除。资源包里没有详细说明优化级别但从文件命名看至少有一个独立的优化 pass。我一般会先做常量折叠再做死代码删除因为常量折叠后可能产生新的死代码。比如a 3 4;折叠成a 7;如果 a 后面没被用到死代码删除就能把它干掉。// 常量折叠示意 TreeNode* foldConstants(TreeNode* node) { if (node-nodeType NODE_ADD) { TreeNode* l foldConstants(node-left); TreeNode* r foldConstants(node-right); if (l-nodeType NODE_CONST r-nodeType NODE_CONST) { int val std::stoi(l-value) std::stoi(r-value); return makeConstNode(std::to_string(val)); } } return node; }这段代码递归折叠左右子树如果两边都是常量节点就计算出结果并返回一个新的常量节点。参数说明NODE_ADD 是加法节点类型NODE_CONST 是常量节点类型makeConstNode 是构造常量节点的工厂函数。注意递归顺序是先处理子节点再判断当前节点否则嵌套表达式折不全。3.2 Python 后端objectcode.py 如何把中间代码翻译成 MIPSobjectcode.py 是整条链路的最后一环读入中间代码可能是 inter.txt 或 innerCode.txt输出 result.asm。摘要里说「利用 python 对中间代码进行处理并生成 mips 汇编码并且可以成功在 PCSpim 上运行」说明这个脚本是独立于 C 部分的用 Python 做后端翻译。常见做法是逐行读中间代码按指令类型映射到 MIPS 指令。比如中间代码的LOAD x翻译成lw $t0, xADD翻译成add。寄存器分配可以用简单的栈式分配所有临时变量放栈上用 $sp 和 $fp 管理。# objectcode.py 核心翻译逻辑示意 def translate(inner_code_lines): asm [] for line in inner_code_lines: parts line.strip().split() if not parts: continue op parts[0] if op LOAD: var parts[1] asm.append(flw $t0, {var}) elif op ADD: asm.append(add $t0, $t0, $t1) elif op STORE: var parts[1] asm.append(fsw $t0, {var}) return asm这段脚本的逻辑按行解析中间代码根据操作码生成对应 MIPS 指令。参数说明inner_code_lines 是中间代码行列表asm 是输出的汇编行列表。注意这里只是示意实际寄存器分配要复杂得多但课程设计级别用这种简单映射就能跑通。生成 result.asm 后用 PCSpim 打开就能运行。3.3 构建与运行makefile.bat 和 cCompiler.sln 怎么选资源包里同时有 makefile.bat 和 cCompiler.sln说明作者提供了两种构建方式。makefile.bat 适合命令行快速编译cCompiler.sln 适合用 Visual Studio 打开调试。如果你在 Windows 上直接双击 makefile.bat 或者用 VS 打开 sln 都行。# makefile.bat 里常见的编译命令 cl /EHsc /Fe:compiler.exe lex.yy.c y.tab.c Praser.cpp innerCode.cpp codeOptimize.cpp tools.cpp tree.cpp block.cpp这条命令用 MSVC 的 cl 编译器把所有源文件编译成 compiler.exe。参数说明/EHsc 启用 C 异常处理/Fe: 指定输出文件名。注意如果 y.tab.c 是 C 文件而其他是 C可能需要分开编译再链接否则会有名字修饰问题。4. 避坑与排查编译不通过、结果不对、PCSpim 跑不起来4.1 现象编译时报 y.tab.h 找不到或 token 未定义原因y.tab.h 没有生成或者生成路径和 Praser.cpp 里的 include 路径不一致。bison 加 -d 才会生成头文件如果只跑了 bison 没加 -d或者生成到了别的目录就会报这个错。解决确认 bison 命令带了 -d检查 y.tab.h 是否在编译器能找到的 include 路径下。如果用的是 VS把 y.tab.h 所在目录加到项目附加包含目录里。4.2 现象中间代码生成阶段报「未声明变量」但变量明明声明了原因符号表作用域没处理好变量声明在某个 block 里但查表时已经弹栈了。block.cpp 里压栈弹栈的时机不对或者 Praser.cpp 遍历顺序有问题。解决检查 block.cpp 的 enterScope 和 exitScope 调用位置确保声明语句处理完再退出作用域。可以在查表失败时打印当前作用域栈内容看变量到底在不在。4.3 现象result.asm 在 PCSpim 里加载后报语法错误原因Python 脚本生成的 MIPS 汇编格式不对比如标签重复、指令操作数个数不对、缺少 .data 或 .text 段声明。解决先用 PCSpim 的单步调试看哪一行报错对照 MIPS 指令手册检查操作数。常见问题是 sw 和 lw 的偏移量没写对或者标签名用了中文或特殊字符。4.4 现象优化后结果反而错了原因优化 pass 改变了程序语义比如常量折叠时没考虑溢出或者死代码删除删掉了有副作用的语句。解决先关掉优化跑一遍确认基础版本结果正确再逐个开启优化 pass定位是哪个 pass 引入的错误。常量折叠要注意整数溢出死代码删除要保留函数调用和赋值。4.5 现象compiler.exe 运行后没输出或者输出文件为空原因输入文件路径不对或者程序读不到 test.c。资源包里有 test 目录和 test.c但 exe 的工作目录可能不是资源根目录。解决把 test.c 拷到 exe 同目录或者用绝对路径传参。检查代码里读文件的路径是硬编码还是命令行参数硬编码的话改成相对路径或加参数解析。5. 进阶用法把这份编译器改成你自己的课程设计5.1 扩展语法加一个 for 循环或数组如果你想在这份资源基础上加功能最直接的是改 compiler.y 加文法产生式然后在 Praser.cpp 里加对应的语法树节点处理最后在 innerCode.cpp 里加中间代码生成。比如加 for 循环文法里加FOR ( expr ; expr ; expr ) stmt语法树加 NODE_FOR 节点中间代码生成时翻译成条件跳转和回跳。/* compiler.y 里加 for 循环产生式示意 */ stmt : FOR ( expr ; expr ; expr ) stmt { $$ makeForNode($3, $5, $7, $9); } ;这段 yacc 规则里$3 是初始化表达式$5 是条件$7 是步进$9 是循环体。makeForNode 构造语法树节点。注意 yacc 的 $$ 和 $n 是语义值栈的引用类型要在 %union 里定义好。5.2 验证方法用测试用例覆盖每条路径资源包里有 test 目录和 test.c但只有一两个用例不够。我一般会准备一组测试变量声明与赋值、算术运算、条件分支、循环、函数调用、错误检测未声明变量、类型不匹配。每个用例跑一遍对比中间代码和 MIPS 输出是否符合预期。测试类型输入特征预期结果变量声明赋值int a; a 1;中间代码有 STOREMIPS 有 sw算术运算a b c * d;中间代码有 MUL 和 ADD条件分支if (a b) ...中间代码有跳转指令错误检测使用未声明变量报错并记录到 errorList优化a 3 4;折叠成 a 75.3 一个具体技巧用 y.output 定位文法冲突y.output 是 yacc 生成的冲突报告里面会列出移进/归约冲突和归约/归约冲突。课程设计答辩时如果被问到「你的文法有没有二义性」直接打开 y.output 看冲突数量就行。常见冲突来源是表达式优先级没定义好比如a b * c到底先算哪个。解决办法是在 yacc 里用 %left、%right、%nonassoc 声明优先级。/* 在 compiler.y 里声明运算符优先级 */ %left - %left * / %right 这三行声明了加减左结合、乘除左结合、赋值右结合。注意顺序先声明的优先级低后声明的高。这样 yacc 就能自动解决表达式优先级冲突y.output 里的冲突数会减少。从那以后我每次改文法都会先跑一遍 bison -v 看 y.output确认冲突数没增加再继续。希望帮到你。本文还有配套的精品资源点击获取
返回列表