简介:这份《自制编程语言》配套资料面向希望理解语言底层运行机制的开发者与计算机专业学生,围绕语言设计、实现与使用三条主线展开,帮助读者打通从语法规则到可执行程序的完整认知链路。压缩包内仅有1个PDF文件,体积约2.38MB,属于轻量级阅读材料,适合集中通读,也便于在动手实践时随时对照查阅。内容覆盖词法分析与语法分析的基本思路、编译器与解释器的分工、Crowbar 与 Diksam 两个示例语言的版本演进及垃圾回收改造,并穿插 yacc/lex、bison/flex、MinGW 等工具链的安装与配置说明;同时对比 Linux 与 Windows 下 make.sh、make.bat 的构建差异,点出跨平台编译中常见的环境问题。读者可据此建立从语言规范、解析器生成到运行时管理的知识框架,为后续自行实现小型语言积累可复用的参考。目前已有870人学习下载。
1. 从一堆编译不过的 ZIP 说起:这套自制编程语言资料真正解决什么问题
自制编程语言这个话题,卡住大多数人的从来不是语法理论,而是"我把书上代码敲完了,make 报了一屏错,然后就没有然后了"。这套相关资料的价值恰好在这里:它不是一份理论讲义,而是一组带版本号的、能跑起来的最小实现快照——crowbar 从 book_0_1 到 book_0_4,Diksam 同样四个版本,外加 calc、mycalc、llparser 三类前置练习,每个目录里都自带 Makefile、make.sh、make.bat。你不需要先想清楚"我要设计一门什么样的语言",先把 0_1 编出来、跑通 test.crb,理解一个解释器从 .l/.y 到可执行文件的最短路径,比读十篇综述都管用。
它适合两类人:一类是写过业务代码、想知道for循环到底怎么被翻译成字节码或直接求值的工程师;另一类是被龙书劝退过、想找一份能 debug 的小体量源码的在校生。不适合谁也想说清楚——如果你要的是 LLVM 那套工业级后端、或者想直接拿现成框架生成一门生产语言,这套资料会让你失望,它连代码优化都基本没碰。它就是"教学用但真能跑"的定位,而恰恰是这个定位,让它成为少数你能在一周内读完主循环源码的编译器项目。
2. 拆包先看骨架:crowbar 与 Diksam 两条演进线的分工
拿到压缩包解压后,第一件事不是急着 make,而是把目录树看清楚。这一步偷懒,后面所有的报错你都会归错因——很多人以为是环境问题,其实是跑错了目录,进了_ex或者进了只有源码没有 Makefile 的中间态目录。
2.1 目录结构与版本递进:book_0_1 到 book_0_4 各加了什么
解压出来的顶层大致是这样的结构,我按"你会先碰哪个"排序:
| 目录 | 定位 | 依赖 | 建议顺序 |
|---|---|---|---|
| calc | 极简表达式求值器 | yacc/lex | 第 1 个看 |
| mycalc | 带变量与赋值的小计算器 | yacc/lex | 第 2 个看 |
| mycalc_ex | mycalc 的习题解答 | 同上 | 卡住时对照 |
| llparser | 手写 LL(1) 递归下降版本 | 纯 C,无生成器 | 第 3 个看 |
| llparser_ex | llparser 的习题解答 | 同上 | 对照用 |
| crowbar_book_0_1 ~ 0_4 | 动态类型脚本语言四个快照 | bison/flex | 主线 |
| diksam_book_0_1 ~ 0_4 | 静态类型语言四个快照 | bison/flex、oniguruma | 进阶 |
版本递进这件事,别去背"0_3 加了 GC、0_4 加了类"这种二手结论,最可靠的办法是直接 diff 两个相邻目录。这份资料的好处是每个快照都是完整可编译的独立工程,不是 patch,所以 diff 出来的每一条都是真金白银的语言特性增量:
# 在解压后的根目录执行,只看新增的源文件和被改动的函数 diff -ru crowbar_book_0_2 crowbar_book_0_3 | head -200 # 只想看新增了哪些 .c/.h,跳过庞大的上下文 diff -rq crowbar_book_0_2 crowbar_book_0_3第一条命令的-r是递归,-u是统一格式(可读性最好),head -200是为了不让终端被刷爆——两个快照之间的差异动辄几千行,先看前 200 行基本能判断这次改的是词法、语法还是运行时。第二条-q只输出"哪些文件不同",适合先建立全局印象。我一般会先跑第二条,看到新增了execute.c里一大坨、或者多出一个heap.c,就知道这一版动的是执行器和内存管理,然后带着这个预判去读源码,效率比盲读高一个量级。
crowbar 这条线的设计取向是"动态类型 + 树遍历解释":变量不需要声明类型,值在运行时带着类型标签走,执行器直接对着抽象语法树递归求值,没有中间字节码。这个选择对学习者极其友好——你不需要先理解栈式虚拟机的调度,就能看到eval()如何对着一棵Expression树算出一个Value。代价是慢,但这个项目本来就不追求快。Diksam 则反过来,静态类型、需要声明、要做类型检查,代码量明显更大,配套还要链 oniguruma 做正则字面量,-lonig这个链接参数就是它带来的。
2.2 calc、mycalc、llparser:三个前置练习各自练的是什么
很多人跳过这三个目录直接冲 crowbar,然后在%start、%token、移进归约冲突里卡三天。这三个练习不是热身运动,它们各自对应一个独立的认知台阶。
calc 只做一件事:把1+2*3这样的表达式串变成结果。它小到你可以把.y文件一行一行读完,理解%%分隔的三段结构——声明段放 token 和优先级,规则段写expr: expr '+' term,用户代码段放main和yyerror。这里最关键的是优先级和结合性的写法:
/* calc 里 %left / %right 的典型写法,顺序即优先级,越靠后越高 */ %left '+' '-' %left '*' '/' %right UMINUS %% expr: expr '+' expr { $$ = $1 + $3; } | expr '*' expr { $$ = $1 * $3; } | '-' expr %prec UMINUS { $$ = -$2; } | '(' expr ')' { $$ = $2; } | NUMBER { $$ = $1; } ;%left表示左结合(1-2-3算成(1-2)-3),%right右结合,书写顺序决定优先级——写在后面的优先级更高,所以*在+之后,乘法先算。%prec UMINUS是给一元负号强行指定优先级,不然-1*2会被解析成-(1*2),虽然结果一样但语义树不对,遇到-2+3就露馅了。这段代码之后,你应该能回答一个问题:为什么1+2*3不需要括号也正确?因为移进/归约冲突被优先级表裁决掉了。
mycalc 在 calc 基础上加了变量、赋值和符号表。这一步加进来的东西,就是 crowbar 里SymbolTable、Variable那些结构的雏形。看 mycalc 时重点盯两处:赋值语句的语法规则怎么写(IDENT '=' expr),以及符号表用什么数据结构存(通常是线性数组或简单哈希,因为变量少)。mycalc_ex 是配套习题解答,卡住的时候对照,但别一上来就抄——解答里有些写法是为了练习题服务的,未必是最优工程实践。
llparser 是最容易被低估的一个。它把同样的表达式求值,用手写递归下降重写了一遍,不用 yacc/lex。手写解析器的好处是没有生成器的黑盒,你能清清楚楚看到"当前读到的 token 是什么、往前进一个、失败就回溯"。这份资料把它和 mycalc 并排放,目的就是让你对比两条路线:生成器省事但报错信息晦涩、调试靠-v生成的output文件;手写可控但工作量大、左递归要手动消除。llparser_ex 同样是解答版。
学过这一圈再进 crowbar,你对crowbar.y里的文法规则就不会有陌生感,看到%%就知道该找什么。
2.3 为什么这套实现用 C89 而不是 C++ 或脚本语言
打开 Makefile 你会看到一串编译参数:-Wall -Wswitch-enum -ansi -pedantic -DDEBUG。-ansi和-pedantic组合起来就是强制 C89 标准并禁止任何扩展,-Wswitch-enum要求 switch 覆盖枚举的所有取值(漏一个就警告),-DDEBUG打开调试分支。这套参数不是随手写的,它保证了代码在任何符合标准的 C 编译器上行为一致——这在教学场景里非常重要,因为读者用的可能是 MinGW、Cygwin、clang、gcc 各种组合。
用 C 而不是 C++,核心原因是让读者"看得见内存"。crowbar 的对象、字符串、符号表都是手动 malloc/free,Diksam 还要自己做 GC,这些在 C++ 里会被智能指针和容器藏起来。教学项目最怕的就是关键逻辑藏在标准库后面。代价是代码里到处是malloc之后的判空和free之前的置 NULL,读起来啰嗦,但每一个内存动作都摊在明面上。
再一个现实原因:这本书写作年代较早,配套代码对编译器的要求停留在"有 gcc 就行"。这直接导致了后面 Windows 用户的一大半痛苦——MinGW 装好了但没 bison,bison 装了但路径带空格,路径没问题了但 make 的名字叫 mingw32-make。这些坑我们在第 3、5 章细说。
3. 把 bison/flex 拉进 Windows:MinGW 与 Cygwin 的取舍
Linux 用户到这一步基本可以跳过——apt install bison flex gcc make之后直接 make 就完事。Windows 用户才是这套资料真正的门槛所在,而门槛不在代码,在工具链的组装方式。这一章把两条可行路线摊开讲,包括各自的代价。
3.1 MinGW 路线:安装、gmake 命名与 mingw32-base
MinGW 提供的是"Windows 上的 gcc",编译产物是原生 exe,不依赖任何额外的 DLL 运行时(除了系统自带的 msvcrt)。这对分发和调试都友好。安装流程大致是:下载安装器,在 Installation Directory 里选一个不含空格的路径(比如D:\MinGW),组件列表里至少勾上mingw32-base(含 gcc、binutils、mingw32-make)和mingw32-gcc-g++(有些示例会用到),然后 Apply Changes 等它下载完。
装完之后D:\MinGW\bin要进 PATH。这一步的验证方式:
# 打开一个新的 cmd 窗口(必须新开,PATH 才会生效) gcc --version mingw32-make --version注意第二个命令的名字。MinGW 把 make 改名成mingw32-make.exe是为了避免和别的 make 冲突,但这份资料的 Makefile 里写的是gmake。所以你要么在D:\MinGW\bin里复制一份改名,要么每次手敲全名:
# 方式一:复制一份改名为 gmake.exe(推荐,一劳永逸) copy D:\MinGW\bin\mingw32-make.exe D:\MinGW\bin\gmake.exe # 方式二:不改名,直接调用 cd /d D:\work\crowbar_book_0_1 D:\MinGW\bin\mingw32-make.execd /d里的/d是允许跨盘符切换,很多人从 C 盘切到 D 盘时忘了这个参数,结果还在原目录里跑 make,报 "No targets specified and no makefile found"——这个报错 90% 不是 Makefile 的问题,是你站错地方了。
MinGW 自带的组件里没有 bison 和 flex。这两个要单独从 GnuWin32 那套包里下,各自有独立的安装器,选 "Complete package, except sources" 就行。装完之后同样要把它们的 bin 目录加进 PATH。这里埋着本套资料里出现频率最高的一个坑,第 5 章第一条专门讲。
3.2 Cygwin 路线:能跑通,但你要接受 cygwin.dll
Cygwin 走的是另一条路:它在 Windows 上模拟了一套 POSIX 环境,configure、make、bash、m4全都是原生可用的,装 bison/flex 只需要在安装器里勾选Devel分类下的make、gcc,以及Interpreters分类下的m4(bison 依赖 m4 做宏展开,这个依赖关系很多人不知道,装完 bison 不装 m4 就会遇到一连串 m4 打不开文件的报错)。
Cygwin 的优势是省心,./configure && make这套流程原样可用,不需要你操心 make 叫什么名字。代价有两个:一是编译出来的 exe 依赖cygwin.dll,把这个 exe 拷到没装 Cygwin 的机器上就跑不起来;二是路径语义不同,Cygwin 里的/usr/local映射到 Windows 目录树上某个位置,make install经常装到你意想不到的地方。
如果你的目标只是"把 crowbar 编出来看看效果",Cygwin 更快。如果你想把这套代码当长期读物、反复改反复编,我建议 MinGW——原生 exe、调试器衔接顺畅、没有 DLL 依赖的干扰。
两条路线不要混用。用 Cygwin 的 gcc 编译、再用 MinGW 的 ranlib 处理产物,或者反过来,会得到一些非常诡异的报错,比如找不到-lmsvcp60,或者 .a 文件明明在却链接不上。第 5 章会讲这个具体症状。
3.3 oniguruma 5.9.4 的编译:ranlib 与 make install 的路径陷阱
Diksam 这条线要链 oniguruma(正则表达式库),版本是 5.9.4。Linux 下三步走完:
# Linux 下的标准三步,注意最后一步可能要 sudo wget http://example.invalid/onig-5.9.4.tar.gz # 实际地址见资料内附链接 tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure make make installWindows 下就麻烦得多,因为./configure是 shell 脚本,得在 bash 里跑,但你又希望它用 MinGW 的 gcc 编译出原生库。混合环境的典型做法是:用 Cygwin 的 bash 执行./configure(让它生成 Makefile),配置时指定CC为 MinGW 的 gcc。这样能跑通大半,但会在两个地方翻车。
第一处是make install。Cygwin 的 shell 把/usr/local/lib解释成 Cygwin 根目录下的路径,而 MinGW 的ranlib.exe把它解释成D:\MinGW\...或者干脆不认,于是出现这种报错:
libtool: install: ranlib /usr/local/lib/libonig.a D:\MinGW\bin\ranlib.exe: '/usr/local/lib/libonig.a': No such file Makefile:415: recipe for target 'install-libLTLIBRARIES' failed第二处是.libs目录下的静态库没有被正确索引。绕过办法是不走make install,手动把库建好索引再拷到 MinGW 的 lib 目录:
# 在 onig-5.9.4 目录下,用 bash 手动完成安装的最后一步 cd .libs ranlib libonig.a # 生成符号索引,缺这一步链接时会报 undefined reference cp libonig.a /d/MinGW/lib/ # 拷到 MinGW 的库搜索路径 cp ../oniguruma.h /d/MinGW/include/ # 头文件同理ranlib的作用是给静态库生成符号表(armap),没有它,gcc -lonig能找到文件但找不到符号,报一堆 undefined reference,而你会以为是函数名写错了。cp的目标路径按你自己的 MinGW 安装位置改,路径里带空格会再次触发问题,这也是为什么前面强调装到D:\MinGW这种干净路径。
做完之后验证一下,写个两行的测试程序链一下-lonig,能过就说明库装对了。别跳过这个验证直接去编 Diksam,否则后面出错你分不清是库的问题还是代码的问题。
4. 第一次 make:从 .y/.l 到可执行文件的完整链路
环境齐了,进目录敲 make,但在此之前你得知道 make 到底会替你做什么。这份资料的 Makefile 不复杂,看懂依赖顺序,报错时你才知道是哪一环断了。
4.1 编译链路:bison 生成什么、flex 生成什么
一个用 bison/flex 写的解释器,构建过程是两段式的:先生成 C 源码,再编译 C 源码。Makefile 里体现为两组规则。
第一组是生成器规则:
# crowbar 的 Makefile 片段(节选,实际行号会因版本不同略有差异) y.tab.c y.tab.h: crowbar.y bison --yacc -dv crowbar.y lex.yy.c: crowbar.l flex crowbar.lbison --yacc -dv crowbar.y里,--yacc让输出文件名用 yacc 的传统命名(y.tab.c/y.tab.h)而不是 bison 默认的crowbar.tab.c,这样源码里的#include "y.tab.h"才找得到;-d是生成头文件(里面是 token 的宏定义),-v是生成y.output,记录所有状态和冲突,调试文法时全靠它。flex crowbar.l生成lex.yy.c,里面是yylex()函数。
第二组是编译规则,把生成的 C 和各手写模块逐个编成 .o:
# 典型的批量编译规则,$< 是第一个依赖,$@ 是目标 %.o: %.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG $< -o $@那串参数值得逐个说清楚:-c只编译不链接;-g带调试信息(后面 gdb 要用);-Wall开常用警告;-Wswitch-enum要求 switch 覆盖枚举全部取值,写解释器时这个警告能提前抓出漏掉的分支;-ansi -pedantic锁死 C89;-DDEBUG定义 DEBUG 宏,代码里#ifdef DEBUG的断言和打印才会生效。
最后一组是链接,把所有 .o 合成可执行文件。如果这一版依赖 oniguruma,链接行会多一个-lonig。
4.2 一次完整的构建实录
假设你在D:\work\crowbar_book_0_1,MinGW 装在D:\MinGW:
# 1. 进目录(注意 /d 跨盘符) cd /d D:\work\crowbar_book_0_1 # 2. 先看看有没有 Makefile,别在空目录里瞎敲 dir Makefile # 3. 执行构建,用你复制出来的 gmake,或者全名 gmake # 4. 如果失败,先清理再重来,避免半成品 .o 干扰判断 gmake clean gmake成功的输出长这样(顺序大致固定:先 bison、再 flex、然后一串 gcc -c、最后链接):
bison --yacc -dv crowbar.y flex crowbar.l gcc -c -g lex.yy.c gcc -c -g y.tab.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG main.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG interface.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG create.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG execute.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG eval.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG string.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG string_pool.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG util.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG native.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG error.c gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG error_message.c gcc -o crowbar *.o这份输出本身就是一张地图:create.c建 AST,execute.c跑语句,eval.c算表达式,string.c和string_pool.c管字符串(后者是字符串驻留池,用来让字符串比较变成指针比较),native.c挂内置函数,error.c和error_message.c管报错。你后面读源码时,按这张地图跳文件,比从 main.c 顺序读快得多。
gmake clean这一步别省。中途失败过一次的话,目录里会留下半截 .o 和过期的y.tab.h,下次 make 看时间戳以为不用重编,于是你对着同一份旧产物反复怀疑人生。清理重来是成本最低的排除法。
4.3 用 test.crb 做第一次运行验证
编出 exe 只是第一步,能跑通示例脚本才算环境真的没问题:
# 运行自带测试脚本 ./crowbar test/test.crb # 或者按 Windows 的习惯 crowbar.exe test\test.crb如果输出正常,恭喜,工具链这关过了。如果报错,注意报错信息的形态:这套实现里error.c会打印行号和错误类型,比如"第 5 行:非法字符(0x0d)"。看到 0x0d 别怀疑代码,那是换行符问题,第 5 章专门讲。看到"语法错误"但行号是对的,多半是.y文件没重新生成,回到上一步gmake clean。
跑通之后建议立刻做一件事:随便改一下test/test.crb,加两行你自己写的小程序——一个变量赋值、一个 if、一个循环。这一步的目的是确认"改脚本不用重新编译"这个事实,也就是确认你手上的是解释器而不是编译器。很多人跑完示例就以为完事了,其实还没真正理解这套东西的运行模型。
5. 排错手册:这几类报错几乎每个 Windows 用户都会撞上
这一章的内容全部来自实际构建过程里反复出现的失败模式。每条按"现象 → 原因 → 解决"写,你可以当查询表用。
5.1 bison 装在带空格的路径里,m4 集体打不开文件
现象很唬人,一次刷出五六行:
m4: cannot open `Files': No such file or directory m4: cannot open `(x86)\GnuWin32\Bison/share/bison': No such file or directory m4: cannot open `D:\Program': No such file or directory m4: cannot open `(x86)\GnuWin32\Bison/share/bison/m4sugar/m4sugar.m4': No such file or directory原因:bison 在执行时要把自己的 m4 宏文件路径作为参数传给 m4,而这个路径是拼接出来的字符串,没有加引号。装在C:\Program Files (x86)\GnuWin32下时,路径里的空格把参数切成了三段,m4 分别去找D:\Program、Files、(x86)\...,自然全找不到。这是老版本 GnuWin32 打包方式的固有缺陷,不是你操作错了。
解决:把 bison 重装到不含空格的路径,比如D:\GnuWin32,然后更新 PATH。如果不想重装,可以设置环境变量BISON_PKGDATADIR指向真实的 share/bison 目录,让它绕开自动拼接:
# 临时生效,在当前 cmd 窗口里设置 set BISON_PKGDATADIR=D:\GnuWin32\share\bison # 永久生效(需要管理员权限的 cmd) setx BISON_PKGDATADIR "D:\GnuWin32\share\bison" /Msetx写进的是系统环境变量,/M表示机器级,不加就是当前用户级。改完必须新开窗口验证。
5.2 y.tab.h 找不到,其实是 bison 根本没跑成功
现象:
mycalc.l:3:19: error: y.tab.h: No such file or directory gcc: y.tab.c: No such file or directory原因有三种,得按顺序排除。第一种,bison 那一步失败了(比如上一条的 m4 问题),Makefile 继续往下走,flex 生成的lex.yy.c里#include "y.tab.h"就找不到头文件。第二种,你在 mac 或 Linux 上手动敲了cc lex.yy.c -o example -ll,跳过了 bison 这一步——生成器不会自动运行,得先bison -d xxx.y。第三种,-d参数漏了,bison 只生成了y.tab.c没生成头文件。
解决:先单独跑生成器,看它自己报什么错:
# 单独验证 bison 是否能正常工作 bison --yacc -dv crowbar.y ls y.tab.c y.tab.h # 两个文件都得在 # 再单独验证 flex flex crowbar.l ls lex.yy.c这两条能过,问题就在 Makefile 的依赖顺序或者清理不彻底,gmake clean重来。过不了,问题在工具链,回到上一条。
5.3 换行符、编码与 0x0d 报错
现象:脚本明明看着没问题,运行时报"第 5 行:非法字符(0x0d)",或者更隐蔽——不报错,但字符串比较永远不相等。
原因:Windows 编辑器保存文件时用 CRLF(\r\n),而这套词法分析器的空白处理只认\n和空格,\r被当成非法字符。另一个来源是文件编码,源码里如果混进了 Shift-JIS 或 GBK 的中文注释,在某些编译器下会连带影响行尾字节的解析。
解决:转成 LF。Linux 下一条命令:
# 批量转换,先备份 dos2unix test/test.crbWindows 下没有 dos2unix 的话,用 Git Bash 自带的,或者用编辑器手动切(VS Code 右下角点 CRLF 改成 LF)。这一步的教训是:这套代码的所有源文件和测试脚本都应该是 LF,包括.c、.h、.y、.l、.crb。我见过有人在.y文件里混了 CRLF,bison 报的错指向一个完全无关的行号,排查了两小时。
提示:下载压缩包解压后,先全量检查一遍换行符,比出事之后再回头找便宜得多。
5.4 2.5 节配套版本里 lexicalanalyzer.c 的未初始化指针
现象:llparser 或某一版词法分析器跑起来直接崩,或者读到奇怪的行。定位到lexicalanalyzer.c里这段:
static char *st_line; /* ... */ while (fgets(st_line, LINE_BUF_SIZE, stdin) != NULL) { parse_line(); }原因:st_line声明成指针但从未分配内存,fgets往一个野指针写数据,属于典型的未定义行为。在某些平台上是直接段错误,在某些平台上"看起来能跑",那只是运气。
解决:改成栈上数组,或者先 malloc:
/* 方案一:栈上数组,最简单,注意 LINE_BUF_SIZE 别开太大 */ static char st_line[LINE_BUF_SIZE]; while (fgets(st_line, LINE_BUF_SIZE, stdin) != NULL) { parse_line(); } /* 方案二:如果确实需要指针语义,动态分配并检查返回值 */ static char *st_line = NULL; if (st_line == NULL) { st_line = malloc(LINE_BUF_SIZE); if (st_line == NULL) { fprintf(stderr, "out of memory\n"); exit(1); } }改完记得确认parse_line()里没有对st_line做sizeof运算——数组和指针在这一点上行为不同,这是改这类 bug 时最容易引入的次生问题。资料包里这个位置是有勘误的,遇到崩溃先来这里对一眼。
5.5 -lmsvcp60 链接失败与两套工具链混用
现象:
/usr/bin/ld: cannot find -lmsvcp60原因:msvcp60是旧版微软 C++ 运行库,Linux 上根本没有这个 .so,Cygwin 里也未必有。出现这个报错基本可以断定:Makefile 是别人在别的环境下配好的,或者你混用了两套工具链——比如用 Cygwin 的 gcc 去编一个原本为 MinGW 准备的工程,链接参数里还留着 Windows 特有的库。
解决:先确认你用的是哪套编译器,which gcc看路径。然后打开 Makefile 找-lmsvcp60,删掉——这套代码是纯 C89,不需要 C++ 运行库。如果删掉之后出现新的 undefined reference,那才是真正缺的库(多半是 oniguruma,回去看 3.3)。
混用工具链的另一个典型症状是.a文件明明在却链接不上,或者ranlib报文件不存在。判断方法:看你链接的 .a 是用哪套工具链的ar打包的,索引工具就得用同一套。Cygwin 打的包用 Cygwin 的 ranlib,MinGW 的用 MinGW 的。这种事没有捷径,只能保持一套到底。
6. 从跑通到读懂:一套按周推进的源码阅读路线
跑通之后,真正的价值在于读。但直接打开crowbar_book_0_4从 main.c 开始读,多半会在第三个文件放弃——0_4 是最终形态,代码量最大、特性最全,不适合当入口。我给一条自己走过、也推荐给别人的顺序,按周切分,每周的产出都是"能说清楚一件事"。
| 周次 | 目标目录 | 要回答的问题 | 建议动手改的地方 |
|---|---|---|---|
| 第 1 周 | calc | 一个表达式串怎么变成结果 | 加一个%取模运算符 |
| 第 2 周 | mycalc | 变量存在哪、怎么查 | 加一个print语句 |
| 第 3 周 | llparser | 手写解析器和生成器差在哪 | 去掉一层左递归,看文法怎么改 |
| 第 4 周 | crowbar_book_0_1 | 从源码到 AST 再到值的完整路径 | 加一个内置函数 |
| 第 5 周 | crowbar_book_0_3 | 字符串驻留池为什么能让比较变快 | 打印池的命中率 |
| 第 6 周 | diksam_book_0_2 | 静态类型检查在哪一步做 | 让某个类型错误报得更清楚 |
这张表的关键不是周次,是"每周只回答一个问题"。编译器项目最容易犯的错是想一次看懂全部,结果每个文件都翻了两页。第 4 周那个改动最有价值——给 crowbar 加一个内置函数(比如abs),你会被迫走完"在 native.c 里注册 → 在符号表里绑定 → 在调用时找到 → 参数怎么传 → 返回值怎么造"这条完整链路,走一遍胜过读十遍。
验证你读懂了的办法,不是能复述架构图,而是能回答具体的实现问题。比如:crowbar里字符串比较为什么可以直接比指针?答案在string_pool.c——所有字符串进池时做了驻留,相同内容的字符串只存一份,所以指针相等等价于内容相等,比较从 O(n) 降到 O(1)。再比如:create.c和execute.c为什么要分开?因为一个是把 token 流变成树(前端),一个是遍历树求值(后端),分开之后你可以只换前端或者只换后端。这类问题答不上来,说明那个文件还没读透。
再说一个具体的调试技巧。这套代码编译时带了-g,所以可以直接上 gdb,在eval()里下断点,然后单步看一个表达式怎么被求值:
gdb ./crowbar (gdb) break eval (gdb) run test/test.crb (gdb) print *node # 看当前节点类型 (gdb) continueprint *node打出来的结构体里有个type字段,那就是 AST 节点的类型标签。跟着它走一遍 if 语句的执行,比看源码注释清楚得多。用 gdb 的时候记得-g不能少,MinGW 编译出来的 exe 是支持 gdb 的,这一点比 Cygwin 方案舒服。
最后一个坑,也是我踩得最狠的一次:我当年图省事,直接在 0_4 目录里改代码试新特性,改崩了之后想退回 0_3 对照,发现两个目录的 Makefile 和源码结构已经不一样了,根本没法 diff。从那以后我每次都强制把每个版本目录单独复制一份出来改,原始包永远保持只读,改之前先diff -rq确认基线。这套资料的价值恰恰在版本递进,你把它的版本线破坏了,它就退化成一份普通的教学代码了。希望帮到你。
本文还有配套的精品资源,点击获取