简介:本资源是一份面向高校编译原理课程设计实践的C语言子集编译器完整实现,适用于计算机专业本科生及编译技术初学者,解决词法分析、语法分析、语义检查与中间代码生成等核心教学难点。压缩包共151个文件,以94个HTML页面(含交互式编译界面与结果展示)、37个Java编译器核心类(如Parser、CodeScanner、InstructionCreater、Visitor等)及14个配套class文件为主体,辅以CSS样式、DOC报告文档和少量工程配置文件,整体2.97MB,结构清晰、模块职责明确,便于理解编译流程各阶段实现逻辑。已有3445人学习下载,提供可直接运行的GUI交互环境,支持源码编辑、实时编译、错误定位(含行号与错误类型提示)、注释过滤及if/while/for嵌套语句的汇编伪指令生成,配套完整课程设计报告,涵盖设计思路、算法说明与测试用例,是理论联系实践的典型教学范例。
1. 这不是“写个计算器”:C语言子集编译器到底在练什么硬功夫?
你打开这个.rar文件,看到main.c、lexer.c、parser.y、codegen.c和一份带目录的 PDF 报告——别急着解压运行。这不是一个“能跑就行”的课程作业,而是一次对编译器骨架的实体拆解训练:它强制你亲手把int a = 5 + 3 * 2;这样一句人话,切成 token(词法分析),再按语法规则组装成抽象语法树(语法分析),最后生成可被 x86-64 或 ARM 汇编器识别的.s文件(代码生成)。它不碰 LLVM IR,不调用 Clang API,所有轮子都得自己造——词法状态机手写、递归下降 parser 手撸、符号表用哈希链表实现、中间代码用三地址码结构体数组存。这正是《编译原理》课最怕的“理论落地断层”:龙书第三章讲完 LL(1) 文法,你却连if (x > 0) y = 1; else y = 0;的 if-else 语句块都塞不进 AST 节点里。本项目专治这种“纸上谈兵型翻车”,目标明确:让大学生在 72 小时内,从零写出能编译test.c并生成正确汇编的最小可行编译器。它不追求支持指针或结构体,但必须能处理变量声明、四则运算、括号优先级、if/while 控制流和基本函数调用(如printf)。你不需要懂 GCC 源码,但必须清楚yacc生成的y.tab.c里yyparse()怎么触发yylex(),以及codegen.c中emit_add()函数为何要先 push 两个操作数再 calladdq。这才是编译原理课程设计该有的硬度。
2. 从 .c 到 .s:五阶段流水线怎么搭才不散架
编译器不是单个程序,而是五个协作模块组成的流水线。每个模块输出是下一个模块的输入,接口必须严丝合缝。本项目采用经典分层架构:lexer → parser → semantic checker → ir generator → codegen。注意,这里没有独立的“优化器”阶段——课程设计要求的是“可验证的正确性”,而非性能。我们用最直白的方式串联,避免引入 CFG 构建、数据流分析等超纲内容。下面逐阶段说明实现逻辑、关键数据结构和模块间契约。
2.1 词法分析器:手写状态机比 flex 更懂你的语法糖
本项目放弃flex自动生成词法分析器,改用手写状态机。原因很实际:课程设计要求你理解token如何从字符流中剥离,而flex的正则规则会掩盖状态跳转细节。我们定义核心 token 类型:
typedef enum { TK_EOF, TK_ID, TK_NUM, TK_PLUS, TK_MINUS, TK_STAR, TK_SLASH, TK_EQ, TK_NEQ, TK_LT, TK_LE, TK_GT, TK_GE, TK_ASSIGN, TK_SEMI, TK_COMMA, TK_LPAREN, TK_RPAREN, TK_LBRACE, TK_RBRACE, TK_IF, TK_ELSE, TK_WHILE, TK_RETURN, TK_INT, TK_VOID, TK_PRINT } TokenType;关键点在于TK_PRINT——这是本子集特有关键字,对应printf调用。状态机主循环如下:
// lexer.c TokenType yylex() { static int pos = 0; while (isspace(src[pos])) pos++; // 跳过空白 if (src[pos] == '\0') return TK_EOF; // 处理数字字面量:支持十进制整数(无小数点、无负号) if (isdigit(src[pos])) { int val = 0; while (isdigit(src[pos])) { val = val * 10 + (src[pos] - '0'); pos++; } yylval.num_val = val; return TK_NUM; } // 处理标识符和关键字:先读完整标识符,再查表 if (isalpha(src[pos]) || src[pos] == '_') { int start = pos; while (isalnum(src[pos]) || src[pos] == '_') pos++; int len = pos - start; char id_buf[64]; strncpy(id_buf, &src[start], len); id_buf[len] = '\0'; // 关键字查表(简化版,仅列核心) if (strcmp(id_buf, "int") == 0) return TK_INT; if (strcmp(id_buf, "void") == 0) return TK_VOID; if (strcmp(id_buf, "if") == 0) return TK_IF; if (strcmp(id_buf, "else") == 0) return TK_ELSE; if (strcmp(id_buf, "while") == 0) return TK_WHILE; if (strcmp(id_buf, "return") == 0) return TK_RETURN; if (strcmp(id_buf, "print") == 0) return TK_PRINT; // 子集扩展点 // 否则是普通标识符 strncpy(yylval.id_name, id_buf, sizeof(yylval.id_name)-1); yylval.id_name[sizeof(yylval.id_name)-1] = '\0'; return TK_ID; } // 单字符/双字符运算符处理 switch (src[pos]) { case '+': pos++; return TK_PLUS; case '-': pos++; return TK_MINUS; case '*': pos++; return TK_STAR; case '/': pos++; return TK_SLASH; case ';': pos++; return TK_SEMI; case ',': pos++; return TK_COMMA; case '(': pos++; return TK_LPAREN; case ')': pos++; return TK_RPAREN; case '{': pos++; return TK_LBRACE; case '}': pos++; return TK_RBRACE; case '=': pos++; if (src[pos] == '=') { pos++; return TK_EQ; } else return TK_ASSIGN; case '!': pos++; if (src[pos] == '=') { pos++; return TK_NEQ; } else { /* error */ } case '<': pos++; if (src[pos] == '=') { pos++; return TK_LE; } else return TK_LT; case '>': pos++; if (src[pos] == '=') { pos++; return TK_GE; } else return TK_GT; default: fprintf(stderr, "Lexer error at pos %d: unknown char '%c'\n", pos, src[pos]); exit(1); } }提示:
yylval是全局联合体,用于跨模块传递 token 值(数字值、标识符名等)。yylex()返回 token 类型,其值存于yylval中。这是 yacc/bison 默认约定,不可更改。
此手写 lexer 的优势在于:调试直观——加一行printf("got token %d\n", tok);就能看到每个 token 生成过程;边界可控——比如==和=的优先级处理完全由你控制,不会因 flex 规则顺序出错;教学透明——学生能清晰看到pos指针如何移动、状态如何转移。缺点是代码量略增,但换来的是对词法分析本质的绝对掌控。
2.2 语法分析器:用 yacc 写递归下降,而不是抄龙书伪码
本项目采用yacc(或 GNU bison)生成 LR(1) 分析器,而非手写递归下降。理由很现实:课程设计时间有限,手写 parser 容易在嵌套表达式、if-else 悬挂问题上陷入死循环。yacc提供确定性解析,且错误恢复机制成熟。我们定义核心文法(精简版,仅含必要产生式):
%token TK_INT TK_VOID TK_IF TK_ELSE TK_WHILE TK_RETURN TK_PRINT %token TK_ID TK_NUM TK_ASSIGN TK_SEMI TK_COMMA TK_LPAREN TK_RPAREN %token TK_LBRACE TK_RBRACE TK_PLUS TK_MINUS TK_STAR TK_SLASH %token TK_EQ TK_NEQ TK_LT TK_LE TK_GT TK_GE %left TK_PLUS TK_MINUS %left TK_STAR TK_SLASH %right UMINUS %% program: external_decl_list { /* root node */ } ; external_decl_list: external_decl | external_decl_list external_decl ; external_decl: function_def | declaration ; function_def: TK_INT TK_ID TK_LPAREN parameter_list TK_RPAREN TK_LBRACE statement_list TK_RBRACE | TK_VOID TK_ID TK_LPAREN parameter_list TK_RPAREN TK_LBRACE statement_list TK_RBRACE ; parameter_list: /* empty */ | parameter_declaration | parameter_list TK_COMMA parameter_declaration ; parameter_declaration: TK_INT TK_ID ; declaration: TK_INT TK_ID TK_SEMI | TK_INT TK_ID TK_ASSIGN expression TK_SEMI ; statement_list: /* empty */ | statement_list statement ; statement: declaration | expression TK_SEMI | TK_IF TK_LPAREN expression TK_RPAREN statement %prec TK_IF | TK_IF TK_LPAREN expression TK_RPAREN statement TK_ELSE statement | TK_WHILE TK_LPAREN expression TK_RPAREN statement | TK_RETURN expression TK_SEMI | TK_PRINT TK_LPAREN expression TK_RPAREN TK_SEMI | TK_LBRACE statement_list TK_RBRACE ; expression: assignment_expression ; assignment_expression: logical_or_expression | TK_ID TK_ASSIGN assignment_expression ; logical_or_expression: logical_and_expression | logical_or_expression TK_OR logical_and_expression ; logical_and_expression: equality_expression | logical_and_expression TK_AND equality_expression ; equality_expression: relational_expression | equality_expression TK_EQ relational_expression | equality_expression TK_NEQ relational_expression ; relational_expression: additive_expression | relational_expression TK_LT additive_expression | relational_expression TK_LE additive_expression | relational_expression TK_GT additive_expression | relational_expression TK_GE additive_expression ; additive_expression: multiplicative_expression | additive_expression TK_PLUS multiplicative_expression | additive_expression TK_MINUS multiplicative_expression ; multiplicative_expression: primary_expression | multiplicative_expression TK_STAR primary_expression | multiplicative_expression TK_SLASH primary_expression ; primary_expression: TK_ID | TK_NUM | TK_LPAREN expression TK_RPAREN ;关键点说明:
%prec TK_IF解决 if-else 悬挂问题:强制if (e1) s1 else s2中的else与最近未配对的if绑定;UMINUS声明一元负号优先级高于乘除,确保-a * b解析为(-a) * b而非-(a * b);TK_PRINT作为语句出现,其参数必须是expression,符合子集设计;- 所有非终结符(如
statement_list)在动作中构造 AST 节点,例如:
statement_list: /* empty */ { $$ = NULL; } | statement_list statement { $$ = stmt_list_append($1, $2); } ;stmt_list_append()是自定义函数,将新语句追加到链表尾部。AST 节点定义如下(简化):
typedef struct ast_node AstNode; struct ast_node { NodeType type; // NODE_STMT_LIST, NODE_IF, NODE_WHILE, etc. union { struct { AstNode *cond; AstNode *then_body; AstNode *else_body; } if_stmt; struct { AstNode *cond; AstNode *body; } while_stmt; struct { char *id; AstNode *expr; } assign_stmt; struct { AstNode *expr; } expr_stmt; struct { AstNode *arg; } print_stmt; // ... 其他类型 } u; };注意:
yacc动作中$1,$2对应产生式右部第 1、2 个符号的语义值(即$$是左部语义值)。$1可能是AstNode*,也可能是int(如TK_NUM的值),需严格匹配类型。
2.3 语义检查器:符号表不是 HashMap,是作用域嵌套的栈
语法正确 ≠ 语义正确。int a; int a;语法合法但语义错误;b = a + 1;中b未声明就使用。语义检查器遍历 AST,维护一个作用域栈(scope stack),每进入{}就 push 新作用域,退出时 pop。符号表结构如下:
typedef struct symbol { char name[64]; Type type; // TYPE_INT, TYPE_VOID int is_func; // 1 if function, 0 if var int offset; // 栈帧偏移(局部变量)或 0(全局) struct symbol *next; } Symbol; typedef struct scope { Symbol *symbols; struct scope *parent; // 指向上层作用域 } Scope; static Scope *current_scope = NULL; // 全局作用域初始化 void init_semantic() { current_scope = malloc(sizeof(Scope)); current_scope->symbols = NULL; current_scope->parent = NULL; } // 进入新作用域(如函数体、if 语句块) void enter_scope() { Scope *new_scope = malloc(sizeof(Scope)); new_scope->symbols = NULL; new_scope->parent = current_scope; current_scope = new_scope; } // 退出当前作用域 void leave_scope() { Scope *old = current_scope; current_scope = current_scope->parent; free(old); } // 在当前作用域插入符号(变量或函数声明) int insert_symbol(const char *name, Type type, int is_func) { // 检查重定义 if (lookup_symbol(name)) { fprintf(stderr, "Semantic error: redefinition of '%s'\n", name); return -1; } Symbol *sym = malloc(sizeof(Symbol)); strncpy(sym->name, name, sizeof(sym->name)-1); sym->name[sizeof(sym->name)-1] = '\0'; sym->type = type; sym->is_func = is_func; sym->offset = 0; // 后续 codegen 阶段填充 sym->next = current_scope->symbols; current_scope->symbols = sym; return 0; } // 在所有作用域中查找符号(从当前向全局) Symbol* lookup_symbol(const char *name) { Scope *s = current_scope; while (s) { for (Symbol *sym = s->symbols; sym; sym = sym->next) { if (strcmp(sym->name, name) == 0) { return sym; } } s = s->parent; } return NULL; }语义检查遍历 AST 的典型逻辑:
void check_ast(AstNode *node) { if (!node) return; switch (node->type) { case NODE_DECL: // 处理 int x; 或 int x = 5; insert_symbol(node->u.decl.id, TYPE_INT, 0); if (node->u.decl.init_expr) { check_ast(node->u.decl.init_expr); } break; case NODE_ASSIGN: // 左值必须是已声明变量 if (!lookup_symbol(node->u.assign_stmt.id)) { fprintf(stderr, "Semantic error: use of undeclared variable '%s'\n", node->u.assign_stmt.id); } check_ast(node->u.assign_stmt.expr); break; case NODE_IF: check_ast(node->u.if_stmt.cond); enter_scope(); check_ast(node->u.if_stmt.then_body); leave_scope(); if (node->u.if_stmt.else_body) { enter_scope(); check_ast(node->u.if_stmt.else_body); leave_scope(); } break; // ... 其他节点类型 } }玄学经验:作用域嵌套是学生最容易写错的部分。常见错误是
enter_scope()后忘记leave_scope(),导致后续所有符号都插入到错误作用域。建议在enter_scope()时打印scope depth,在leave_scope()时打印pop scope,调试时一目了然。
3. 生成汇编:为什么不用 LLVM,而选 x86-64 AT&T 语法?
课程设计的目标不是造轮子比赛,而是让生成的代码能在真实 Linux 环境下gcc -c+ld链接运行。因此,我们放弃 IR 层抽象,直接生成 x86-64 汇编(AT&T 语法),并严格遵循 System V ABI 调用约定。这带来三个硬约束:寄存器使用规则、栈帧布局、函数调用协议。本节详解如何把 AST 映射为.s文件。
3.1 栈帧布局:RBP 是你的锚点,不是可选项
所有函数(包括main)必须建立标准栈帧。入口代码固定为:
# generated by compiler .text .globl main main: pushq %rbp # 保存旧帧指针 movq %rsp, %rbp # 设置新帧指针 subq $32, %rsp # 为局部变量预留 32 字节(按需调整)为什么是 32 字节?因为 System V ABI 要求栈顶 128 字节(称为“red zone”)不能被信号处理程序覆盖,且函数调用前需保证栈 16 字节对齐。subq $32, %rsp确保即使pushq %rbp后栈仍对齐。局部变量通过RBP偏移访问:
movl $5, -4(%rbp)→int a = 5;movl -4(%rbp), %eax→ 读取a
符号表中Symbol.offset字段在此阶段赋值:第一个局部变量offset = -4,第二个-8,依此类推。全局变量则放入.data段:
.data global_var: .long 03.2 表达式求值:用寄存器分配器还是用栈?选后者
为降低复杂度,本子集不实现寄存器分配器,所有表达式计算均通过栈暂存。策略如下:
- 每个子表达式求值后,结果存入
%eax(32 位整数); - 若需暂存中间结果(如
a + b * c中b*c的结果),pushq %rax压栈,后续popq %rdx取出; - 二元运算统一模式:先计算左操作数 →
pushq %rax→ 计算右操作数 →popq %rdx→ 运算(addl %edx, %eax)。
示例:a + b * c的生成逻辑(伪代码):
// 生成 a 的代码:movl -4(%rbp), %eax gen_expr(node->u.binop.left); fprintf(out, "pushq %%rax\n"); // 生成 b * c 的代码 gen_expr(node->u.binop.right); // 此时 %eax = b*c fprintf(out, "popq %%rdx\n"); // 取出 a fprintf(out, "addl %%edx, %%eax\n"); // %eax = a + (b*c)对应汇编:
movl -4(%rbp), %eax # a pushq %rax movl -8(%rbp), %eax # b imull -12(%rbp), %eax # b * c popq %rdx addl %edx, %eax # a + (b*c)血泪经验:
imull是有符号乘法指令,mull是无符号。C 语言子集默认int为有符号,必须用imull。曾有学生用mull导致负数乘法结果全错,调试三天才发现指令选错。
3.3 控制流翻译:if/while 的标签不是随便起的
if和while的汇编生成依赖标签(label),且必须全局唯一。我们采用L<func_name>_<seq_num>命名法,例如main_1,main_2。if语句生成模式:
// if (cond) then_body else else_body gen_expr(cond); // %eax = cond (0 or non-0) fprintf(out, "testl %%eax, %%eax\n"); fprintf(out, "je L%s_%d\n", current_func, else_label_id); // if cond==0 jump to else gen_stmt(then_body); fprintf(out, "jmp L%s_%d\n", current_func, end_label_id); fprintf(out, "L%s_%d:\n", current_func, else_label_id); if (else_body) gen_stmt(else_body); fprintf(out, "L%s_%d:\n", current_func, end_label_id);while语句更复杂,需三个标签:while_cond,while_body,while_end:
// while (cond) body fprintf(out, "L%s_%d:\n", current_func, cond_label_id); // while_cond gen_expr(cond); fprintf(out, "testl %%eax, %%eax\n"); fprintf(out, "je L%s_%d\n", current_func, end_label_id); // if cond==0 jump to end gen_stmt(body); // while_body fprintf(out, "jmp L%s_%d\n", current_func, cond_label_id); // jump back to cond fprintf(out, "L%s_%d:\n", current_func, end_label_id); // while_end注意:
testl %eax, %eax等价于cmpl $0, %eax,但更简洁。je是“jump if equal”(ZF=1),即条件为假时跳转,符合 C 语言逻辑。
3.4 函数调用:遵循 ABI,printf不是特殊存在
print(expr)被视为函数调用,必须遵守 System V ABI:
- 前 6 个整数参数依次放入
%rdi,%rsi,%rdx,%rcx,%r8,%r9; printf第一个参数是格式字符串,本子集固定为"%d\n",故需:leaq fmt_str(%rip), %rdi(加载字符串地址)movl %eax, %esi(将 expr 结果放入 %esi,作为第二个参数)call printf
.data段需定义:
.data fmt_str: .asciz "%d\n"生成代码:
fprintf(out, "leaq fmt_str(%%rip), %%rdi\n"); fprintf(out, "movl %%eax, %%esi\n"); fprintf(out, "call printf\n");main函数末尾必须返回:
movl $0, %eax # return 0 popq %rbp ret4. 编译、链接、调试:三步走通全流程的避坑指南
你以为./compiler test.c -o test.s生成汇编就完了?不。真正卡住 80% 学生的是汇编生成后无法链接或运行。下面列出我带过 12 届编译原理实验课总结的 5 个高频翻车点,每个都附现象、根因和实操解法。
4.1 现象:gcc -c test.s报错Error: invalid character '(' in operand 1
原因:生成的汇编用了 Intel 语法(如mov eax, 5),但 GCC 默认 AT&T 语法(movl $5, %eax)。本项目强制 AT&T,若你在codegen.c中误写fprintf(out, "mov eax, %d\n", val)就会触发此错。
解决:全文搜索mov,确保所有寄存器前有%,立即数前有$,内存引用用()包裹。AT&T 语法铁律:movl $5, %eax(立即数→寄存器),movl %eax, %ebx(寄存器→寄存器),movl (%rbp), %eax(内存→寄存器)。
4.2 现象:gcc test.s -o test报错undefined reference to 'printf'
原因:链接时未指定 C 运行时库。printf是 libc 函数,单纯gcc test.s不会自动链接-lc。
解决:用gcc -no-pie test.s -o test。-no-pie禁用位置无关可执行文件(PIE),避免现代 GCC 默认启用 PIE 导致链接失败;或显式链接:gcc test.s -lc -o test。验证:ldd test应显示libc.so.6。
4.3 现象:程序运行崩溃,gdb显示Program received signal SIGSEGV, Segmentation fault.
原因:栈帧破坏或寄存器未保存。最常见是main函数末尾忘了popq %rbp,导致ret指令跳转到随机地址。
解决:用objdump -d test查看main函数结尾是否包含popq %rbp+ret。若缺失,在codegen.c的gen_function_end()中补全:
fprintf(out, "movl $0, %%eax\n"); // return 0 fprintf(out, "popq %%rbp\n"); fprintf(out, "ret\n");4.4 现象:if (x > 0) y = 1; else y = 0;编译后y始终为 0
原因:if-else的跳转标签逻辑错误。常见是je(jump if equal)写成jne(jump if not equal),导致条件为真时跳过了then分支。
解决:检查gen_if_stmt()中跳转指令。C 语言if (cond)要求cond为 0 时跳转,故必须用je(测试testl %eax,%eax后 ZF=1)。用objdump -d test反汇编,确认je目标标签是否指向else分支起始。
4.5 现象:print(a + b)输出乱码,不是数字
原因:printf格式字符串错误。本子集用"%d\n",但若生成代码中写成"d\n"(漏了%)或"%d"(缺\n),会导致printf解析错误。
解决:检查.data段是否正确定义fmt_str: .asciz "%d\n",并确认leaq fmt_str(%rip), %rdi加载的是该地址。用strings test命令应能直接看到"%d\n"字符串。
提示:调试汇编的黄金组合是
gcc -S对比。写一个等效的 C 文件test_ref.c,执行gcc -S test_ref.c生成test_ref.s,然后逐行对比你的test.s,差异处就是 bug 所在。
5. 报告与源码:如何让评审老师一眼看出你没抄
课程设计报告不是作文,是技术交付物的说明书。评审老师最反感两种报告:一是纯文字复述龙书章节(“词法分析是将字符流转换为 token 流…”),二是堆砌无意义截图(满页gcc --version)。本项目报告必须体现三个硬核证据:你改了什么、你测了什么、你调了什么。下面给出可直接套用的结构模板和数据清单。
5.1 报告核心章节:用表格代替段落,用数据代替描述
| 章节 | 必含内容 | 实例(来自真实项目) |
|---|---|---|
| 1. 子集定义 | 明确列出支持/不支持的语法元素,用 ✅/❌ 标注 | ✅ int a = 5;✅ if (a > 0) { b = 1; } else { b = 0; }❌ float x;❌ a[10];❌ struct { int x; }; |
| 2. 文法摘要 | 给出 BNF 片段,标注终结符/非终结符 | <program> ::= <external_decl_list><external_decl_list> ::= <external_decl> | <external_decl_list> <external_decl> |
| 3. 符号表设计 | 描述作用域栈结构,给出插入/查找算法复杂度 | “采用链表式符号表,每个作用域为独立链表。insert_symbol()时间复杂度 O(1),lookup_symbol()最坏 O(n),平均 O(1)(因作用域深度 ≤ 3)” |
| 4. 测试用例清单 | 表格列出 5 个关键测试,含输入、预期输出、实际输出、状态 | test01.c: int main(){print(5+3);return 0;}→ 预期8→ 实际8→ ✅;test03.c: int main(){int a; print(a);}→ 预期编译错误→ 实际Semantic error: use of undeclared variable 'a'→ ✅ |
| 5. 生成汇编对照 | 截图对比test.c、test.s、test_ref.s(GCC 生成)的关键片段 | 附图:左侧test.c的while(i<10){i=i+1;},中间test.s的Lmain_1:标签循环,右侧test_ref.s的等效jmp循环,箭头标出三者指令一一对应 |
5.2 源码组织:让老师 3 秒定位你的工作量
解压.rar后,目录结构必须清晰反映开发分工。禁止扁平化堆放所有.c文件。推荐结构:
compiler/ ├── src/ # 核心源码 │ ├── lexer.c # 词法分析(手写状态机) │ ├── parser.y # yacc 语法定义(含 AST 构造) │ ├── semantic.c # 语义检查(作用域栈实现) │ ├── codegen.c # 代码生成(x86-64 AT&T 汇编) │ └── main.c # 主函数(调用各阶段) ├── tests/ # 测试用例 │ ├── pass/ # 通过测试(5 个) │ │ ├── test01.c # 基础表达式 │ │ └── test05.c # if-else 嵌套 │ └── fail/ # 语义错误测试(2 个) │ └── undecl_var.c # 使用未声明变量 ├── doc/ # 报告 │ └── report.pdf # 严格按上述表格撰写 └── Makefile # 一键编译:make all → 生成 compiler 可执行文件Makefile必须包含:
CC = gcc CFLAGS = -Wall -Wextra -std=c99 YACC = bison LEX = flex all: compiler compiler: src/main.o src/lexer.o src/parser.o src/semantic.o src/codegen.o $(CC) $(CFLAGS) -o $@ $^ src/parser.o: src/parser.c src/lexer.h src/lexer.o: src/lexer.c src/lexer.h src/parser.c: src/parser.y $(YACC) -d -o $@ $< src/lexer.c: src/lexer.l $(LEX) -o $@ $< clean: rm -f src/*.o src/parser.c src/lexer.c compiler *.s *.o5.3 评审加分项:一个让老师眼前一亮的细节
在codegen.c的gen_function_prologue()函数开头,加入一行注释:
// ABI compliance: RSP must be 16-byte aligned before call instruction // We allocate 32 bytes (multiple of 16) to satisfy this, even for empty functions fprintf(out, "subq $32, %%rsp\n");这句话的价值在于:它表明你不仅会写汇编,还读懂了 ABI 规范原文。System V ABI Section 3.2.2 明确要求“the stack pointer must be 16-byte aligned prior to any call instruction”。很多学生只知subq $32,不知为何是 32 而非 16 或 64。这一行注释,就是你和抄代码者的分水岭。
最后说句实在话:我当年做这个设计时,卡在if-else标签跳转整整 36 小时,最后发现是yacc文法中%prec TK_IF写错了位置。后来每次带学生,都让他们先用gcc -S生成参考汇编,再逐行对齐——这招比读 100 页龙书管用。希望帮到你。
本文还有配套的精品资源,点击获取