
简介DJGPP 2.04 是一套面向 DOS 环境的开源 C/C 开发工具链主要解决在老式 PC、虚拟机或嵌入式场景中源码编译与程序构建的问题适合复古计算爱好者、系统原理学习者以及需要在 DOS 下开发游戏或系统级软件的开发者。该版本由 GNU 工具移植而来整合了 GCC 编译器、汇编器、链接器、GNU Make、调试器 GDB、库管理器及大量标准头文件和库文件可在没有现代操作系统的条件下完成从编辑、编译到链接的完整开发流程。整套压缩包共 372 个文件以 h 头文件、exe 可执行程序、info 说明文档以及 djl、lib、bat 等辅助文件为主压缩后约 6.95MB体积紧凑便于在早期硬件环境部署。资源与《Linux0.01内核分析与操作系统设计配书光盘》教学场景紧密相关同时附带 man 手册页与示例代码帮助读者理解早期操作系统内核、底层启动流程及 DOS 平台编程方式。目前已有 405 人学习下载对研究编译工具发展史和操作系统教学均有独特价值。1. DJGPP 2.04 是什么一个让 32 位 C/C 在 DOS 里跑起来的编译器套件DJGPP 2.04 这个名字在今天的开发者眼里很陌生但只要你碰 DOS 下的复古游戏、老式工业电脑或者想用现代 C 标准给老系统写点正经工具它就绕不开。它做的事是把 GCC、make、gdb 和 binutils 整个搬进 DOS 环境让你在命令行下敲一条 gcc 命令就编译出能在纯 DOS 里运行的 32 位保护模式程序而不是 Turbo C 那种只能摸 640KB 内存的 16 位产物。DJGPP 2.04 是这一系列的最后一个主流发布解决了一批老内核的兼容性问题补齐了 C 标准库在 DOS 下的可用度。适合它的场景很明确老硬件上的实用工具、DOS 演示和游戏开发、操作系统的教学实验。它的目标用户不是要做现代 GUI 的人而是需要让代码在旧环境下可靠运行的人。2. 搭建 DJGPP 2.04 开发环境解压、环境变量与第一个可执行文件DJGPP 2.04 的安装不是双击一个 exe 就完事它更像一套绿色软件官方把整套工具链打散成一批 zip 包下载回归 DOS 目录结构后解压、设两个环境变量就能用。这个流程二十多年没变过反而成了它的优点——不写注册表不留服务删掉整个目录就是卸载换机器迁移也只需要拷走整个目录。2.1 下载与目录规划为什么坚持干净的根目录我一般会把所有 zip 包解压到同一个根目录比如 C:\DJGPP。这里的第一个坑是路径里不能有空格DJGPP 自身的脚本和 GNU make 在解析带空格的路径时会切错参数头文件路径拼接失败报错却指向 stdio.h排查起来很绕。第二个坑是解压时必须保留 zip 内自带的目录层次很多人习惯右键“解压到当前文件夹”一下子把 bin、include、lib 全摊平在一个目录里gcc 找不到标准库各种 No such file 跟着来。解压完成后检查五个位置bin\gcc.exe、bin\g.exe、bin\make.exe、lib\libc.a、include\stdio.h。这五个在主体就算齐了。C 用户还要确认 lib\libstdcxx.a 存在DJGPP 2.04 的 C 运行库拆成 libstdcxx 和 libsupcxx 两个包漏了其中一个链接阶段会报一堆 undefined reference 到 operator new看不出是库缺失还是代码问题。提示不要图省事下载别人精简过的“绿色版 / 一键版”。DJGPP 工具链整体耦合编译器、binutils、libc 之间对目标文件格式的版本非常敏感混用精简包会踩到 4.5 节的链接错误。2.2 配置 DJGPP 与 PATH两个环境变量决定编译器的认知DJGPP 2.04 靠两个环境变量定位整个工具链一个是 DJGPP指向 djgpp.env 文件一个是 PATH指向 bin 目录。Windows cmd 里的最小配置如下。set DJGPPC:\DJGPP\DJGPP.ENV set PATHC:\DJGPP\BIN;%PATH%第一行的逻辑是gcc、gdb、make 启动后会读取 DJGPP 变量指向的 djgpp.env这个 env 文件里写好了默认 CPU 类型、默认编译选项、库搜索路径和头文件搜索路径。变量没设DJGPP 工具就不知道自己该按哪个平台工作等价于一个不知道自己目标的编译器。第二行把 bin 塞进 PATH让你在任何目录下敲 gcc 都能被找到。最容易被忽略的是 DJGPP 这个名字必须全大写工具链内部按大写变量名精确匹配cmd 的 set 不区分大小写但传给子进程时变量名是小写就真的读不到。我一般会直接把这两个变量写进 Windows 系统环境变量而不是每次开 cmd 重新 set。因为编译老代码时经常同时开好几个终端一个跑 make一个跑 gdb一个盯汇编输出只在一个窗口 set 会漏掉另外两个报错时还以为编译器坏了。写完系统变量要记得重启终端让新进程重新加载环境当前窗口里的旧 PATH 不会自动更新这是很多人配完环境变量发现不生效的第一个原因。2.3 最小编译命令从 hello.c 到 hello.exe 的完整流程环境变量就绪后在工作目录新建一个 hello.c。#include stdio.h int main(void) { printf(hello from DJGPP 2.04\n); return 0; }在命令行执行编译cd C:\DJGPP\WORK gcc -O2 -stdgnu99 -o hello.exe hello.c选项拆开看-O2 是常用的优化级别对 DOS 目标最稳妥-stdgnu99 把 C 标准定到 C99 加 GNU 扩展DJGPP 的 stdio.h 对 C99 的覆盖比 C11 完整老代码也熟悉这套-o hello.exe 指定输出名不写的话默认生成 a.out在 DOS 下还得改名才能运行容易漏。编译成功会多出 hello.exe。我跑生成的 exe 时被 Windows 弹过一次“不是有效的 Win32 应用程序”这是正常现象它是 MZ 头的 DOS 可执行程序头部绑了一小段 DOS 扩展器 stubWindows 的 DOS 虚拟机不支持 32 位 DPMI 程序所以双击报错不等于编译失败。想确认产物身份用 16 进制编辑器看前几个字节MZ 头后面不是 PE 头而是 DOS 扩展器的实模式代码。2.4 运行的秘密CWSDPMI 与 DOS 扩展器怎么把 32 位程序送进实模式系统要理解 hello.exe 为什么能跑在 DOS 里先得厘清运行模型。DOS 内核只认识 16 位实模式程序CPU 被限制在 1MB 地址空间内而 DJGPP 2.04 编译出来的是 32 位保护模式代码两者之间隔着一层转换这层就是 DOS 扩展器。DJGPP 使用的扩展器是 CWSDPMI一个 DPMI 服务程序。它的执行顺序是在 DOS 下敲 hello.exeDOS 按 MZ 格式把程序装入内存程序入口处是一小段实模式 stubstub 会在当前目录或 PATH 里找 CWSDPMI.EXE加载它再把 CPU 从实模式切到保护模式建立 DPMI 服务完成切换后才跳转到真正的 32 位 C 运行时入口最后执行 main。所以 hello.exe 不是一个独立自足的文件发布时务必把 CWSDPMI.EXE 一起带上否则换一台没有它的机器程序会在加载阶段就退出界面都不给你一个。如果不想让用户带外部文件可以用 bin 下的 stubedit.exe 把 CWSDPMI 静态嵌进可执行文件。stubedit hello.exe这个命令会打开一个底层的 stub 编辑界面在界面上把 cwsdpmi 的 stub 绑定到当前 exe。绑定后文件会增大 10KB 左右但启动时不依赖外部 CWSDPMI。常见做法是开发期用外部 CWSDPMI发布期静态绑入两边都避免运行时缺文件。我自己的习惯是开发期也静态绑定这样每次在 DOSBox 里同步文件只拷一个 exe少一个遗漏点。2.5 用 DOSBox 做运行测试验收编译产物前的最后一步只有真正在 DOS 环境里跑一次才算验收工具链。常见做法是装一个 DOSBox把 C:\DJGPP 整个目录挂载为 DOS 的 C 盘然后在虚拟机里执行mount c c:\djgpp c: cd work hello.exe运行结果应当是打印出 hello from DJGPP 2.04然后正常回到 DOS 提示符。如果程序没有输出就直接退出先看当前目录有没有 CWSDPMI.EXE如果打印输出后系统卡死多半是 DOSBox 的 CPU 核心与保护模式切换冲突把 dosbox.conf 里的 coredynamic 改成 corenormal 再试。这一章的终点是把代码、编译、运行三条链穿起来。链通了下一章聊工具链的高级用法才有依托。3. 用好 DJGPP 2.04 的完整工具链编译选项、Makefile 与调试DJGPP 2.04 不只是 gcc 一家独大它还带了一套完整的 binutils 和调试器。学会把它们组合起来老平台开发效率才追得上现代环境而不是每次重编一遍全项目。3.1 gcc 常用编译选项-O2、-std 和 -march 在 DOS 目标下怎么选DJGPP 2.04 的 gcc 是针对 i586 目标构建的默认指令集基线在 Pentium 一代附近。如果你把编译产物拿到更老的 386/486 上跑默认选项生成的指令会让老 CPU 直接非法指令异常。所以第一个要明确的是目标硬件。只在 DOSBox 里跑用默认或 -marchi586 都行要兼顾 486 机器就加 -marchi486。一个常见编译命令模板gcc -O2 -marchi486 -stdgnu99 -Wall -c video.c gcc -O2 -marchi486 -stdgnu99 -o game.exe main.o video.o input.o参数说明-O2 是性能与体积的平衡点-Os 在空间紧张时换体积-march 限制生成的指令集决定了产物能跑在什么档次的 CPU 上-Wall 打开编译警告老代码迁移时靠它抓未声明函数。需要留意 DJGPP 2.04 里 -stdgnu99 与 -stdc99 的差别不大但涉及内联汇编和 asm 关键字时gnu99 更省事老 C 代码里的asm不会被严格模式拦下。还有一个经常被忽略的选项组合是 -ffunction-sections 和 -fdata-sections。它把每个函数、数据段拆成独立小节配合链接器的 --gc-sections 可以把没用到的部分整体丢弃。对 DOS 下体积敏感的工具很实用尤其从老仓库拉来的代码经常整个目录都被编译进去实际只用到其中一小部分函数。gcc -O2 -ffunction-sections -fdata-sections -c *.c gcc -o tool.exe *.o -Wl,--gc-sections第一行编译时按函数和数据切分段落第二行链接时丢弃没人引用的段。效果在控制台工具上通常能省 20% 到 30% 体积代价是调试时符号表变大行号定位偶尔不准。所以这个选项组我一般只用在发布前的最终构建调试阶段关掉。3.2 多文件项目一个可以直接抄的 Makefile 模板DJGPP 自带 GNU make可以在 DOS 命令行下直接跑 make 命令。写多文件工程时我通常从 min 依赖规则的 Makefile 开始。CC gcc CFLAGS -O2 -stdgnu99 -Wall -marchi486 TARGET game.exe OBJS main.o video.o input.o $(TARGET): $(OBJS) $(CC) -o $(TARGET) $(OBJS) main.o: main.c video.h input.h $(CC) $(CFLAGS) -c main.c video.o: video.c video.h $(CC) $(CFLAGS) -c video.c input.o: input.c input.h $(CC) $(CFLAGS) -c input.c clean: del $(OBJS) $(TARGET)这套规则讲了两件事显式依赖和隐式命令。每条“目标: 依赖列表”后面带 Tab 缩进的行是生成目标的命令GNU make 执行时先比较目标和依赖的时间戳依赖比目标新才重建目标。clean 用的是 del 而不是 rm因为 DJGPP 的命令行环境没有 rmWindows cmd 下 del 同样有效。这里最经典的翻车点是缩进用了空格而不是 Tabmake 会报 missing separator 然后直接停止我在这上面丢过不少时间。如果源码文件很多可以考虑模式规则和自动推导但不建议新手一上来就用。DJGPP 2.04 在 DOS 下跑的 make 版本较老通配符和二次展开的行为细节跟新版不一样踩到会很难查。先用写全依赖的笨办法项目稳定后再优化省下来的时间比省下的几行字符值钱。3.3 用 gdb 调试 DJGPP 程序从黑匣子到断点DJGPP 2.04 的 gdb 是 DOS 原生程序意味着你可以直接在 DOSBox 里启动它调试另一个 DOS 程序。编译时打开调试符号并关闭优化gcc -g -O0 -o hello.exe hello.c gdb hello.exe进入 gdb 命令行后常用流程是(gdb) break main (gdb) run (gdb) next (gdb) print i (gdb) quitbreak main 在函数入口设断点run 启动程序在断点停下后 next 是单步执行print 打印变量当前值quit 退出。对 DOS 程序来说最实用的是拿它查段错误run 之后如果看到 SIGSEGV用 backtrace 看栈回溯基本能定位到是哪个函数写穿了数组。注意 gdb 在 DOSBox 里运行也需要 CWSDPMI.EXE而且调试器本身要占内存DOSBox 默认的 XMS 设置太低时先把 DOSBox 配置里的 memsize 调到 16 或 32 再跑。遇到难缠的内存问题gdb 有一种现代环境里不常见的玩法直接在保护模式下访问显存和端口。你在 gdb 里用 x/wx 0xB8000 可以直接查看 VGA 文本缓冲区的内容在调试图形驱动时不需要额外打日志函数比插入一堆调试代码干净得多。3.4 用 binutils 做产物瘦身与检查strip、objcopy 的实际场景DJGPP 2.04 自带的 binutils 里日常最常用的三条是 strip、objcopy 和 strings。strip 去掉符号表把可执行文件体型减下来strip -s game.exe对发布版本做这一步是基本礼仪一个带完整调试符号的 DOS 程序体积能差一倍以上。-s 表示 strip all连重定位信息一起去掉副作用是 gdb 无法再对这个 exe 下断点所以 strip 要放在最后阶段调试期间不要碰。objcopy 在自制操作系统和引导程序场景出场。DJGPP 的链接器默认产出带 DOS 扩展器的可执行格式但你的内核最终要交给引导器加载到固定内存地址需要的是一段没有文件头、没有重定位的纯二进制。命令objcopy -O binary kernel.exe kernel.bin-O binary 把 COFF 段按内存布局原样导出生成 flat binary。这个文件没有了 MZ 头和保护模式 stub必须由引导代码按约定地址加载并自行切换保护模式。用这个命令前建议先在链接脚本里把内核段布局排好常见做法是让 .text 起始地址等于期望的加载地址否则出来的 bin 会在开头带一坨填充数据加载时错位。strings 是个被低估的检查工具strings game.exe | grep -i error\|fail它把所有可打印字符串捞出来配合 grep 看程序内置的错误提示不用运行就能排查资源文件路径、配置文件格式这类问题。我在处理老游戏汉化时经常用它确认文本是 ASCII 还是 GBK比盲改二进制稳得多。4. DJGPP 2.04 避坑指南环境变量、路径分隔符与内存模型的典型翻车现场DJGPP 2.04 的报错信息往往不像现代编译器那么友好很多问题表现为“编译不过”或“运行崩溃”实际根源却藏在环境配置和内存模型里。下面按我踩过的顺序列出 5 个高频坑每条按现象、原因、解决的顺序说清楚。4.1 stdio.h not found环境变量 DJGPP 丢失的症状与修复现象gcc 一编译任何 .c 文件立刻报stdio.h: No such file or directory但 include\stdio.h 明明存在。原因DJGPP 环境变量没设置或设置了但指向的 djgpp.env 路径不对。gcc 知道自己是 DJGPP 工具链但没被告知去哪找标准头文件。另一个隐蔽点是你把 djgpp.env 的名字写错比如 Windows 默认隐藏扩展名文件名真实是 DJGPP.ENV.txt指针就断了。解决先用 echo %DJGPP% 确认变量存在且路径正确再看文件管理器里有没有被系统吞掉扩展名。我在 Windows 上习惯在 cmd 里执行 dir C:\DJGPP\DJGPP.E* 看真实文件名比肉眼省心。配好这两个变量后gcc -v 打印的 search 路径里能看到 include 目录一眼就能验证工具链认知是否到位。4.2 Makefile 里文件路径被反斜杠截断路径分隔符的坑现象Makefile 里写 C:\DJGPP\LIB\libc.amake 报错说找不到文件或把路径截断成 C: 加一串乱码。原因GNU make 把反斜杠当作转义字符处理DOS 风格路径里的 \D、\L 会被吃掉一层。这个问题在 DJGPP 2.04 自带的旧版 make 里尤其明显。解决Makefile 内部统一用正斜杠写路径比如 C:/DJGPP/LIB/libc.aWindows 和 DOS 的文件系统层都能识别正斜杠。需要传给编译器参数时也尽量用正斜杠只在用户交互的命令行里保留反斜杠习惯。现在写任何 DOS 项目 Makefile我第一件事就是全局替换反斜杠省得后面的坑连成串。4.3 长文件名支持LFN 开关与 8.3 短文件名的取舍现象程序在 DOSBox 里 fopen(config.dat, r) 正常放到纯净 DOS 6.22 上 fopen 返回 NULL或者文件读到一半数据错乱。原因DJGPP 2.04 通过 LFN 接口访问长文件名DOS 默认没开启该接口时需要驱动程序支持。纯净 DOS 只有 8.3 短文件名而你的源代码和资源文件在 Windows 下解压时名字已经拉长程序按长名字去找自然找不到。解决两条路。一是给 DOS 加载 LFN 驱动但老机器环境不好说二是把涉及运行期资源访问的文件名全部压成 8.3 格式配置叫 config.cfg存档叫 save.dat。我在移植老游戏时默认按短文件名规划资源省掉一长串兼容性报告。4.4 C 异常或大数组段错误DPMI 内存不够时的现象与处理现象C 程序里 throw 一个异常程序直接崩溃或者 main 里声明一个 10MB 的局部数组运行到函数入口就段错误。原因DJGPP 2.04 的 C 异常依赖 DPMI 提供的内存管理CWSDPMI 分配给程序的堆大小默认从系统配置读取DOSBox 默认内存不足时异常处理栈和大的栈上数组都放不下。局部数组超过几 MB 时往往超出内核对堆栈段的预留运行时报段错误。解决给 CWSDPMI 配置更大的内存上限。常见做法是改 dosbox.conf 的 memsize 到 64或者在发布前用 stubedit 设置程序需要的 DPMI 内存池大数组改用 malloc 放堆不要放栈。我给老平台移植代码定的规矩是单块栈数组不超过 64KB超过一律堆分配这条规矩救了不少次。4.5 undefined reference to __main工具链版本混用的典型症状现象链接时报 undefined reference to __main代码里根本没写过这个符号。原因这是 DJGPP 特有的 C 运行时初始化符号。GCC 在 C 程序启动时通过调用 __main 完成全局构造器流程该符号由 libc.a 提供。问题是 binutils、libc 和 gcc 三者版本混搭时GCC 认为自己需要 __main而链接器链接的库却改了符号名或没有导出目标文件格式对不上最终链接失败。解决严格按照同一套发布的 DJGPP 2.04 包安装不要混用不同分支的 gcc 和 libc。检查方法是用 nm 在 libc.a 里搜索符号nm libc.a | grep main确认存在后再查 gcc 的 specs 文件里的 crt 相关路径是否指到同版本目录。这个问题没有快速补丁把环境清理干净重装全套是最省时间的做法比对着 ld 脚本折腾半天下班。5. 进阶用法验证编译产物与移植老代码的两个实用习惯工具链跑顺之后有两件事值得固定成习惯拿到陌生 exe 先验证迁移老代码只动封装层。这两件事能帮你把“运气好能过”变成“每次都能过”。5.1 验证产物三种快速检查手段我不太相信“刚才编译过了”这句话。拿到一个 DJGPP 产物至少做三道检查。第一道是 file 确认格式file game.exe输出出现 32 位、DOS 可执行、保护模式这些关键词说明工具链目标正确如果显示 16 位 COM 或 EXE多半是环境里混入了其他 DOS 工具。第二道是 strings 扫内嵌路径strings game.exe | grep -i config\\如果在输出里看到反斜杠形式的路径程序在 DOS 下解析路径时会出问题趁发布前改掉。第三道是完整启动模拟发布前把 exe、CWSDPMI.EXE 和资源文件放进同一个目录在 DOSBox 里从干净机器模拟一次完整启动。这步不能省我数不清多少次在开发机上有 CWSDPMI 所以没暴露换目录后程序直接神秘消失。5.2 把老 Turbo C 代码迁到 DJGPP只动封装层不动业务逻辑从 Turbo C 迁到 DJGPP 2.0490% 的工作集中在头文件和平台 API 替换。我给自己定的规则是造一个 target-config.h把平台差异全压进去。#ifndef TARGET_CONFIG_H #define TARGET_CONFIG_H /* Turbo C 的远指针在 DJGPP 平坦内存里不需要 */ #if defined(__DJGPP__) #include stdlib.h #define _fmalloc malloc #define _ffree free #else #include alloc.h #endif #endif这个头文件的作用是业务代码继续调用 _fmalloc底层在 DJGPP 下被替换成 malloc。32 位保护模式的平坦内存里所有指针都够远远指针概念本来就该废弃用宏映射一下业务代码不用改。移植时我会先把编译错误全部排干净再逐个替换图形和中断部分最后跑回归。图形模式是另一个深水区保护模式访问显存需要映射gdb 设断点查 VGA 缓冲比 printf 调试高效。这套流程走完一个项目基本就有了自己的移植模板。我现在拿到任何老代码第一反应是先找它的平台依赖集中在哪几个头文件而不是急着编译。这个习惯让后来的移植工作顺手不少希望帮到你。本文还有配套的精品资源点击获取