简介:DJGPP 2.04 是一套专供 DOS 环境的开源 C/C++ 开发工具链,由 GNU 编译器套件移植而来,适合需要维护老式 DOS 程序、研究操作系统底层原理或体验早期个人计算机开发流程的中高级开发者。压缩包共 372 个文件、约 6.95 MB,核心组件涵盖 GCC、链接器、汇编器、GNU Make、GDB 调试器及配套库;文件类型以 127 个 .h 头文件和 52 个 .exe 可执行程序为主,另附 .info 文档、.bat 脚本、.lib 库和大量 .1 格式的手册页,既可用于直接安装,也方便逐文件分析工具链结构。在 DOS 环境下,借助这套工具链能够构建游戏、图形界面乃至系统级软件,兼容场景丰富。目前已有 401 人学习/下载。包内保留的经典 GNU 工具手册,还能与《Linux0.01内核分析与操作系统设计》配书场景配合使用,帮助读者在无现代操作系统支持的环境下完成源码编译、链接与调试,加深对早期 GCC 移植和系统开发流程的理解,对复古计算和操作系统教学具有独特参考价值。 我在整理旧硬盘时,翻出一个十年前用 DJGPP 2.04 写的游戏雏形。花了半个多小时把它丢进 QEMU 里的 FreeDOS 重新跑起来,那种熟悉又陌生的感觉让我决定把这套老工具链的经验完整写下来。DJGPP 2.04 是一套把 GNU 工具链搬到 DOS 上的完整开发环境,包含编译器、汇编器、链接器、调试器和 C 库,核心价值是让 DOS 程序以 32 位保护模式运行,彻底突破 640KB 直接寻址的天花板。如果你要做复古平台工具、写 DOS 小游戏、研究老硬件编程,或者只是好奇 32 位程序怎么在 16 位的操作系统下存活,这篇笔记应该能帮你少走不少弯路。
1. 为什么 DJGPP 2.04 这个“老古董”还能打
1.1 一套工具链包办 DOS 开发全流程
很多人一听到 DJGPP 就以为它只是个编译器,其实它是一整套工具链。核心包里除了 gcc,还有 gas、ld、gdb、make、sed、awk,以及一个被精心裁剪过的 C 库。这意味着你在 DOS 下不需要像 Borland 时代那样额外找一堆补丁工具,基本开箱即用。
我第一次用 DJGPP 2.04 时最惊讶的是它的完整度:从编辑代码到编译、链接、调试,甚至生成静态库,一条龙都能在 DOS 命令行里完成。你不需要像用 Turbo C 那样到处找第三方库来补标准库的缺失,DJGPP 库对 C99 的支持已经相当像样,至少stdint.h、inttypes.h这些头文件都能直接用。对于要读老代码、跑老工程的人来说,这个完整度能省掉大量“缺头文件”的折腾。
1.2 32 位保护模式让它彻底摆脱 640K 天花板
DJGPP 和早先的 16 位 DOS 编译器最大的差异,就是它生成的程序运行在 32 位保护模式下。你可以把 DOS 想象成一栋只有一楼能住人的老房子,实模式下程序只能直接访问 1MB 内存,其中留给应用程序的常规内存只有 640KB。DJGPP 则通过 DPMI(DOS Protected Mode Interface)给这栋老房子加盖了高层楼层,程序可以寻址到 4GB 平坦地址空间。
这里有个关键点:程序虽然运行在保护模式,但操作系统还是 DOS,所以程序没办法直接逃避 DOS 的规则。DJGPP 运行时会帮你处理模式切换,当你的代码调用 DOS 功能或 BIOS 中断时,它会悄悄切回实模式完成请求,再切回来继续运行。正是这种“两边通吃”的设计,让 DJGPP 程序既能享受 32 位 CPU 的能力,又能继续使用 DOS 环境下的文件、键盘、鼠标和硬件接口。
1.3 哪些人现在还会需要它
别以为这是电子垃圾。至少三类人现在还会主动装 DJGPP 2.04:
- 复古游戏开发者:想给 DOS 平台写新游戏,或者维护老项目,Allegro 库在 DJGPP 下有非常成熟的移植版本。
- 硬件控制/嵌入式爱好者:通过并口、串口、声卡或 VGA 直接操作硬件,DOS 下没有硬件抽象层干扰,DJGPP 提供的端口读写函数就是直来直去。
- 操作系统课/逆向工程学习者:想在纯 DOS 环境里观察 32 位保护模式与实模式切换,理解中断、GDT、页表等概念,DJGPP 是最容易上手的实验平台。
当然,如果你只是想在虚拟机里跑跑老游戏,那直接装 FreeDOS 就够了,不需要碰编译工具。但如果你有“自己动手写点东西”的冲动,DJGPP 2.04 几乎是唯一还活着的选项。
2. 搭建 DJGPP 2.04 环境:下载、解压、跑通第一个程序
2.1 需要拿哪些包,以及解压时的铁律
DJGPP 的安装并不复杂,但有两个新手容易踩的坑:一是不知道要拿哪些包,二是一不小心把压缩包解压得到处都是。
基础包是djdev204.zip,它包含 C 库、头文件、DPMI 宿主 CWSDPMI 以及最基础的编译运行环境。但光有它还不行,你得再从镜像站的 current 目录里拿对应版本的 gcc、binutils、make、gdb 等二进制包。DJGPP 的二进制包命名一般很长,比如 gcc、bnu、mak、gdb 这种缩写加版本号,认准前缀就行。
解压时的铁律是:所有包必须解压到同一个根目录,而且要保持压缩包内部的目录结构。DJGPP 官方推荐的路径是C:\DJGPP,但理论上任何不含空格的短路径都可以。我第一次用就图省事解压到D:\My Tools\DJGPP\,结果环境变量配置时被中间的空格搞到怀疑人生。DOS 工具链对路径中的空格容忍度很低的,老老实实用C:\DJGPP最省心。
2.2 环境变量与命令行配置
解压完之后,需要配置两个环境变量。首先设置DJGPP指向环境文件:
set DJGPP=C:\DJGPP\DJGPP.ENV然后把 DJGPP 的 bin 目录加入 PATH:
set PATH=C:\DJGPP\bin;%PATH%DJGPP.ENV这个文件是 DJGPP 程序的配置中心,C 库、算法库和 DPMI 的行为都由它控制。如果你偷懒不设置DJGPP,直接运行 gcc 会大概率报错,提示找不到djgpp.env,然后一脸懵。
在 Windows 9x/ME 上,你可以在 AUTOEXEC.BAT 里写这两行让系统自动设置。在 Windows NT 系列的命令行窗口里,手动设置后当前会话就能用了。纯 DOS 环境则必须写在 AUTOEXEC.BAT 里,因为 DOS 没有会话级环境变量的概念。
2.3 编译 Hello World 并理解发生了什么
环境配好后,写一个最普通的 C 程序:
#include <stdio.h> int main(void) { printf("Hello, DJGPP 2.04!\n"); return 0; }然后执行:
gcc -Wall hello.c -o hello.exe如果一切正常,你会得到一个能在 DOS 下运行的 32 位保护模式程序。这里有两个细节值得一提:
第一,生成的文件虽然是.exe后缀,但它并不是传统的 MZ 格式,而是以 DOS Stub 开头、后跟 COFF 调试信息的 DJGPP 专属格式。在 Windows 的命令行窗口直接执行它也可能会闪退或报错,因为它需要 DPMI 服务,而 Windows NT 默认不提供完整 DPMI。你得用 CWSDPMI 或者投入 DOSBox/FreeDOS 环境里去跑。
第二,运行时 DJGPP 会自动查找CWSDPMI.EXE。这个文件在核心包里就有,通常放在C:\DJGPP\bin下。它充当 DPMI 服务器,负责让程序进入保护模式、管理内存映射。如果缺少它,程序会打印一句DPMI not available然后退出。第一次跑通时看到熟悉的 32 位程序在 DOS 下跑起来,还是挺有成就感的。
3. 实际开发里绕不开的 C 库特性与底层接口
3.1 访问 DOS/BIOS 中断的正确姿势
DJGPP 程序运行在保护模式,面对的是平坦的 32 位地址空间,但 DOS 和 BIOS 的中断服务依然活在实模式世界里。如果你需要调用一个中断,不能直接用int指令,而是要把寄存器状态填进__dpmi_regs结构体,然后调用__dpmi_int。
比如读系统日期,传统实模式程序会调用INT 21h功能2Ah。在 DJGPP 下就得先把 AH、AL 这些字段设置好,再交给__dpmi_int,最后从返回的寄存器里拿到数据。这对从 Turbo C 迁移过来的老程序员来说有点绕,但习惯之后会发现它其实很规律:你能调用的任何 DOS 中断,都可以用这一套机制封装成 C 函数。
我个人的经验是:除非你确实在做一个需要直接操作系统的工具,否则别每件事都走中断。DJGPP 的 C 库已经帮你封装好了大部分常用功能,比如文件读写、内存分配、目录扫描都用标准 C 或 POSIX 函数就能搞定。真正需要中断的场景,一般集中在图形模式切换、鼠标驱动、声卡配置这些硬件级操作上。
3.2 内存分配没有想象中复杂,但别踩远指针的坑
在 16 位 DOS 时代,指针分为 near 和 far,程序员成天和段地址较劲。DJGPP 不一样,它运行在 32 位保护模式下,默认所有指针都是 32 位平坦地址,你写malloc直接能分到几 MB 甚至几十 MB 内存,这在当年是难以想象的。
但这里有个经典坑:你拿到的指针是保护模式下的线性地址,不能直接拿它去访问 DOS 实模式内存或硬件映射区,比如显存。DJGPP 提供了一组专门函数,比如_dos_ds可以拿到 DOS 默认数据段选择子,再用选择子加偏移去访问实模式内存。图形编程里也常用_farpokeb、_farpokew这类函数往绝对地址写东西。
新手最容易犯的错误是直接取指针地址就硬往显存里塞,结果画面全花。解决办法很简单:遇到需要访问物理地址或 DOS 段内存时,不要自己拼地址,去查 DJGPP 的手册,用库函数中转。这套设计虽然粗犷,但功能非常完善,DOS 下能想到的坑它都有解药。
3.3 想让屏幕亮起来:Allegro 库和 VBE 现实
如果你打算在 DJGPP 下写图形程序,最省力的方式是直接用 Allegro 游戏库。Allegro 在 DOS 下通过 VBE(VESA BIOS Extensions)驱动显卡,支持从 320x240 到 1024x768 等常见模式,而且内部帮你处理了线性帧缓冲和模式切换。
一个最简的 Allegro 程序大概长这样:
#include <allegro.h> int main(void) { allegro_init(); set_color_depth(32); if (set_gfx_mode(GFX_AUTODETECT, 640, 480, 0, 0) != 0) { allegro_message("图形模式初始化失败: %s\n", allegro_error); return 1; } putpixel(screen, 320, 240, makecol(255, 0, 0)); readkey(); allegro_exit(); return 0; } END_OF_MAIN()注意结尾那个END_OF_MAIN(),这是 Allegro 在 DJGPP 上必须的宏,它会把 main 的入口改造成 DJGPP 运行时要求的形式。如果你漏了它,链接时通常会报错误,这是一个很典型的 DOS 平台特性,Windows 版本的 Allegro 根本不需要这个。
如果你的目标是写一个极其轻量的图形测试,不想引入整个 Allegro,那就直接调 VBE。DJGPP 下可以通过中断设置线性模式,然后拿到 LFB(线性帧缓冲)地址,用普通指针直接操作像素。这个方案灵活,但代码量一下子就会涨上去。
3.4 长文件名和路径处理
DJGPP 2.04 对长文件名的支持是通过LFN环境变量控制的。在 Windows 9x 环境下,如果你执行set LFN=y,程序就能通过 Windows 提供的接口读取长文件名。在纯 DOS 或 FreeDOS 下,这个功能不存在,只能以 8.3 短文件名为主。
我踩过的最深的一个坑是:在 Windows 下用长文件名编译出了程序,拷到 FreeDOS 里跑,结果程序找不到自己的配置文件。原因就是代码里写死了英文长路径,而 target 环境的文件系统根本不认识。解决办法是:如果你发布给纯 DOS 环境,所有运行时读取的文件路径都用 8.3 短名。开发时用长名方便,发布前至少做一次短名验证。
另外,如果程序需要扫描目录,findfirst、findnext在 DJGPP 下的行为受 LFN 环境影响很大。你最好显式检查环境变量,或者干脆写一个小的封装层,统一处理两种模式,这样代码的可移植性会好很多。
4. 调试技巧和性能调优,都是被逼出来的
4.1 调试器能用,但 printf 大法在复古平台上更香
DJGPP 自带 GDB 移植版,功能上支持断点、单步、查看内存。可问题是,DOS 不像现代操作系统有独立的多任务隔离,调试器和被调试程序共享同一套 DOS 环境,一旦程序搞坏了中断向量,GDB 也一起崩,甚至需要重启。
我在调试 DJGPP 程序时,最常用的是printf加返回码。因为 DOS 下没有复杂的日志框架,直接在控制台打印状态、显示关键变量的值,就是最直接的手段。如果你非得用 GDB,建议在虚拟机上做远程调试,或者用 DJGPP 自带的stub机制,让被调试程序和调试器通过串口通信,但这套方案配置成本偏高,日常开发不划算。
4.2 用 RDTSC 做高精度计时
DJGPP 的 C 库提供clock(),但它的精度在 DOS 下只有大概 55ms,对性能分析来说太粗了。如果你在 486 以上 CPU 上运行,可以直接用rdtsc指令读取 CPU 周期计数:
static inline unsigned long long rdtsc(void) { unsigned long long t; __asm__ __volatile__("rdtsc" : "=A" (t)); return t; }这个函数在高版本 GCC 下编译时,"=A"约束会正确地把 EDX:EAX 组合成 64 位整数。你可以在代码段前后各打一个时间戳,相减就是这段代码大概消耗的 CPU 周期。把它除以 CPU 主频,就能估算出纳秒或微秒级的时间开销,比clock()好用太多了。
4.3 优化代码时最容易见效的几件事
- 使用
-O2而不是默认等级:DJGPP 默认的优化等级很保守,-O2能在完全不破坏兼容性的前提下带来明显提速。 - 考虑
-march=i586:如果你的目标 CPU 是 Pentium 或更高,可以告诉编译器使用该架构的特性指令,老的 386 型号就没法运行了。 - 减少模式切换:在 DOS 下,每次调用中断或访问 DOS 文件系统都可能导致模式切换,频繁读写文件会非常慢。能一次读一大块内存,就别一遍遍小读取。
- 显存直接批量写:如果做图形程序,避免逐个像素
putpixel,把整块数据先放缓冲区,再用dosmemput之类的函数一次性拷贝到显存,速度能差出一个数量级。
有一次我优化一个 320x240 的火焰特效,从逐个像素画到改成整条扫描线批量写入,帧率直接翻了三倍。原因就是减少了大量隐藏的函数调用和端口操作。
5. 和同时代其他 DOS 开发工具的取舍
5.1 为什么不是 Borland C++ 或 Open Watcom
Borland C++ Builder、Turbo C++ 在 16 位 DOS 时代确实辉煌,但它们早已闭源,而且对 32 位保护模式的支持很弱。Open Watcom 也一样能生成 32 位 DOS 程序,工具链也完整,但你在社区找资料时会发现,真正还在活跃维护的 DOS 开源项目,绝大多数都建立在 DJGPP 之上。
DJGPP 2.04 的优势首先是免费开源,想要什么组件都能从官方镜像拿到完整源码。其次是它与现代 GCC 工具链的延续性,你在 Linux 上写的算法代码,稍作修改就能在 DJGPP 下编译成 DOS 程序。这种能跨平台共享代码的能力,在古早工具里非常稀有。
5.2 在 DOSBox、FreeDOS 和虚拟机里跑 2.04 程序
运行 DJGPP 生成的 EXE,最省事的办法是装一个 FreeDOS 虚拟机镜像,然后把可执行文件拖进去跑。DOSBox 也是个选择,但要注意默认的 CPU 配置可能对保护模式程序不太友好,偶尔会遇到 DPMI 初始化失败。解决办法是在 DOSBox 配置里开启core=dynamic,并确保内存设置足够大。
如果你需要在现代 Windows 下快速验证编译产物,可以考虑用 QTEMU 加 FreeDOS,或者直接用 DOSBox-X 这类更活跃的分支。我个人的强烈建议是QEMU + FreeDOS:它更接近真实 DOS 环境,对各种硬件模拟也更严格,跑出来的结果可信度更高。
还有一个小技巧:DJGPP 程序的崩溃信息,可以在DJGPP.ENV里设置CRT相关选项,让它生成详细的异常转储。文件名类似gmon.out或core,配合gdb能定位到具体代码位置。在纯 DOS 环境下这个功能很管用,别等出了问题再到处找原因。
最后再分享一点我自己的体会:不要被 DJGPP 2.04 的年代感吓到。它的编译速度虽然比不上现代工具链,但对几百 KB 规模的项目几乎无感;更关键的是,它背后积累的那套 DOS 环境下的解决方案,是今天任何“现代”工具都给不了的。如果你手里有老代码、老项目,或者单纯想在裸机上写点低层东西,DJGPP 2.04 依然是最值得花一个下午去上手的那一个。
本文还有配套的精品资源,点击获取