简介本资源为中国海洋大学2020年春季《编译原理》课程全套实验代码与配套文档面向计算机专业本科生及编译技术初学者系统覆盖词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成、错误处理及编译器集成等八大核心环节助力学习者从理论走向实践。压缩包含74个文件774KB以18个C源码、8个Lex.l与8个Yacc.y脚本、8个头文件.h、7个说明文本.txt及4个Makefile为主干辅以可执行文件.exe、测试用例.p/.i和实验要求文档.doc结构完整、模块清晰支持逐阶段调试与整合验证。已有4412人学习下载内容真实源自教学实践包含完整实验流程、典型文法示例、符号表构建逻辑、三地址码生成规则及常见错误诊断机制是深入理解编译器构造全流程的优质实操范本。1. OUC编译原理全部实验不是抄代码是亲手把“hello world”编译成机器指令的完整闭环在青岛海洋大学OUC计算机学院的编译原理课上“全部实验”四个字背后不是零散的语法分析器练习而是一条从词法扫描→语法解析→语义检查→中间代码生成→目标代码优化→可执行文件落地的完整流水线。我带过三届OUC本科生做这套实验最常听到的崩溃瞬间是写完LL(1)分析器输入ab*c能输出语法树但一跑gcc -S对比发现自己的四元式生成顺序和GCC差了两行——不是逻辑错是符号表作用域链没建对。这套实验真正卡人的地方从来不是算法本身而是每个模块之间数据结构的隐式契约词法单元怎么传给语法分析器才不丢类型信息抽象语法树节点如何携带位置信息供错误提示用寄存器分配时如何让变量生命周期和CFG支配关系对齐它面向的是能手写C语言子集编译器、能看懂.s汇编反推IR、能用objdump -d验证自己生成的x86-64指令是否符合调用约定的工程能力。如果你正被OUC编译原理实验报告压得喘不过气或者刚在Quartus里调通数码管显示却搞不定编译器后端的寄存器分配——这篇笔记就是为你写的实操路径图。2. 实验环境与工具链用最小依赖复现OUC标准实验平台OUC编译原理实验对环境有明确约束LinuxUbuntu 20.04 LTS为主、Python 3.8用于前端脚本、Flex/Bison词法/语法生成器、GCC 9.4作为参考后端和链接器、GDB调试生成的目标代码。关键不是装一堆工具而是让每个工具只做一件事并暴露中间产物——这是排查实验问题的命脉。2.1 搭建可追溯的编译流水线OUC实验要求所有中间结果必须可查看、可验证。比如词法分析阶段不能只输出“识别成功”而要生成带行号、列号、token类型、原始字符串的.tokens文件语法分析阶段必须输出.astJSON格式AST树和.parse_trace移进/归约动作日志。以下命令链是我在实验室统一部署的最小可行方案# 1. 安装核心工具OUC镜像已预装但本地需验证版本 sudo apt update sudo apt install -y flex bison build-essential gdb python3-pip pip3 install ply3.11 # 注意OUC实验指导书指定PLY 3.11高版本会破坏语义动作绑定 # 2. 创建标准项目结构严格按OUC实验报告目录命名 mkdir -p ouc-compiler/{lexer,parser,irgen,codegen,tests} cd ouc-compiler # 3. 生成词法分析器.l文件需含%option yylineno启用行号 flex -o lexer/lex.c lexer/lexer.l gcc -c -o lexer/lex.o lexer/lex.c -I/usr/include/flex # 4. 生成语法分析器.y文件需含%define api.pure full bison -d -o parser/parser.c parser/parser.y gcc -c -o parser/parser.o parser/parser.c # 5. 链接并生成可执行编译器注意OUC要求main函数在driver.c中 gcc -o compiler driver.c lexer/lex.o parser/parser.o -lfl提示OUC实验评分细则明确要求./compiler test.c必须输出.tokens、.ast、.ir、.s四类文件。因此driver.c中必须调用generate_tokens()、parse_ast()、emit_ir()、codegen_to_asm()四个函数并将结果分别写入对应文件。不要试图用printf打桩代替文件输出——去年有7个小组因未生成.ir文件直接扣掉30%过程分。2.2 用GDB逆向验证生成代码的正确性很多同学卡在代码生成阶段生成的.s文件能as汇编、能ld链接但运行时段错误。根本原因往往是寄存器使用冲突或栈帧布局错误。OUC实验要求用GDB单步跟踪生成的可执行文件验证三条关键路径函数调用时%rdi/%rsi/%rdx是否按x86-64 ABI正确传参局部变量是否全部分配在%rbp-8、%rbp-16等负偏移位置ret指令前%rsp是否恢复到调用前值验证命令如下以test.c含int main(){return 1;}为例# 编译生成.s文件后手动汇编链接 gcc -c -o test.o test.s gcc -o test.out test.o # 用GDB加载并反汇编main函数 gdb ./test.out (gdb) disassemble main (gdb) break *main10 # 在mov %eax,%eax后设断点 (gdb) run (gdb) info registers rsp rbp rax # 关键检查rsp是否等于rbp说明栈帧干净若rsp ! rbp说明你的代码生成器没生成leave或pop %rbp指令——这正是OUC实验报告第4题的扣分点。3. 词法与语法分析为什么OUC坚持用Flex/Bison而非手写递归下降OUC编译原理实验第一、二个实验词法分析器、语法分析器强制要求用Flex/Bison实现而非更易理解的递归下降。这不是为了增加难度而是训练对形式文法与自动机本质的直觉。学生常误以为“能识别if (x0) y1;就算过关”但OUC评分标准里藏着三个硬性指标保留注释与空白符的位置信息用于后续错误提示对非法token如0xG十六进制非法字符必须输出精确行列号语法分析器必须支持左递归消除如E → E T | T需改写为E → T E且E的FIRST/FOLLOW集计算正确3.1 Flex规则中的“隐形契约”行号与token边界OUC实验要求.tokens文件每行格式为[行号,列号] token_type lexeme。但Flex默认不提供列号需手动维护yycol变量。关键代码片段如下lexer.l%{ #include stdio.h int yycol 0; %} %% [ \t\n] { if (yytext[0] \n) { yylineno; yycol 0; // 行尾重置列号 } else if (yytext[0] \t) { yycol (yycol / 4 1) * 4; // OUC要求tab按4空格计 } else { yycol; } } [a-zA-Z_][a-zA-Z0-9_]* { fprintf(yyout, [%d,%d] ID \%s\\n, yylineno, yycol, yytext); yycol yyleng; // 标识符长度计入列号 } [0-9] { fprintf(yyout, [%d,%d] NUM \%s\\n, yylineno, yycol, yytext); yycol yyleng; } . { fprintf(yyout, [%d,%d] ERROR \Invalid char %c\\n, yylineno, yycol, yytext[0]); yycol; } %% int yywrap() { return 1; }参数说明yycol必须在匹配每个token后累加yyleng当前token长度否则列号会丢失。OUC实验报告中“token位置错误”是最高频扣分项——去年23份报告里17份在此处出错。3.2 Bison中语义动作的“副作用陷阱”Bison语法文件.y中语义动作{...}块看似只是C代码但OUC实验要求所有AST节点必须动态分配且父节点指针必须指向子节点内存地址。常见错误是// ❌ 错误局部变量地址在函数返回后失效 expr: expr term { struct ASTNode* node malloc(sizeof(struct ASTNode)); node-type PLUS; node-left $1; // $1是expr的语义值但若$1是栈变量则危险 node-right $3; $$ node; } // ✅ 正确所有节点必须malloc且子节点指针必须有效 expr: expr term { struct ASTNode* node malloc(sizeof(struct ASTNode)); node-type PLUS; node-left $1; // $1必须是malloc返回的指针 node-right $3; // $3同理 $$ node; }OUC实验验收时会用Valgrind检测内存泄漏valgrind --leak-checkfull ./compiler test.c。若出现definitely lost直接判定实验不合格。4. 中间代码与目标代码生成从四元式到x86-64汇编的不可跳过环节OUC编译原理实验第三、四、五个实验语义分析、中间代码生成、目标代码生成构成后端核心。学生最容易在这里“玄学翻车”生成的.s文件能通过as但./a.out运行结果与GCC编译结果不一致。根本原因在于OUC要求中间代码必须是三地址码四元式且目标代码必须严格遵循x86-64 System V ABI——这不是选择题是硬性规范。4.1 四元式生成的三个强制约束OUC实验指导书明确规定四元式格式为(op, arg1, arg2, result)且必须满足arg1/arg2为标识符或常量禁止出现嵌套表达式如(, a, (, b, c), t1)非法必须拆成(, b, c, t2)(, a, t2, t1)所有临时变量t1,t2...必须按顺序编号不得跳跃或重复t1,t3缺失即判错数组访问a[i]必须展开为(, a, (*, i, 4), t1)假设int占4字节偏移计算必须显式写出生成逻辑示例expr → expr term# Python伪代码PLY中semantic action def p_expr_plus_term(p): expr : expr term # 申请新临时变量 temp_var ft{next_temp_id()} # 全局计数器从1开始 # 生成四元式(, p[1], p[3], temp_var) quad (, p[1], p[3], temp_var) quads.append(quad) p[0] temp_var # 语义值设为临时变量名注意OUC实验报告要求提交quads.txt格式必须为(, a, b, t1)括号、逗号、空格缺一不可。去年有学生用f({op}, {arg1}, {arg2}, {res})生成但arg1含空格时导致(, x y, z, t1)——这违反“arg1为标识符或常量”的约束被退回重做。4.2 x86-64代码生成寄存器分配的“血泪经验”OUC实验要求目标代码使用%rax,%rbx,%rcx,%rdx,%rsi,%rdi六个通用寄存器禁止使用%r8及以上扩展寄存器因实验机房虚拟机仅模拟基础寄存器。关键难点是函数参数必须用%rdi,%rsi,%rdx,%rcx,%r8,%r9传递前6个但OUC实验限定最多3个参数故只用%rdi/%rsi/%rdx局部变量必须分配在栈上%rbp-8,%rbp-16...寄存器仅用于临时计算call指令前必须push %rbp保存旧帧指针ret前必须pop %rbp一个典型int add(int a, int b) { return ab; }的生成模板add: push %rbp mov %rsp, %rbp # 参数a在%rdib在%rsi mov %rdi, %rax # a - %rax add %rsi, %rax # ab - %rax pop %rbp ret避坑若忘记push %rbpGDB中info registers rbp会显示乱码值导致ret跳转到非法地址——这是OUC实验中最常见的段错误来源。5. 常见问题排查OUC编译原理实验的5个高频翻车点OUC编译原理实验的验收不是“跑通就行”而是逐行比对中间产物与标准答案。以下是近三年助教记录的5个最高频问题按现象→原因→解决给出精准定位路径5.1 现象.tokens文件中数字token的列号比实际位置少1原因Flex规则中[0-9]匹配后未执行yycol yyleng导致列号停留在数字第一个字符位置。例如123在第5列开始应记录[1,5]但错误实现记录[1,5]只记了1的位置。解决在数字规则末尾添加yycol yyleng;并用echo -e int a123; | ./compiler测试输出。5.2 现象Bison报错conflicts: 1 shift/reduce但语法明显无歧义原因OUC实验要求消除左递归但学生常错误改写为E → T | E T仍含左递归正确应为E → T E和E → T E | ε。Bison无法自动消除必须手动改写文法。解决用bison -v parser.y生成parser.output搜索state 5查看冲突状态对照FIRST/FOLLOW集重新设计文法。5.3 现象生成的.s文件as时报错error: invalid character $ in operand原因代码生成器输出了ATT语法如movq $1, %rax但OUC实验要求Intel语法mov rax, 1。实验指导书明确要求-masmintel。解决在GCC调用时添加-masmintel或在.s文件首行添加.intel_syntax noprefix。5.4 现象./a.out运行结果为随机大数而非预期值原因四元式中常量未加引号如(, 5, , a)应为(, 5, , a)。OUC标准答案校验脚本用正则\\d匹配常量无引号则匹配失败。解决在生成四元式时对常量arg1/arg2强制加单引号f({val}, ...)。5.5 现象GDB中info registers rax显示值正确但print $rax为0原因GDB默认显示十进制而x86-64寄存器值为64位若高位为1如0xffffffffffffffff十进制显示为-1但print命令按有符号解释。解决用p/x $rax查看十六进制或p/u $rax查看无符号十进制——OUC实验报告要求截图必须含p/x输出。6. 进阶验证技巧用GCC内建函数反向校验你的编译器正确性OUC编译原理实验的终极验证不是“自己跑通”而是与GCC生成的汇编逐指令比对。但直接比对.s文件效率极低我教学生用三个GCC内建函数快速定位差异点6.1 用__builtin_frame_address(0)捕获栈帧基址在测试程序test.c中插入#include stdio.h int main() { void* frame __builtin_frame_address(0); // 获取当前rbp值 printf(Frame address: %p\n, frame); return 0; }编译你的编译器生成test.s再用GCC生成gcc_test.s用diff比对两文件中main:标签后的push %rbp和mov %rsp,%rbp指令位置。若你的版本在mov指令后多了一行sub $8,%rsp说明栈帧分配错误——OUC实验要求局部变量区必须紧邻%rbp下方禁止预留额外空间。6.2 用__builtin_return_address(0)验证调用链修改test.c为调用函数int helper() { return __builtin_return_address(0) (void*)main ? 1 : 0; } int main() { return helper(); }你的编译器生成的helper函数必须在ret前将%rbp恢复为main的帧指针。若helper中pop %rbp后%rbp值与main中不同则调用约定破坏——OUC实验第5题直接判0分。6.3 用__builtin_constant_p()触发常量折叠优化写一个含常量表达式的测试int main() { const int a 2; const int b 3; return a * b 1; // 应折叠为7 }GCC会生成mov $7, %eax而你的编译器若生成mov $2, %rax; imul $3, %rax; add $1, %rax说明未实现常量折叠——OUC实验虽不强制要求优化但若中间代码含冗余四元式如(, 2, , t1)(, 3, , t2)( *, t1, t2, t3)会被扣分。我带实验时有个铁律每次提交前用gcc -S -O0 test.c生成gcc.s再用diff -u gcc.s your.s | grep ^ | head -10看前10行新增指令。如果出现lea、shl、sar等非基础指令说明你的代码生成器引入了GCC的优化逻辑——而OUC实验要求“朴素生成”必须删掉。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?