
简介lcc42 是《可变目标C编译器设计与实现》一书的配套源码面向计算机专业学生、编译器爱好者和系统软件开发者。源码包可在 VC6 环境下编译方便读者对照教材理解编译器前后端与目标代码生成机制。包体仅 924KB共 459 个文件含 78 个 C 源文件、72 个头文件并包括 YACC 语法文件、汇编文件及 dsp/dsw/vcproj/sln 等工程文件附 makefile 与 readme 说明目录结构清晰Windows 下可直接打开工程编译。目前已有 222 人学习下载。借助教材与源码读者能完整运行并跟踪解析、语义分析、中间代码生成与优化等关键流程还可利用包内 wf、switch、array、paranoia 等示例验证编译器行为适合作为深入学习编译器构造的可靠参考。 如果你手里正好有一份lcc42的源码而且你还装着一个十多年没动过的VC6那我建议你别急着删。把lcc42在VC6里完整编译一遍能让你透彻理解“编译器到底是怎么做出来的”甚至比看十遍《编译原理》教材都有用。lcc本身是Chris Fraser和David Hanson两位大佬写的轻量级C编译器最终发布版本停在4.2用不到两万行代码实现了预处理、词法分析、语法分析、中间代码生成、优化和多种后端结构相当紧凑。这条实践路径特别适合正在啃编译原理的学生、维护老工具链的嵌入式开发者以及所有希望通过源码构建来验证编译器设计思路的人。1. lcc42源码到底是个什么结构1.1 一份源码三套可执行程序我第一次拿到lcc的源码包时也很疑惑为什么目录里既有src又有cpp和etc好像东西很杂。实际理清楚之后就明白了lcc在设计上就分三层真正干活的预处理器cpp、编译器本体rcc以及最外层的驱动lcc。驱动负责解析命令行参数、把源文件依次交给cpp和rcc用户平常执行的lcc其实是这层包装rcc才是做词法、语法、语义分析和代码生成的“幕后黑手”cpp则单独负责宏展开和头文件包含。三者分别编译成三个exe再通过路径搜索组合起来。目录结构和关键文件大概是这样的目录主要文件在构建中的角色cpp/cpp.c、cpp.h预处理器源码可单独编译为 cpp.exesrc/lex.c、parse.c、sym.c、expr.c、stmt.c、tree.c、gen.c、dag.c、output.c编译器本体源码链接为 rcc.exesrc/x86/部分版本win32.c、linux.c后端代码负责生成目标平台汇编etc/lcc.c驱动程序源码链接为 lcc.exeinclude/stdarg.h、stddef.h 等lcc私有头文件集合lib/运行时支持部分一般不需要手工编译这六个目录里最核心的是src下那一堆C文件。它们之间不是平行关系而是严格分层的lex.c把字符流切分成tokenparse.c按文法把token拼成语法树sym.c和types.c维护符号表与类型信息tree.c再生成中间表示树最后gen.c和dag.c负责指令选择与寄存器分配输出汇编代码。每一个文件对应编译过程的一个阶段这种模块化程度在真实编译器里算是相当收敛的。1.2 为什么说lcc是“教科书级编译器”lcc的代码风格是典型的C89风格这一点恰好和VC6的C编译能力互相匹配。阅读左边那一列文件时其实就等于在读一遍龙书里的实现lex.c负责词法分析parse.c负责语法分析sym.c和types.c负责符号表与类型系统tree.c、gen.c、dag.c负责中间表示和指令选择后端再根据平台输出汇编。整个流程是单向、无循环依赖的阅读顺序非常友好。更重要的是lcc内部自带一套“自举”机制。它先由一个宿主编译器这里就是VC6的cl.exe把源码编成可执行文件然后用这个可执行文件去重新编译自己的源码如果两次编译出来的行为一致就说明这个编译器能“自己生自己”。这一趟流程走下来远比背下一堆Bison/Yacc语法更能理解编译器是如何闭环工作的。lcc选择C89作为自身实现语言不是因为它保守而是为了让编译器可以在最广泛的宿主环境下构建——这在今天依然是很聪明的设计。2. 准备工作让VC6能用的第一步2.1 获取源码和目录要求获取lcc42源码一般有两个途径一个是官方在GitHub上维护的drh/lcc仓库里找4.2版本tag另一个是各种老软件站上的源码包。拿到后先解压我强烈建议你解压到纯英文且没有空格的路径下比如D:\lcc42。不要放在C:\Program Files\...这种带空格的目录里否则后面的驱动脚本和批处理命令都要被折磨lcc这种老代码对带引号的路径处理得很不优雅。解压后先花五分钟翻一下README和doc目录。编译前读文档这件事很多人觉得是浪费时间但lcc的构建顺序、平台宏定义、需要的命令行参数文档里基本都有说明。当年我偷懒跳过了这一步结果在#define上浪费了大半个下午。文档里还会明确写出当前版本支持哪些目标平台像x86、MIPS、SPARC都有但不同版本的构建入口略有差异这些信息都藏在doc里。2.2 把VC6的命令行工具装进PATHVC6安装好后默认不会把cl.exe、nmake.exe塞进系统的全局PATH。你需要先打开一个“命令行编译环境”。最标准的方法是找到VC6安装目录下的VCVARS32.BAT用命令行执行它cd C:\Program Files\Microsoft Visual Studio\VC98\Bin VCVARS32.BAT如果你的机器上装的是VC6SP6一般路径就在C:\Program Files\Microsoft Visual Studio\VC98。执行完后用cl命令验证一下cl /?能看到编译器版本信息就说明环境OK。如果提示“不是内部或外部命令”说明脚本没跑成功要检查路径是否正确。这一步搞定后VC6的编译器、链接器、头文件路径就都进当前命令行了下面编译lcc时就不用手动配INCLUDE和LIB。注意VCVARS32.BAT只对当前命令行窗口生效每开一个新窗口都要重跑一次。偷懒的办法是写一个vc6env.bat放在固定位置内容就是一行call C:\Program Files\Microsoft Visual Studio\VC98\Bin\VCVARS32.BAT以后每次要编译前先执行它这样能省下不少重复输入的麻烦。2.3 构建顺序先cpp再rcc后lcc阅读完文档后你会发现lcc的官方构建不是一把梭而是讲究顺序的。必须先编译出预处理器cpp因为后续编译scanf/printf等复杂头文件时要用到它再编译编译器本体rcc最后编译最外层的驱动lcc。把这三个exe都生成好lcc才算能真正工作。这个顺序不是硬性依赖——cpp并不参与编译rcc的源码——但先编译cpp会让你在验证环境时更早暴露问题排查起来也简单。如果cpp都编译不过说明你的命令行环境或者头文件路径配置有问题这时候去调rcc只会更加混乱。先易后难这是老程序员构建多组件项目时的通用策略。3. 亲手把它编译出来3.1 第一步编译预处理器cpp进入lcc源码包里的cpp目录直接敲cd D:\lcc42\cpp cl /c /I..\include /Dwin32 cpp.c link /out:cpp.exe cpp.obj也可以用一行命令完成编译和链接cl /Fe:cpp.exe /I..\include /Dwin32 cpp.c这里面的几个参数要注意/I..\include告诉cl去lcc的include目录下找头文件/Dwin32是告诉预处理器当前目标平台是Windows 32位。如果编译时遇到unistd.h找不到或者某些POSIX函数未定义不要慌这是正常现象lcc在Windows下本来就是靠条件编译切换到win32分支的。如果确实报错优先检查宏名是win32还是_WIN32以源码里实际判断的分支为准。3.2 第二步编译编译器本体rcc回到src目录老规矩cd D:\lcc42\src cl /c /I..\include /Dwin32 /D_WIN32 *.c link /out:rcc.exe *.obj如果想让链接参数更明确也可以这样cl /Fe:rcc.exe /I..\include /Dwin32 /D_WIN32 *.cMSVC的cl.exe是会自行展开*.c通配符的所以这里可以直接用通配符。如果你源码包的x86后端目录下有额外文件比如src\x86\win32.c在编译时把那几个文件也带上但通常4.2版本在前端里已经做好了平台分支win32.c会被条件编译包含不需要额外指定。提示如果在编译时看到warning C4013: xxx undefined; assuming extern returning int这通常不是致命错误但要留意。如果它是一个库函数的声明缺失会导致链接期出现“未解析的外部符号”这时通常需要给cl加上/D_CRT_NONSTDC_NO_DEPRECATE之类的宏开关或者手动声明函数原型。3.3 第三步编译驱动lcc驱动在etc目录下名字叫lcc.c编译最简单cd D:\lcc42\etc cl /Fe:lcc.exe /I..\include lcc.c驱动本身并不包含复杂的编译逻辑它的任务是解析命令行、定位cpp和rcc并调用它们。所以编译完这个exe之后还要让它在运行时找得到另外两个程序。lcc驱动支持通过环境变量LCCDIR指定工具链根目录你可以在命令行里这样设置set LCCDIRD:\lcc42 set PATH%LCCDIR%\cpp;%LCCDIR%\src;%PATH%具体变量名以你下载版本的源代码为准但思路一致让lcc.exe能找到cpp.exe和rcc.exe。如果你不想每次编译前都set一遍也可以把这三行写进一个build_lcc.bat以后一键加载。3.4 自举验证用lcc编译lcc工具链齐了最激动人心的环节来了——自举验证。随便写个hello.c用lcc编译cd D:\lcc42 lcc -o hello.exe hello.c hello.exe能看到输出就说明基本链路由驱动到预处理器再到编译器本体都通了。更硬核一点的验证方式是用新生成的lcc.exe把lcc源码再编译一遍lcc -o rcc_self.exe src\*.c如果这个rcc_self.exe也能正常编译一个测试文件那就说明这套编译器已经从“被VC6编译”进化到“能自己编译自己的源码”自举闭环达成。严格的构建流程还会把rcc和rcc_self做字节级对比但Windows下由于时间戳和PDB等问题跑功能测试就足够说明问题。3.5 安装布局如果你打算长期使用lcc42建议把三个exe集中到一个目录比如D:\lcc42\bin然后把LCCDIR指向它。后续为lcc编写并安装标准库头文件、运行时库时这一布局也更清晰。我自己的习惯是保留一份“原始编译日志”把第一次成功构建的全部命令和输出保存为文本文件后续一旦改坏某个文件照着日志回滚就非常快。4. 常见问题排查与避坑记录4.1 cl命令不存在现象在CMD里敲cl提示“不是内部或外部命令”。这八成是没有先执行VCVARS32.BAT。记住这个脚本只对当前命令行窗口生效每开一个新窗口都要重跑一次。偷懒的办法是写一个vc6env.bat然后放在固定位置但每次开新窗口手动执行其实也不费事。4.2 头文件找不到现象编译时报fatal error C1083: Cannot open include file: stdio.h: No such file or directory。检查两处一是VC6的环境变量INCLUDE是否包含C:\Program Files\Microsoft Visual Studio\VC98\Include二是lcc自带的D:\lcc42\include路径是否通过/I传给了cl。如果两处都对把/I换成带引号写法再试老式程序对路径中的空格确实容易出问题。4.3 源码里有long longVC6不认现象编译lcc源码时出现error C2061: syntax error : identifier long之类的报错。VC6时代的C编译器并不把long long当作标准类型。lcc自己的源码为了能在各种老编译器上构建通常已经规避了这个问题但在某些移植版本里会出现。最简单的替代方案是把源码里的long long统一替换成__int64或者用typedef先行声明typedef __int64 long long;注意这类问题只出现在“用VC6编译lcc源码”的阶段lcc所生成的代码是否支持long long是另一回事。4.4 链接期一堆未解析外部符号现象编译通过但链接时大量unresolved external symbol _xxx。新手最容易忽略的是没有指定正确的运行时库。VC6字符界面链接时默认需要匹配控制台程序的启动代码。如果rcc.exe链接失败先确认是不是用了/SUBSYSTEM:CONSOLE再确认源码里确实有main函数。lcc的编译器本体和驱动都有各自的main编译时不要把它们都塞进同一个EXE链接命令要按我前面给的rcc、lcc分组来不要一股脑混在一起。错误类型典型提示排查手法环境变量缺失cl 不是内部或外部命令重新运行VCVARS32.BAT头文件路径cannot open include file检查INCLUDE与/I参数语法错误C2059/C2061优先检查平台宏与C89兼容性链接错误unresolved external symbol检查入口、子系统、是否混用了main运行找不到工具cant find cpp设置LCCDIR检查PATH4.5 lcc跑起来后提示找不到cpp或rcc现象编译命令敲下去了但报错内容变成类似Cant open cpp: No such file or directory。这是驱动lcc.exe编译好了但它在运行时不了解工具路径。老版本lcc的驱动会通过环境变量或编译时写死的路径去搜cpp和rcc最常见的原因是LCCDIR没有设置正确或者三个exe放得太分散。把cpp.exe和rcc.exe放到与lcc.exe相同或明确的子目录下再核对LCCDIR即可。4.6 换行符/编码导致的怪异行为如果你在Windows上用记事本打开过Linux换行的源码文件编译报错行号与实际不符的情况就会发生。lcc源码本身多是Unix换行LFVC6一般也能读但一旦源码被其他工具转成CRLF个别字符串和宏拼接可能出现诡异报错。遇到这种情况统一用VS Code或Notepad把文件转成CRLF或保持LF再重新编译问题通常在十分钟内解决。5. 编译成功之后的深入学习方向到这里lcc42在VC6下编译的完整闭环已经走完。说句实话整个流程并不复杂真正值钱的不是那几个命令而是你看着一个编译器从零开始运行起来再用它编译自己源码时的那个瞬间。接下来如果你还想深入我建议沿着两条线走。一条是“读源码”把lex.c、parse.c、tree.c这三块串起来对照龙书章节理解词法分析、自顶向下语法分析、中间代码生成每看懂一个文件你就离“看懂编译器”更近一步。另一条是“改源码”试着给lcc加一种新的语法糖或者把x86后端换成ARM Thumb指令集的简化版本这种改动哪怕只跑通一个测试程序也比单纯做几道编译原理刷题更有成就感。最后分享一个小技巧编译这套源码时建议全程打开一个日志文件把每一步的命令和输出记录成文本。因为我后来尝试改后端时就发现无论你把代码改成什么样回滚到“VC6下能编译”的基线永远是最稳妥的排障方式。保存好这套编译日志等于给你的老工具链备了一份保险。本文还有配套的精品资源点击获取