1. 从零手写 C 编译器 XCC为什么我选择 AST 而不是边解析边生成写一个能跑通int main(){return 0;}的编译器不难难的是让它支持变量声明、算术运算、if-else、while 循环、函数调用还要生成能在 Linux 64 位上直接汇编链接的代码。我这次用两天时间搭出了 XCC 的基础部分核心思路就一句话先把源码变成 AST再从 AST 生成目标代码。为什么不用「边解析边生成」因为 C 语言里嵌套依赖太多了。比如int k add(global_var i, j);你得先算global_var i再算j然后按 System V ABI 把参数塞进%rdi、%rsi最后call add。如果解析到一半就吐汇编遇到嵌套函数调用、while 循环里的条件跳转寄存器分配和标签管理会乱成一锅粥。AST 相当于先把「要做什么」和「怎么做」解耦语法分析阶段只负责建树代码生成阶段再遍历树、分配栈帧、生成标签。XCC 目前支持的特性包括int/double/char/char*变量声明、赋值、 - * /算术、 ! 比较、if-else、while、全局变量、函数定义与调用、作用域符号表。目标平台是 Linux x86-64用 Flex 做词法、Bison 做语法最终输出output.s再用as和ld链接成可执行文件。整个项目文件结构如下xcc/ ├── lexer.l # 词法分析 ├── parser.y # 语法分析 AST 构建 ├── ast.h / ast.c # AST 节点定义与创建 ├── ast_codegen.h/.c # AST 遍历生成汇编 ├── symbol.h/.c # 符号表全局 局部作用域 ├── type.h/.c # 类型系统 ├── Makefile └── sample1.c # 测试用例我试过用 AI 辅助写这套东西一开始用 deepseek-r1分析能力不错但出码速度确实慢后来切到 Qwen-code生成 AST 节点定义和代码生成模板快了很多而且它会主动建议加测试用例做回归。下面我把可复制的骨架、AST 定义、代码生成模板和排错清单都摊开讲。2. TaoToken 统一 Key 调度 Qwen-code前置配置与项目骨架在开始写代码之前先把 AI 辅助通道配好。我用的是 TaoToken 的统一 Key 调度它能把 Qwen-code 这类模型的请求统一走一个 API 入口省得每个模型单独配 Key。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。配置的核心是三件套Base URL API Key Model ID。如果你用 Claude Code 或者 Cline 这类工具通常在 settings 里填这三个字段。以 Claude Code 的配置为例在~/.claude/settings.json里写{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥, ANTHROPIC_MODEL: qwen-code } }如果你用 Codex 的auth.json格式类似{ base_url: https://taotoken.net/api, api_key: sk-你的TaoToken密钥, model: qwen-code }Cline 的 MCP 配置里也是同样的三件套Base URL 填https://taotoken.net/apiModel ID 填qwen-code。配好之后你在编辑器里让 AI 帮你生成 AST 节点结构体、写代码生成函数请求就会走 TaoToken 的统一通道调度到 Qwen-code。项目骨架的 Makefile 是关键它把 Flex、Bison、GCC 串起来CC gcc CFLAGS -Wall -g OBJS lexer.o parser.o symbol.o type.o ast.o ast_codegen.o all: compiler compiler: $(OBJS) $(CC) -o $ $^ lexer.c: lexer.l parser.h flex -o lexer.c lexer.l parser.c parser.h: parser.y bison -v -d -o parser.c parser.y %.o: %.c $(CC) $(CFLAGS) -c $ -o $ run: compiler sample1.c ./compiler sample1.c as -g output.s -o output.o ld output.o -o output ./output; echo Result: $$? clean: rm -f *.o lexer.c parser.c parser.h compiler output.s output.o output这里有个坑parser.h是 Bison 生成的lexer.l里要#include parser.h拿 token 定义所以 Makefile 里必须让lexer.c依赖parser.h否则第一次编译会报找不到 token。我踩过这个坑顺序错了就得make clean重来。符号表的设计分全局和局部两张表。全局表存函数和全局变量局部表存当前函数的局部变量用scope_level区分作用域。插入局部变量时current_offset从-8开始每次减type_size这样每个变量在栈帧里的偏移就定下来了。symbol_table_enter_scope()和symbol_table_exit_scope()负责作用域进出退出时把该层的符号弹掉。3. AST 节点定义与代码生成模板可复制的核心配置AST 是整个编译器的中枢。我在ast.h里用一个大union把所有节点类型塞进ASTNode结构体节点类型用枚举区分typedef enum { AST_NUMBER, AST_DOUBLE, AST_STRING, AST_VARIABLE, AST_BINARY_OP, AST_UNARY_OP, AST_EXPRESSION_STMT, AST_DECLARATION, AST_ASSIGNMENT, AST_IF_STMT, AST_WHILE_STMT, AST_BLOCK, AST_RETURN_STMT, AST_PROGRAM, AST_STATEMENT_LIST, AST_FUNCTION_DEF, AST_FUNCTION_CALL, AST_PARAMETER } ASTNodeType;每个节点带line_no用于报错。二元操作节点存op、left、right声明节点存data_type、name、initializer、is_global函数定义节点存return_type、name、parameters、body。创建函数统一用ast_create_xxx命名比如ASTNode* ast_create_binary_op(BinaryOp op, ASTNode* left, ASTNode* right, int line_no) { ASTNode* node malloc(sizeof(ASTNode)); node-type AST_BINARY_OP; node-line_no line_no; node-binary_op.op op; node-binary_op.left left; node-binary_op.right right; return node; }代码生成阶段ast_codegen.c里最关键的是类型判断和寄存器选择。整数运算走%rax/%rbx浮点运算走%xmm0/%xmm1。我写了个get_expression_type()递归判断表达式类型遇到AST_BINARY_OP就看左右操作数只要有一个是double就按浮点处理。以x y为例x是inty是double生成的汇编是movq -8(%rbp), %rax # 读 x cvtsi2sd %rax, %xmm0 # int 转 double movsd %xmm0, %xmm1 # 保存左操作数 movsd -16(%rbp), %xmm0 # 读 y comisd %xmm0, %xmm1 # 比较 setb %al # x y 则置位 movzbq %al, %rax # 零扩展函数调用的参数传递按 System V ABI前 6 个整数参数进%rdi、%rsi、%rdx、%rcx、%r8、%r9前 8 个浮点参数进%xmm0-%xmm7。我在ast_generate_function_call()里先把所有参数算出来压栈再按顺序弹到寄存器。这里有个细节参数求值顺序和寄存器分配顺序要分开处理否则嵌套调用会覆盖。栈帧分配用预扫描calculate_local_vars_size()遍历函数体把所有局部变量声明的大小加起来在函数序言里subq $N, %rsp一次性分配。if-else 分支取两边较大值while 循环体单独算。这样比每个变量单独subq $8高效也避免了栈对齐问题。4. 编译运行验证从 sample1.c 到可执行文件写个测试用例sample1.cint global_var 100; char* get_name() { return xxx1; } int add(int a, int b) { return a b; } int main() { int x 1; double y 5.3; while (x y) { x x 1; } char* name get_name(); global_var global_var 20; int i x y; int j 30; int k add(global_var i, j); return k; }执行make run流程是./compiler sample1.c生成output.s然后as -g output.s -o output.o汇编ld output.o -o output链接最后./output运行退出码就是k的值。生成的output.s里数据段先放 double 常量和字符串常量.section .data .align 8 double_const_1: .double 5.300000 string_const_1: .asciz xxx1全局变量global_var也在数据段.data global_var: .quad 100main函数被特殊处理成_start入口这样ld链接后能直接执行。函数序言保存%rbp、设置新栈帧、分配局部变量空间_start: pushq %rbp movq %rsp, %rbp subq $48, %rspwhile 循环用标签实现.L0: movq -8(%rbp), %rax cvtsi2sd %rax, %xmm0 movsd %xmm0, %xmm1 movsd -16(%rbp), %xmm0 comisd %xmm0, %xmm1 setb %al movzbq %al, %rax testq %rax, %rax jz .L1 # 循环体 jmp .L0 .L1:函数调用add(global_var i, j)生成movq global_var(%rip), %rax pushq %rax movq -32(%rbp), %rax popq %rbx addq %rbx, %rax pushq %rax movq -40(%rbp), %rax pushq %rax popq %rax movq %rax, %rdi popq %rax movq %rax, %rsi call add运行后echo $?能看到退出码。如果k算出来是 15110020130说明整条链路通了。5. 常见报错排查清单401、local proxy failed、reading choices、OAuth配 TaoToken 和跑编译器时我遇到过几类典型报错列出来对照排查。401 UnauthorizedAPI Key 没填对或者 Base URL 写成了https://taotoken.net少了/api。检查settings.json或auth.json里的ANTHROPIC_BASE_URL必须是https://taotoken.net/apiKey 以sk-开头。如果 Key 是从别处复制的注意有没有多余空格。local proxy failed本地代理配置冲突。如果你之前配过其他工具的代理环境变量HTTP_PROXY/HTTPS_PROXY可能指向了一个已经关掉的端口。清掉这些环境变量或者确认代理服务在跑。TaoToken 的请求走标准 HTTPS不需要额外代理层。reading choices 报错这通常是模型返回格式和客户端预期不一致。检查 Model ID 是否填的qwen-code有些客户端对模型名大小写敏感。如果用的是 Cline 的 MCP 配置确认model字段和 TaoToken 支持的模型列表对得上。OAuth 相关报错Claude Code 有时会尝试走 OAuth 流程但 TaoToken 用的是 API Key 认证。在settings.json里显式设置ANTHROPIC_API_KEY后OAuth 流程会被跳过。如果还报 OAuth 错检查有没有残留的~/.claude/credentials.json删掉重试。编译器本身的报错也要注意variable x not declared说明符号表查找失败检查声明是否在symbol_table_insert之前too many double constants说明常量池满了把MAX_DOUBLE_CONSTS调大syntax error in statement看 Bison 的parser.output文件定位冲突规则。6. 继续扩展 XCC从基础部分到更完整的编译器XCC 的基础部分跑通后下一步可以加数组、指针运算、结构体、更多控制流for、do-while、switch。AST 的扩展很直接加节点类型、加创建函数、在ast_generate_code的 switch 里加分支。符号表要支持嵌套作用域和类型检查代码生成要处理更复杂的寄存器分配。如果你也想用 AI 辅助写这类项目建议把 TaoToken 的 Coding Plan 用起来长期编码和 Agent 场景下统一 Key 调度能省不少切换成本。模型对话入口在 https://taotoken.net/api 接入文档在 https://taotoken.net/api-keys 需要生成 Key 就去控制台。把 Base URL、Key、Model ID 三件套配好让 Qwen-code 帮你生成 AST 节点和代码模板你专注在类型系统和 ABI 细节上效率会高很多。最后留个实用技巧每次改完代码生成逻辑先跑make test-all三个测试用例覆盖算术、if、while回归通过再跑sample1.c。汇编链接报错时用as -g保留调试信息gdb ./output单步看寄存器比盯着output.s猜快得多。
阅读完成 · 觉得有帮助?