
簡(jiǎn)介DJGPP 2.04 是一套面向 DOS 環(huán)境的開(kāi)源 C/C 開(kāi)發(fā)工具鏈主要解決在老式 PC、虛擬機(jī)或嵌入式場(chǎng)景中源碼編譯與程序構(gòu)建的問(wèn)題適合復(fù)古計(jì)算愛(ài)好者、系統(tǒng)原理學(xué)習(xí)者以及需要在 DOS 下開(kāi)發(fā)游戲或系統(tǒng)級(jí)軟件的開(kāi)發(fā)者。該版本由 GNU 工具移植而來(lái)整合了 GCC 編譯器、匯編器、鏈接器、GNU Make、調(diào)試器 GDB、庫(kù)管理器及大量標(biāo)準(zhǔn)頭文件和庫(kù)文件可在沒(méi)有現(xiàn)代操作系統(tǒng)的條件下完成從編輯、編譯到鏈接的完整開(kāi)發(fā)流程。整套壓縮包共 372 個(gè)文件以 h 頭文件、exe 可執(zhí)行程序、info 說(shuō)明文檔以及 djl、lib、bat 等輔助文件為主壓縮后約 6.95MB體積緊湊便于在早期硬件環(huán)境部署。資源與《Linux0.01內(nèi)核分析與操作系統(tǒng)設(shè)計(jì)配書(shū)光盤(pán)》教學(xué)場(chǎng)景緊密相關(guān)同時(shí)附帶 man 手冊(cè)頁(yè)與示例代碼幫助讀者理解早期操作系統(tǒng)內(nèi)核、底層啟動(dòng)流程及 DOS 平臺(tái)編程方式。目前已有 405 人學(xué)習(xí)下載對(duì)研究編譯工具發(fā)展史和操作系統(tǒng)教學(xué)均有獨(dú)特價(jià)值。1. DJGPP 2.04 是什么一個(gè)讓 32 位 C/C 在 DOS 里跑起來(lái)的編譯器套件DJGPP 2.04 這個(gè)名字在今天的開(kāi)發(fā)者眼里很陌生但只要你碰 DOS 下的復(fù)古游戲、老式工業(yè)電腦或者想用現(xiàn)代 C 標(biāo)準(zhǔn)給老系統(tǒng)寫(xiě)點(diǎn)正經(jīng)工具它就繞不開(kāi)。它做的事是把 GCC、make、gdb 和 binutils 整個(gè)搬進(jìn) DOS 環(huán)境讓你在命令行下敲一條 gcc 命令就編譯出能在純 DOS 里運(yùn)行的 32 位保護(hù)模式程序而不是 Turbo C 那種只能摸 640KB 內(nèi)存的 16 位產(chǎn)物。DJGPP 2.04 是這一系列的最后一個(gè)主流發(fā)布解決了一批老內(nèi)核的兼容性問(wèn)題補(bǔ)齊了 C 標(biāo)準(zhǔn)庫(kù)在 DOS 下的可用度。適合它的場(chǎng)景很明確老硬件上的實(shí)用工具、DOS 演示和游戲開(kāi)發(fā)、操作系統(tǒng)的教學(xué)實(shí)驗(yàn)。它的目標(biāo)用戶不是要做現(xiàn)代 GUI 的人而是需要讓代碼在舊環(huán)境下可靠運(yùn)行的人。2. 搭建 DJGPP 2.04 開(kāi)發(fā)環(huán)境解壓、環(huán)境變量與第一個(gè)可執(zhí)行文件DJGPP 2.04 的安裝不是雙擊一個(gè) exe 就完事它更像一套綠色軟件官方把整套工具鏈打散成一批 zip 包下載回歸 DOS 目錄結(jié)構(gòu)后解壓、設(shè)兩個(gè)環(huán)境變量就能用。這個(gè)流程二十多年沒(méi)變過(guò)反而成了它的優(yōu)點(diǎn)——不寫(xiě)注冊(cè)表不留服務(wù)刪掉整個(gè)目錄就是卸載換機(jī)器遷移也只需要拷走整個(gè)目錄。2.1 下載與目錄規(guī)劃為什么堅(jiān)持干凈的根目錄我一般會(huì)把所有 zip 包解壓到同一個(gè)根目錄比如 C:\DJGPP。這里的第一個(gè)坑是路徑里不能有空格DJGPP 自身的腳本和 GNU make 在解析帶空格的路徑時(shí)會(huì)切錯(cuò)參數(shù)頭文件路徑拼接失敗報(bào)錯(cuò)卻指向 stdio.h排查起來(lái)很繞。第二個(gè)坑是解壓時(shí)必須保留 zip 內(nèi)自帶的目錄層次很多人習(xí)慣右鍵“解壓到當(dāng)前文件夾”一下子把 bin、include、lib 全攤平在一個(gè)目錄里gcc 找不到標(biāo)準(zhǔn)庫(kù)各種 No such file 跟著來(lái)。解壓完成后檢查五個(gè)位置bin\gcc.exe、bin\g.exe、bin\make.exe、lib\libc.a、include\stdio.h。這五個(gè)在主體就算齊了。C 用戶還要確認(rèn) lib\libstdcxx.a 存在DJGPP 2.04 的 C 運(yùn)行庫(kù)拆成 libstdcxx 和 libsupcxx 兩個(gè)包漏了其中一個(gè)鏈接階段會(huì)報(bào)一堆 undefined reference 到 operator new看不出是庫(kù)缺失還是代碼問(wèn)題。提示不要圖省事下載別人精簡(jiǎn)過(guò)的“綠色版 / 一鍵版”。DJGPP 工具鏈整體耦合編譯器、binutils、libc 之間對(duì)目標(biāo)文件格式的版本非常敏感混用精簡(jiǎn)包會(huì)踩到 4.5 節(jié)的鏈接錯(cuò)誤。2.2 配置 DJGPP 與 PATH兩個(gè)環(huán)境變量決定編譯器的認(rèn)知DJGPP 2.04 靠?jī)蓚€(gè)環(huán)境變量定位整個(gè)工具鏈一個(gè)是 DJGPP指向 djgpp.env 文件一個(gè)是 PATH指向 bin 目錄。Windows cmd 里的最小配置如下。set DJGPPC:\DJGPP\DJGPP.ENV set PATHC:\DJGPP\BIN;%PATH%第一行的邏輯是gcc、gdb、make 啟動(dòng)后會(huì)讀取 DJGPP 變量指向的 djgpp.env這個(gè) env 文件里寫(xiě)好了默認(rèn) CPU 類型、默認(rèn)編譯選項(xiàng)、庫(kù)搜索路徑和頭文件搜索路徑。變量沒(méi)設(shè)DJGPP 工具就不知道自己該按哪個(gè)平臺(tái)工作等價(jià)于一個(gè)不知道自己目標(biāo)的編譯器。第二行把 bin 塞進(jìn) PATH讓你在任何目錄下敲 gcc 都能被找到。最容易被忽略的是 DJGPP 這個(gè)名字必須全大寫(xiě)工具鏈內(nèi)部按大寫(xiě)變量名精確匹配cmd 的 set 不區(qū)分大小寫(xiě)但傳給子進(jìn)程時(shí)變量名是小寫(xiě)就真的讀不到。我一般會(huì)直接把這兩個(gè)變量寫(xiě)進(jìn) Windows 系統(tǒng)環(huán)境變量而不是每次開(kāi) cmd 重新 set。因?yàn)榫幾g老代碼時(shí)經(jīng)常同時(shí)開(kāi)好幾個(gè)終端一個(gè)跑 make一個(gè)跑 gdb一個(gè)盯匯編輸出只在一個(gè)窗口 set 會(huì)漏掉另外兩個(gè)報(bào)錯(cuò)時(shí)還以為編譯器壞了。寫(xiě)完系統(tǒng)變量要記得重啟終端讓新進(jìn)程重新加載環(huán)境當(dāng)前窗口里的舊 PATH 不會(huì)自動(dòng)更新這是很多人配完環(huán)境變量發(fā)現(xiàn)不生效的第一個(gè)原因。2.3 最小編譯命令從 hello.c 到 hello.exe 的完整流程環(huán)境變量就緒后在工作目錄新建一個(gè) hello.c。#include stdio.h int main(void) { printf(hello from DJGPP 2.04\n); return 0; }在命令行執(zhí)行編譯cd C:\DJGPP\WORK gcc -O2 -stdgnu99 -o hello.exe hello.c選項(xiàng)拆開(kāi)看-O2 是常用的優(yōu)化級(jí)別對(duì) DOS 目標(biāo)最穩(wěn)妥-stdgnu99 把 C 標(biāo)準(zhǔn)定到 C99 加 GNU 擴(kuò)展DJGPP 的 stdio.h 對(duì) C99 的覆蓋比 C11 完整老代碼也熟悉這套-o hello.exe 指定輸出名不寫(xiě)的話默認(rèn)生成 a.out在 DOS 下還得改名才能運(yùn)行容易漏。編譯成功會(huì)多出 hello.exe。我跑生成的 exe 時(shí)被 Windows 彈過(guò)一次“不是有效的 Win32 應(yīng)用程序”這是正?,F(xiàn)象它是 MZ 頭的 DOS 可執(zhí)行程序頭部綁了一小段 DOS 擴(kuò)展器 stubWindows 的 DOS 虛擬機(jī)不支持 32 位 DPMI 程序所以雙擊報(bào)錯(cuò)不等于編譯失敗。想確認(rèn)產(chǎn)物身份用 16 進(jìn)制編輯器看前幾個(gè)字節(jié)MZ 頭后面不是 PE 頭而是 DOS 擴(kuò)展器的實(shí)模式代碼。2.4 運(yùn)行的秘密CWSDPMI 與 DOS 擴(kuò)展器怎么把 32 位程序送進(jìn)實(shí)模式系統(tǒng)要理解 hello.exe 為什么能跑在 DOS 里先得厘清運(yùn)行模型。DOS 內(nèi)核只認(rèn)識(shí) 16 位實(shí)模式程序CPU 被限制在 1MB 地址空間內(nèi)而 DJGPP 2.04 編譯出來(lái)的是 32 位保護(hù)模式代碼兩者之間隔著一層轉(zhuǎn)換這層就是 DOS 擴(kuò)展器。DJGPP 使用的擴(kuò)展器是 CWSDPMI一個(gè) DPMI 服務(wù)程序。它的執(zhí)行順序是在 DOS 下敲 hello.exeDOS 按 MZ 格式把程序裝入內(nèi)存程序入口處是一小段實(shí)模式 stubstub 會(huì)在當(dāng)前目錄或 PATH 里找 CWSDPMI.EXE加載它再把 CPU 從實(shí)模式切到保護(hù)模式建立 DPMI 服務(wù)完成切換后才跳轉(zhuǎn)到真正的 32 位 C 運(yùn)行時(shí)入口最后執(zhí)行 main。所以 hello.exe 不是一個(gè)獨(dú)立自足的文件發(fā)布時(shí)務(wù)必把 CWSDPMI.EXE 一起帶上否則換一臺(tái)沒(méi)有它的機(jī)器程序會(huì)在加載階段就退出界面都不給你一個(gè)。如果不想讓用戶帶外部文件可以用 bin 下的 stubedit.exe 把 CWSDPMI 靜態(tài)嵌進(jìn)可執(zhí)行文件。stubedit hello.exe這個(gè)命令會(huì)打開(kāi)一個(gè)底層的 stub 編輯界面在界面上把 cwsdpmi 的 stub 綁定到當(dāng)前 exe。綁定后文件會(huì)增大 10KB 左右但啟動(dòng)時(shí)不依賴外部 CWSDPMI。常見(jiàn)做法是開(kāi)發(fā)期用外部 CWSDPMI發(fā)布期靜態(tài)綁入兩邊都避免運(yùn)行時(shí)缺文件。我自己的習(xí)慣是開(kāi)發(fā)期也靜態(tài)綁定這樣每次在 DOSBox 里同步文件只拷一個(gè) exe少一個(gè)遺漏點(diǎn)。2.5 用 DOSBox 做運(yùn)行測(cè)試驗(yàn)收編譯產(chǎn)物前的最后一步只有真正在 DOS 環(huán)境里跑一次才算驗(yàn)收工具鏈。常見(jiàn)做法是裝一個(gè) DOSBox把 C:\DJGPP 整個(gè)目錄掛載為 DOS 的 C 盤(pán)然后在虛擬機(jī)里執(zhí)行mount c c:\djgpp c: cd work hello.exe運(yùn)行結(jié)果應(yīng)當(dāng)是打印出 hello from DJGPP 2.04然后正常回到 DOS 提示符。如果程序沒(méi)有輸出就直接退出先看當(dāng)前目錄有沒(méi)有 CWSDPMI.EXE如果打印輸出后系統(tǒng)卡死多半是 DOSBox 的 CPU 核心與保護(hù)模式切換沖突把 dosbox.conf 里的 coredynamic 改成 corenormal 再試。這一章的終點(diǎn)是把代碼、編譯、運(yùn)行三條鏈穿起來(lái)。鏈通了下一章聊工具鏈的高級(jí)用法才有依托。3. 用好 DJGPP 2.04 的完整工具鏈編譯選項(xiàng)、Makefile 與調(diào)試DJGPP 2.04 不只是 gcc 一家獨(dú)大它還帶了一套完整的 binutils 和調(diào)試器。學(xué)會(huì)把它們組合起來(lái)老平臺(tái)開(kāi)發(fā)效率才追得上現(xiàn)代環(huán)境而不是每次重編一遍全項(xiàng)目。3.1 gcc 常用編譯選項(xiàng)-O2、-std 和 -march 在 DOS 目標(biāo)下怎么選DJGPP 2.04 的 gcc 是針對(duì) i586 目標(biāo)構(gòu)建的默認(rèn)指令集基線在 Pentium 一代附近。如果你把編譯產(chǎn)物拿到更老的 386/486 上跑默認(rèn)選項(xiàng)生成的指令會(huì)讓老 CPU 直接非法指令異常。所以第一個(gè)要明確的是目標(biāo)硬件。只在 DOSBox 里跑用默認(rèn)或 -marchi586 都行要兼顧 486 機(jī)器就加 -marchi486。一個(gè)常見(jiàn)編譯命令模板gcc -O2 -marchi486 -stdgnu99 -Wall -c video.c gcc -O2 -marchi486 -stdgnu99 -o game.exe main.o video.o input.o參數(shù)說(shuō)明-O2 是性能與體積的平衡點(diǎn)-Os 在空間緊張時(shí)換體積-march 限制生成的指令集決定了產(chǎn)物能跑在什么檔次的 CPU 上-Wall 打開(kāi)編譯警告老代碼遷移時(shí)靠它抓未聲明函數(shù)。需要留意 DJGPP 2.04 里 -stdgnu99 與 -stdc99 的差別不大但涉及內(nèi)聯(lián)匯編和 asm 關(guān)鍵字時(shí)gnu99 更省事老 C 代碼里的asm不會(huì)被嚴(yán)格模式攔下。還有一個(gè)經(jīng)常被忽略的選項(xiàng)組合是 -ffunction-sections 和 -fdata-sections。它把每個(gè)函數(shù)、數(shù)據(jù)段拆成獨(dú)立小節(jié)配合鏈接器的 --gc-sections 可以把沒(méi)用到的部分整體丟棄。對(duì) DOS 下體積敏感的工具很實(shí)用尤其從老倉(cāng)庫(kù)拉來(lái)的代碼經(jīng)常整個(gè)目錄都被編譯進(jìn)去實(shí)際只用到其中一小部分函數(shù)。gcc -O2 -ffunction-sections -fdata-sections -c *.c gcc -o tool.exe *.o -Wl,--gc-sections第一行編譯時(shí)按函數(shù)和數(shù)據(jù)切分段落第二行鏈接時(shí)丟棄沒(méi)人引用的段。效果在控制臺(tái)工具上通常能省 20% 到 30% 體積代價(jià)是調(diào)試時(shí)符號(hào)表變大行號(hào)定位偶爾不準(zhǔn)。所以這個(gè)選項(xiàng)組我一般只用在發(fā)布前的最終構(gòu)建調(diào)試階段關(guān)掉。3.2 多文件項(xiàng)目一個(gè)可以直接抄的 Makefile 模板DJGPP 自帶 GNU make可以在 DOS 命令行下直接跑 make 命令。寫(xiě)多文件工程時(shí)我通常從 min 依賴規(guī)則的 Makefile 開(kāi)始。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)這套規(guī)則講了兩件事顯式依賴和隱式命令。每條“目標(biāo): 依賴列表”后面帶 Tab 縮進(jìn)的行是生成目標(biāo)的命令GNU make 執(zhí)行時(shí)先比較目標(biāo)和依賴的時(shí)間戳依賴比目標(biāo)新才重建目標(biāo)。clean 用的是 del 而不是 rm因?yàn)?DJGPP 的命令行環(huán)境沒(méi)有 rmWindows cmd 下 del 同樣有效。這里最經(jīng)典的翻車(chē)點(diǎn)是縮進(jìn)用了空格而不是 Tabmake 會(huì)報(bào) missing separator 然后直接停止我在這上面丟過(guò)不少時(shí)間。如果源碼文件很多可以考慮模式規(guī)則和自動(dòng)推導(dǎo)但不建議新手一上來(lái)就用。DJGPP 2.04 在 DOS 下跑的 make 版本較老通配符和二次展開(kāi)的行為細(xì)節(jié)跟新版不一樣踩到會(huì)很難查。先用寫(xiě)全依賴的笨辦法項(xiàng)目穩(wěn)定后再優(yōu)化省下來(lái)的時(shí)間比省下的幾行字符值錢(qián)。3.3 用 gdb 調(diào)試 DJGPP 程序從黑匣子到斷點(diǎn)DJGPP 2.04 的 gdb 是 DOS 原生程序意味著你可以直接在 DOSBox 里啟動(dòng)它調(diào)試另一個(gè) DOS 程序。編譯時(shí)打開(kāi)調(diào)試符號(hào)并關(guān)閉優(yōu)化gcc -g -O0 -o hello.exe hello.c gdb hello.exe進(jìn)入 gdb 命令行后常用流程是(gdb) break main (gdb) run (gdb) next (gdb) print i (gdb) quitbreak main 在函數(shù)入口設(shè)斷點(diǎn)run 啟動(dòng)程序在斷點(diǎn)停下后 next 是單步執(zhí)行print 打印變量當(dāng)前值quit 退出。對(duì) DOS 程序來(lái)說(shuō)最實(shí)用的是拿它查段錯(cuò)誤run 之后如果看到 SIGSEGV用 backtrace 看?;厮莼灸芏ㄎ坏绞悄膫€(gè)函數(shù)寫(xiě)穿了數(shù)組。注意 gdb 在 DOSBox 里運(yùn)行也需要 CWSDPMI.EXE而且調(diào)試器本身要占內(nèi)存DOSBox 默認(rèn)的 XMS 設(shè)置太低時(shí)先把 DOSBox 配置里的 memsize 調(diào)到 16 或 32 再跑。遇到難纏的內(nèi)存問(wèn)題gdb 有一種現(xiàn)代環(huán)境里不常見(jiàn)的玩法直接在保護(hù)模式下訪問(wèn)顯存和端口。你在 gdb 里用 x/wx 0xB8000 可以直接查看 VGA 文本緩沖區(qū)的內(nèi)容在調(diào)試圖形驅(qū)動(dòng)時(shí)不需要額外打日志函數(shù)比插入一堆調(diào)試代碼干凈得多。3.4 用 binutils 做產(chǎn)物瘦身與檢查strip、objcopy 的實(shí)際場(chǎng)景DJGPP 2.04 自帶的 binutils 里日常最常用的三條是 strip、objcopy 和 strings。strip 去掉符號(hào)表把可執(zhí)行文件體型減下來(lái)strip -s game.exe對(duì)發(fā)布版本做這一步是基本禮儀一個(gè)帶完整調(diào)試符號(hào)的 DOS 程序體積能差一倍以上。-s 表示 strip all連重定位信息一起去掉副作用是 gdb 無(wú)法再對(duì)這個(gè) exe 下斷點(diǎn)所以 strip 要放在最后階段調(diào)試期間不要碰。objcopy 在自制操作系統(tǒng)和引導(dǎo)程序場(chǎng)景出場(chǎng)。DJGPP 的鏈接器默認(rèn)產(chǎn)出帶 DOS 擴(kuò)展器的可執(zhí)行格式但你的內(nèi)核最終要交給引導(dǎo)器加載到固定內(nèi)存地址需要的是一段沒(méi)有文件頭、沒(méi)有重定位的純二進(jìn)制。命令objcopy -O binary kernel.exe kernel.bin-O binary 把 COFF 段按內(nèi)存布局原樣導(dǎo)出生成 flat binary。這個(gè)文件沒(méi)有了 MZ 頭和保護(hù)模式 stub必須由引導(dǎo)代碼按約定地址加載并自行切換保護(hù)模式。用這個(gè)命令前建議先在鏈接腳本里把內(nèi)核段布局排好常見(jiàn)做法是讓 .text 起始地址等于期望的加載地址否則出來(lái)的 bin 會(huì)在開(kāi)頭帶一坨填充數(shù)據(jù)加載時(shí)錯(cuò)位。strings 是個(gè)被低估的檢查工具strings game.exe | grep -i error\|fail它把所有可打印字符串撈出來(lái)配合 grep 看程序內(nèi)置的錯(cuò)誤提示不用運(yùn)行就能排查資源文件路徑、配置文件格式這類問(wèn)題。我在處理老游戲漢化時(shí)經(jīng)常用它確認(rèn)文本是 ASCII 還是 GBK比盲改二進(jìn)制穩(wěn)得多。4. DJGPP 2.04 避坑指南環(huán)境變量、路徑分隔符與內(nèi)存模型的典型翻車(chē)現(xiàn)場(chǎng)DJGPP 2.04 的報(bào)錯(cuò)信息往往不像現(xiàn)代編譯器那么友好很多問(wèn)題表現(xiàn)為“編譯不過(guò)”或“運(yùn)行崩潰”實(shí)際根源卻藏在環(huán)境配置和內(nèi)存模型里。下面按我踩過(guò)的順序列出 5 個(gè)高頻坑每條按現(xiàn)象、原因、解決的順序說(shuō)清楚。4.1 stdio.h not found環(huán)境變量 DJGPP 丟失的癥狀與修復(fù)現(xiàn)象gcc 一編譯任何 .c 文件立刻報(bào)stdio.h: No such file or directory但 include\stdio.h 明明存在。原因DJGPP 環(huán)境變量沒(méi)設(shè)置或設(shè)置了但指向的 djgpp.env 路徑不對(duì)。gcc 知道自己是 DJGPP 工具鏈但沒(méi)被告知去哪找標(biāo)準(zhǔn)頭文件。另一個(gè)隱蔽點(diǎn)是你把 djgpp.env 的名字寫(xiě)錯(cuò)比如 Windows 默認(rèn)隱藏?cái)U(kuò)展名文件名真實(shí)是 DJGPP.ENV.txt指針就斷了。解決先用 echo %DJGPP% 確認(rèn)變量存在且路徑正確再看文件管理器里有沒(méi)有被系統(tǒng)吞掉擴(kuò)展名。我在 Windows 上習(xí)慣在 cmd 里執(zhí)行 dir C:\DJGPP\DJGPP.E* 看真實(shí)文件名比肉眼省心。配好這兩個(gè)變量后gcc -v 打印的 search 路徑里能看到 include 目錄一眼就能驗(yàn)證工具鏈認(rèn)知是否到位。4.2 Makefile 里文件路徑被反斜杠截?cái)嗦窂椒指舴目蝇F(xiàn)象Makefile 里寫(xiě) C:\DJGPP\LIB\libc.amake 報(bào)錯(cuò)說(shuō)找不到文件或把路徑截?cái)喑?C: 加一串亂碼。原因GNU make 把反斜杠當(dāng)作轉(zhuǎn)義字符處理DOS 風(fēng)格路徑里的 \D、\L 會(huì)被吃掉一層。這個(gè)問(wèn)題在 DJGPP 2.04 自帶的舊版 make 里尤其明顯。解決Makefile 內(nèi)部統(tǒng)一用正斜杠寫(xiě)路徑比如 C:/DJGPP/LIB/libc.aWindows 和 DOS 的文件系統(tǒng)層都能識(shí)別正斜杠。需要傳給編譯器參數(shù)時(shí)也盡量用正斜杠只在用戶交互的命令行里保留反斜杠習(xí)慣?,F(xiàn)在寫(xiě)任何 DOS 項(xiàng)目 Makefile我第一件事就是全局替換反斜杠省得后面的坑連成串。4.3 長(zhǎng)文件名支持LFN 開(kāi)關(guān)與 8.3 短文件名的取舍現(xiàn)象程序在 DOSBox 里 fopen(config.dat, r) 正常放到純凈 DOS 6.22 上 fopen 返回 NULL或者文件讀到一半數(shù)據(jù)錯(cuò)亂。原因DJGPP 2.04 通過(guò) LFN 接口訪問(wèn)長(zhǎng)文件名DOS 默認(rèn)沒(méi)開(kāi)啟該接口時(shí)需要驅(qū)動(dòng)程序支持。純凈 DOS 只有 8.3 短文件名而你的源代碼和資源文件在 Windows 下解壓時(shí)名字已經(jīng)拉長(zhǎng)程序按長(zhǎng)名字去找自然找不到。解決兩條路。一是給 DOS 加載 LFN 驅(qū)動(dòng)但老機(jī)器環(huán)境不好說(shuō)二是把涉及運(yùn)行期資源訪問(wèn)的文件名全部壓成 8.3 格式配置叫 config.cfg存檔叫 save.dat。我在移植老游戲時(shí)默認(rèn)按短文件名規(guī)劃資源省掉一長(zhǎng)串兼容性報(bào)告。4.4 C 異常或大數(shù)組段錯(cuò)誤DPMI 內(nèi)存不夠時(shí)的現(xiàn)象與處理現(xiàn)象C 程序里 throw 一個(gè)異常程序直接崩潰或者 main 里聲明一個(gè) 10MB 的局部數(shù)組運(yùn)行到函數(shù)入口就段錯(cuò)誤。原因DJGPP 2.04 的 C 異常依賴 DPMI 提供的內(nèi)存管理CWSDPMI 分配給程序的堆大小默認(rèn)從系統(tǒng)配置讀取DOSBox 默認(rèn)內(nèi)存不足時(shí)異常處理?xiàng):痛蟮臈I蠑?shù)組都放不下。局部數(shù)組超過(guò)幾 MB 時(shí)往往超出內(nèi)核對(duì)堆棧段的預(yù)留運(yùn)行時(shí)報(bào)段錯(cuò)誤。解決給 CWSDPMI 配置更大的內(nèi)存上限。常見(jiàn)做法是改 dosbox.conf 的 memsize 到 64或者在發(fā)布前用 stubedit 設(shè)置程序需要的 DPMI 內(nèi)存池大數(shù)組改用 malloc 放堆不要放棧。我給老平臺(tái)移植代碼定的規(guī)矩是單塊棧數(shù)組不超過(guò) 64KB超過(guò)一律堆分配這條規(guī)矩救了不少次。4.5 undefined reference to __main工具鏈版本混用的典型癥狀現(xiàn)象鏈接時(shí)報(bào) undefined reference to __main代碼里根本沒(méi)寫(xiě)過(guò)這個(gè)符號(hào)。原因這是 DJGPP 特有的 C 運(yùn)行時(shí)初始化符號(hào)。GCC 在 C 程序啟動(dòng)時(shí)通過(guò)調(diào)用 __main 完成全局構(gòu)造器流程該符號(hào)由 libc.a 提供。問(wèn)題是 binutils、libc 和 gcc 三者版本混搭時(shí)GCC 認(rèn)為自己需要 __main而鏈接器鏈接的庫(kù)卻改了符號(hào)名或沒(méi)有導(dǎo)出目標(biāo)文件格式對(duì)不上最終鏈接失敗。解決嚴(yán)格按照同一套發(fā)布的 DJGPP 2.04 包安裝不要混用不同分支的 gcc 和 libc。檢查方法是用 nm 在 libc.a 里搜索符號(hào)nm libc.a | grep main確認(rèn)存在后再查 gcc 的 specs 文件里的 crt 相關(guān)路徑是否指到同版本目錄。這個(gè)問(wèn)題沒(méi)有快速補(bǔ)丁把環(huán)境清理干凈重裝全套是最省時(shí)間的做法比對(duì)著 ld 腳本折騰半天下班。5. 進(jìn)階用法驗(yàn)證編譯產(chǎn)物與移植老代碼的兩個(gè)實(shí)用習(xí)慣工具鏈跑順之后有兩件事值得固定成習(xí)慣拿到陌生 exe 先驗(yàn)證遷移老代碼只動(dòng)封裝層。這兩件事能幫你把“運(yùn)氣好能過(guò)”變成“每次都能過(guò)”。5.1 驗(yàn)證產(chǎn)物三種快速檢查手段我不太相信“剛才編譯過(guò)了”這句話。拿到一個(gè) DJGPP 產(chǎn)物至少做三道檢查。第一道是 file 確認(rèn)格式file game.exe輸出出現(xiàn) 32 位、DOS 可執(zhí)行、保護(hù)模式這些關(guān)鍵詞說(shuō)明工具鏈目標(biāo)正確如果顯示 16 位 COM 或 EXE多半是環(huán)境里混入了其他 DOS 工具。第二道是 strings 掃內(nèi)嵌路徑strings game.exe | grep -i config\\如果在輸出里看到反斜杠形式的路徑程序在 DOS 下解析路徑時(shí)會(huì)出問(wèn)題趁發(fā)布前改掉。第三道是完整啟動(dòng)模擬發(fā)布前把 exe、CWSDPMI.EXE 和資源文件放進(jìn)同一個(gè)目錄在 DOSBox 里從干凈機(jī)器模擬一次完整啟動(dòng)。這步不能省我數(shù)不清多少次在開(kāi)發(fā)機(jī)上有 CWSDPMI 所以沒(méi)暴露換目錄后程序直接神秘消失。5.2 把老 Turbo C 代碼遷到 DJGPP只動(dòng)封裝層不動(dòng)業(yè)務(wù)邏輯從 Turbo C 遷到 DJGPP 2.0490% 的工作集中在頭文件和平臺(tái) API 替換。我給自己定的規(guī)則是造一個(gè) target-config.h把平臺(tái)差異全壓進(jìn)去。#ifndef TARGET_CONFIG_H #define TARGET_CONFIG_H /* Turbo C 的遠(yuǎn)指針在 DJGPP 平坦內(nèi)存里不需要 */ #if defined(__DJGPP__) #include stdlib.h #define _fmalloc malloc #define _ffree free #else #include alloc.h #endif #endif這個(gè)頭文件的作用是業(yè)務(wù)代碼繼續(xù)調(diào)用 _fmalloc底層在 DJGPP 下被替換成 malloc。32 位保護(hù)模式的平坦內(nèi)存里所有指針都?jí)蜻h(yuǎn)遠(yuǎn)指針概念本來(lái)就該廢棄用宏映射一下業(yè)務(wù)代碼不用改。移植時(shí)我會(huì)先把編譯錯(cuò)誤全部排干凈再逐個(gè)替換圖形和中斷部分最后跑回歸。圖形模式是另一個(gè)深水區(qū)保護(hù)模式訪問(wèn)顯存需要映射gdb 設(shè)斷點(diǎn)查 VGA 緩沖比 printf 調(diào)試高效。這套流程走完一個(gè)項(xiàng)目基本就有了自己的移植模板。我現(xiàn)在拿到任何老代碼第一反應(yīng)是先找它的平臺(tái)依賴集中在哪幾個(gè)頭文件而不是急著編譯。這個(gè)習(xí)慣讓后來(lái)的移植工作順手不少希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取