
简介对于希望深入理解早期操作系统设计理念的开发者、系统架构师和计算机科学专业学生来说这份资源以源代码形式提供了进入 Windows 98 系统的学习入口。压缩包共含 2 个文件包含 1 个 C 语言源文件和 1 份说明文档整体大小仅 1KB非常轻量便于快速下载与直接浏览。虽然包内文件精简但资源描述系统梳理了 Windows 98 的关键组件例如内核进程管理、内存分配、设备驱动响应、图形用户界面窗口与事件循环、FAT 文件系统以及内置网络协议栈等设计话题使读者能够以小见大初步把握该系统的基础构成与演进脉络。目前已有 339 人学习/下载。对于想由浅入深掌握操作系统原理或是首次接触微软早期内核代码的读者而言这份示例可以作为起点通过阅读该 C 源文件感受早期系统代码的简洁风格再结合描述中的模块清单展开对比学习为后续研究更完整的开源系统奠定基础。1. 先说结论你手里的“Windows 98完整源码”大概率是什么每次有人往群里丢一个几百兆的“Windows 98完整源代码”压缩包我都有一种预感解压之后里面要么是符号文件改后缀要么是某个老SDK的示例合集真正能称得上“源码树”的东西少之又少。这背后有个硬事实Windows 98的完整源码从来没有被官方正式公开过网上流传的所谓完整包大多是历史残片、半成品工作目录甚至是爱好者拼出来的伪作。这篇笔记要解决三件事告诉你一份真正的Windows 98源码包在模块上应该长什么样拿到手之后怎么快速验证真伪以及在没有官方源码的情况下用开源项目把同等机制跑起来的完整路线。2. 一份Windows 98源码包该有的骨架从内核到桌面的模块拆解2.1 顶层先认三大件Kernel、GDI与UserWindows 98属于9x内核系和后来的NT内核相比模块边界更模糊但源码顶层依然有清晰的三大件。Kernel负责进程调度、内存管理和线程同步GDI负责画线、填色、字体和位图输出User负责窗口、消息、菜单和控件。这三块在源码树里通常以三个顶层目录出现名字可能是kernel、gdi、user也可能带版本后缀。无论命名怎么变只要顶层缺了其中一个这份“完整源码”就已经不完整了。拿到任何一份源码包我习惯先跑一条命令把结构摸清楚# 生成顶层目录结构最快速度看全貌 find . -maxdepth 2 -type d | sort | head -60 # 统计源码文件类型占比判断是否混入大量非源码文件 find . -type f \( -name *.c -o -name *.h -o -name *.asm \) \ | awk -F. {print $NF} | sort | uniq -c | sort -nr第一条命令让你在几十秒内看到源码树的整体布局GDI、User、Kernel这些关键目录有没有一秒钟就能判断。第二条命令统计.c、.h、.asm三类文件的相对数量。如果.asm文件占比较高别意外9x内核里汇编比例确实很高尤其显示驱动和启动引导部分如果统计结果里出现大量.pdb、.dbg这样的非源码后缀后面那个“完整”就要打个问号。这个检查动作十分钟内能完成是后续所有深读的基础。拿到源码后再对照一份老Windows 9x的公开架构资料来读效率会高很多。这里我把三大件的预期内容列成一张自查表你在源码包里逐项打勾就行模块应该有的核心内容伪包里常见的情况Kernel调度、内存换页、物理内存管理、同步原语只有公共头文件实现文件缺失GDI画刷、字体、位图、显示驱动接口只有一个或几个示例程序User消息循环、窗口类、菜单、输入处理顶层目录直接缺失驱动VxD初始化、消息处理、I/O端口封装以符号文件顶替没有.c/.asm这张表最大的用途不是帮你确认“源码真”而是帮你确认“能读什么”。三大件齐了你至少能把系统主循环讲明白缺了驱动部分你对图形栈的理解就是断裂的。很多人在网上找到资源后兴奋地开始读结果读了两天发现手里的东西根本不成体系白白浪费热情。2.2 启动路径从IO.SYS到VMM32的实模式到保护模式跨越Windows 98的启动过程和后来的NT完全不一样是理解9x内核时最绕的一段。实模式的IO.SYS是16位汇编负责从引导扇区开始做最基本的硬件探测然后加载vmm32.vxd。vmm32.vxd不是普通驱动它是把多个虚拟设备驱动打包在一起的文件其中核心是虚拟机管理器VMM负责从实模式切换进保护模式建立分页、中断和I/O权限模型然后再把Kernel32、GDI、User依次拉起来。我一般建议按这条链去读启动源码引导扇区 → IO.SYS实模式初始化 → 加载VMM32并切换保护模式 → VMM初始化内存管理 → 加载Kernel32/GDI/User → 进入桌面。每一步对应源码里不同目录风格也完全不同IO.SYS里几乎全是汇编和段寄存器操作VMM部分开始有C语言但关键路径仍是汇编。如果你没有启动相关的源码片段可以先用虚拟机观察真实启动过程来补课。在QEMU里跑一个旧Windows系统用调试器观察模式切换点比自己对着源码空想要快得多# 创建一个2GB的虚拟磁盘用于安装旧系统 qemu-img create -f qcow2 legacy98.qcow2 2G # 启动QEMU并开启调试端口等待调试器连接 qemu-system-i386 -m 256 -hda legacy98.qcow2 -S -s这里有三个参数值得解释。-S让CPU在启动后立刻暂停等待调试器接管-s打开gdb远程调试端口-m 256给虚拟机256MB内存模拟老系统常见的物理容量。连接调试器后你可以在IO.SYS和VMM32的切换点打断点观察GDT、LDT和页表的建立过程。这套玩法比单纯读源码更能建立“真实感”回头再读代码很多指令就对上号了。顺着启动链条我列一个快速对照表方便你在源码和运行现场之间切换查找启动阶段CPU模式主要源码模块观察要点引导扇区实模式MBR/boot代码分段加载入口IO.SYS实模式硬件枚举、文件系统中断调用序列VMM32加载实模式→保护模式虚拟机管理器GDT/页表初始化内核拉起保护模式Kernel/GDI/User模块加载顺序2.3 VxD驱动模型为什么驱动占了源码的一半Windows 98的驱动在源码里不叫driver而是叫VxD虚拟设备驱动。这个模型的历史包袱很重每个驱动都伪装成一个虚拟设备文件后缀是.vxd同时承担中断处理、I/O端口过滤和内存映射。驱动目录通常按display、sound、mouse、network细分其中显示驱动代码量最大因为当时显卡厂商各自改一套一套VxD动不动上万行。读VxD源码的关键不是逐行读而是找三个固定入口设备初始化例程、设备终止例程、消息分发例程。VxD之间靠消息机制通讯源码里往往表现为一个巨大的switch-case。如果你在源码里看到一堆以DM_开头的分支那就是VxD消息分发别被它的规模吓到核心逻辑永远集中在少数几个case分支里。真正难的是建立“哪个驱动调了哪个消息”的调用链这个只能靠调试器的日志来验证。VxD和后来的NT驱动模型差别很大读懂之前先摆清边界维度VxDWindows 9xNT驱动Windows NT系运行环境与内核共享地址空间独立内核模式组件入口模型消息分发为主IRP分发为主I/O访问直接端口/内存操作抽象I/O管理器中断处理自身挂接依赖内核中断框架这张表的价值在于防止你用NT驱动的经验去读9x源码。我见过有人拿着NT驱动的阅读习惯去啃VxD结果被消息分发机制绕得晕头转向。记住9x源码里的“设备”是一个软件抽象概念不是硬件意义上的设备这一点想通了驱动代码就入门了一半。3. 没有官方源码怎么练手用开源“近亲”把机制跑起来3.1 先选路线ReactOS还是Wine官方源码拿不到不意味着没法动手。要理解一个完整操作系统的骨架我首选ReactOS。ReactOS是一个目标兼容Windows的开源操作系统有自己的内核、图形子系统和窗口管理器源码组织方式和一个真实Windows系统的模块划分高度接近。虽然它的内核更靠近NT而非9x但内存管理、进程调度、驱动封装这些机制是一脉相承的读它的收获远大于读一些零碎的9x残片。Wine则是一个用户态兼容层它没有自己的内核只负责把Windows API调用翻译给宿主机。用Wine学不到调度、内存和驱动模型它能教你的只有PE加载、API实现和系统调用的转发路径。如果想学系统全貌Wine不是合适的选择如果将来要写兼容层或做二进制翻译Wine的代码库比ReactOS更值得研究。两条路线本身不冲突先后关系看你当前目标。我把选型维度直接列出来维度ReactOSWine系统形态完整操作系统用户态兼容层是否自带内核是否适合学习的系统层启动/内存/驱动API/PE加载与9x机制的相似度中高架构相通低构建难度较高中等为什么我不建议直接下载ReactOS的预编译镜像来观察因为编译过程本身就是学习你在编译输出里能看到每个模块的先后顺序能感受到老系统那种“编译器版本决定一切”的血泪。从源码构建得到的知识深度是下载镜像无法比的。3.2 建立交叉编译环境最小命令在Linux主机上编译ReactOS需要一套交叉工具链。原因很简单ReactOS的目标平台是i386而我们通常跑在x86-64的Linux上。用发行版自带的交叉编译器可以省去很多手工配置。以Ubuntu/Debian系为例最小准备命令是sudo apt update sudo apt install -y build-essential ninja-build cmake mingw-w64 \ p7zip-full wget git # 拉取源码树--depth1只取最新提交 git clone --depth1 https://github.com/reactos/reactos.git cd reactos这里--depth1对只想读代码和编镜像的人足够省掉全部历史提交能少下载很大体积。注意ReactOS对编译器版本有要求发行版自带的MinGW如果版本过高部分老模块会报警告甚至编译失败。我一般建议先别折腾容器直接用系统工具链试一次把错误信息收集好再决定要不要降到某个历史版本的工具链。这年头编译环境的坑八成是版本不匹配造成的而不是你代码写错了。如果你不想污染宿主机环境用容器也是一种常见做法。在容器里安装相同依赖然后把ReactOS源码树挂载进去编译能保证多次构建的一致性。唯一的代价是容器镜像也要占几个GB磁盘紧张的人慎重。3.3 编译一个最简系统并启动它整个ReactOS源码树全量编译耗时大致在半小时到一小时磁盘预留10GB以上才安心。编译命令如下mkdir -p build cd build cmake .. -G Ninja \ -DCMAKE_TOOLCHAIN_FILE../toolchain-mingw32.cmake \ -DREACTOS_SOURCE_DIR.. -DREACTOS_OUTPUT_DIR. ninja bootcd-G Ninja指定Ninja作为构建后端并行度比Make默认设置高编老系统快很多。CMAKE_TOOLCHAIN_FILE指向ReactOS自带的交叉工具链描述文件告诉CMake用哪个编译器、哪个目标平台。REACTOS_OUTPUT_DIR决定生成的镜像和中间文件放哪。ninja bootcd最终会在输出目录生成一个可启动的Live CD镜像。如果只想编内核验证自己的改动可以把目标换成内核模块名但第一次跑通建议直接用bootcd全链路通了再回来玩局部。编译完成后启动验证qemu-system-i386 -cdrom bootcd.iso -m 512 -soundhw all-m 512给512MB内存ReactOS在64位宿主上跑得流畅-soundhw all虚拟出声卡用来看驱动的加载日志。启动后重点观察三件事内核日志里内存检测的结果、设备驱动被枚举并初始化的顺序、桌面出现之前窗口管理器做了什么。这三个观察点正好对应第2章讲的Kernel、驱动、User三大块。跑起来之后再回头读代码你会发现很多函数调用都能在日志里找到落点阅读效率完全不一样。如果编译失败先查日志里的PKG_CONFIG、路径和工具链版本别急着怀疑源码。ReactOS的老模块玄学问题大多出在环境变量上比如REACTOS_SOURCE_DIR没写对就找不到头文件。这种翻车我在项目里见得太多了。4. 拿到一份“完整源码”之后先做三个验证动作4.1 看文件清单不该出现的文件网上经常有人分享打包好的“Windows 98完整源码”但解压之后外面那层目录名看着挺正规里面却充斥着.pdb、.dbg、.map、.ilk这些中间产物。真正规范的源码树不会把构建中间产物打进去源码文件.c/.h/.asm才是绝对主体。如果压缩包最外层是一堆随机命名的文件夹里面满是带时间戳的散文件那它更像从某台开发机上随手抓出来的“工作目录”不是面向公开发布的源码包。一个简单判断法数一下源码文件占压缩包总文件数的比例。正常源码树这个比例在70%以上伪包往往连40%都不到剩下的是符号文件、文档和临时文件。用前面的find统计命令就能算出来几分钟出结果。注意Windows 98时代的源码包不可能没有构建脚本那个年代没法靠IDE一键编译至少会有几个批处理文件或Makefile。没有构建脚本的“源码”只能算考古材料没资格叫工程源码。我和A同学有一次在某论坛下到一个包外面写“全内核”解压后一堆.pdb明显是从某台开发机上偷拷出来的。那种包不是不能读但它能给你的只是片段拼图不是系统全貌。4.2 看构建脚本老编译环境是黑匣子Windows 98内核当年是用老一代C编译器和汇编器构建的。拿到源码包第一件事翻开构建脚本看它引用了什么工具名、什么环境变量、什么SDK路径。如果脚本里写的是你机器上没有的编译器不要立刻找新版替代老代码用新工具链编译大概率会因为语法差异翻车。正确做法是在虚拟机里搭一个和老编译器兼容的系统环境尽量还原脚本假设的环境变量。另一个老项目特有问题是源码编码。简体中文环境的注释可能是GBK或GB2312而代码主体是ASCII。直接用现代编辑器打开这类文件注释区域会显示成乱码如果编辑器自动把整个文件按UTF-8识别还可能把ASCII字符串搞出奇奇怪怪的显示问题。处理方式是先用一个能手动指定编码的十六进制编辑器打开文件头确认编码后再决定要不要批量转换。千万别全目录一键转码把编码猜错了会让源码变得更乱。老构建脚本的另一个常见问题是CRLF换行符。在Linux下直接跑.bat会因为末尾的\r报莫名错误这是常见的翻车点。我的习惯是用sed把CRLF转成LF再看内容但别把原始脚本改坏了因为有些老工具还指望CRLF环境。这里有一个很实际的分辨技巧老构建脚本里如果出现环境变量的赋值与批量文件复制说明这个包至少是被组织过的如果只有一个孤零零的.sln或.dsp说明它只是个工程壳。这两者在阅读价值上是天壤之别。4.3 用字符串与符号交叉验证真伪这个验证动作是最直观的十分钟能出结果。先用file命令看可疑文件的真实类型再用strings提取字符串感受里面的内容密度# 看看目录下所有.c文件的真实类型 file ./kernel/*.c | head -30 # 对最大的几个“源码文件”做字符串提取 strings -n 4 ./shell/some_big_file.c | head -40-n 4是让strings只输出长度至少4的连续可打印字符过滤掉太短的噪声。如果一个大文件跑完strings几乎没输出说明它是压缩过或加密过的源码树里不该出现这种文件如果输出的东西里能认出内核专有术语、函数名、错误文案那这个文件至少来自某个真实系统。注意符号文件里也能提取出大量函数名所以strings只能证明“来自真实系统”不能单独证明“这是源码”。真正的源码文件里除了函数名还应该有注释、条件编译、写法自然的本地变量定义这些特征是符号文件给不了的。更进一步可以找几个已知的系统内部字符串做交叉验。老Windows系统的内核镜像里有些广为人知的调试输出字符串如果这些字符串出现在某个.c文件的源码里说明这个文件很可能是从反汇编里“抄”回来的而不是原始源码。原始源码的注释风格和变量命名通常保留开发者的个人习惯盲目从反汇编还原出来的代码反而缺少这种一致性。这一招能帮你筛掉市面上大部分“逆向重写版”伪源码。5. 阅读老系统源码的四个坑从编译到理解5.1 源码能看不能编环境断层是第一道坎现象源码包目录结构看着完整该有的都在但一编译就报几百条错误头文件路径不对、编译器版本不认、汇编器语法不符。原因Windows 98年代的构建环境今天基本绝迹老编译器、老SDK、老硬件头文件互相绑定。源码本身未必有问题而是它的编译假设和你现在的环境完全不匹配。这类源码通常还依赖特定的环境变量、批处理调用顺序少一个环节都编不起来。解决别想着整体编过。先把目标切成小块找一个最小单元比如单独编一个驱动模块做交叉编译实验。用虚拟机装一套老系统对应年代的工具链再按构建脚本里的步骤手动执行遇到哪个命令失败就补哪个环境变量。# 尝试单独编译一个不依赖硬件的模块不要把整个kernel一起编 gcc -m32 -c -D_WIN32 -I./inc -I./kernel ./kernel/某个独立算法文件.c -o test.o如果连续三次卡在同一类错误上大概率是源码本身残缺停下别再耗时间。读老源码的投入要有上限这条线要提前划好。5.2 宏定义泛滥代码跳来跳去逻辑线断了现象打开一个.c文件满屏都是宏真正的C代码被夹在几十层条件编译之间函数体一会儿出现一会儿消失跳转关系多到理不清。原因Windows 98源码是漫长的历史产物兼容性分支遍布各个角落还要同时支持不同CPU和不同显卡方案预处理器指令成了组织代码的主要手段。有些关键数据结构不用宏展开根本看不出最终形态。解决用预处理器把文件展开一次再读。在GCC环境下下面这条命令可以直接看到宏展开后的真实代码gcc -E -D_WIN32 -I./inc -I./kernel ./kernel/sched.c -o sched.i-E让编译器做完预处理就停下-D_WIN32预定义平台宏-I指定头文件路径输出的sched.i就是宏展开后的完整代码。读的时候以.i为准原文件当作索引来用。这一步能把那些跳来跳去的宏暴力消解掉逻辑主线和分支条件全部摊开阅读体验瞬间不一样。5.3 16位和32位混合指针大小和寻址方式捉弄人现象代码里到处是far、near、段寄存器赋值函数参数明明是个指针却只有16位宽度地址计算需要手工拼接段值和偏移量。原因Windows 98从16位Win3.x继承了大量实模式遗产启动阶段和部分驱动仍是16位代码与32位保护模式代码共存。编译器在面对混合模式时使用不同的指针修饰符阅读时不注意模式切换就会把地址算错。解决读代码前先确认当前文件处于哪种模式。实模式文件看段寄存器、看16位偏移保护模式文件看平坦内存模型。遇到模式切换点在纸上画出物理地址的组成方式实模式下地址是段值左移4位加偏移保护模式下是选择子加偏移。这个换算关系要刻在脑子里不然连数据结构的地址都对不上。用现代IDE的悬停提示看变量类型也有帮助但老源码的指针修饰符未必被现代解析器识别最终还是得自己判断。5.4 源码注释和实际行为对不上别硬读现象函数名暗示它管A任务读进去发现它在做B任务注释写着“退出时释放资源”代码却根本没有释放动作。顺着源码注释走越走越偏。原因老源码经过多年的功能变更函数被反复修改注释和名字经常停留在上一个版本。有些改动是在调试时临时打上去的发布时没有清干净。更麻烦的是源码经过他人转手整理后目录结构和原始构建环境错位函数归属关系失真。解决遇到注释和实际行为冲突时以反汇编伪代码为最终依据。用调试器或反编译器把对应模块的二进制打开对比实际指令序列和源码逻辑。如果源码逻辑与伪代码一致说明源码可信如果不一致以反汇编结果为准把源码当作“参考译文”来用。这个方法听着绕实际用过一次你就懂它的价值——它帮你绕开了源码本身可能被改动过的事实。提示读老系统源码先确认目标范围别想着一口气全读懂。一次只盯一条调用链比漫无目的地翻目录有效得多。6. 最后留一手把读到的机制落到自己的迷你系统上6.1 最小启动骨架从引导到保护模式C入口读再多源码不如自己写一个可以启动的迷你内核。我建议从最简骨架开始一个引导入口文件把CPU推进保护模式跳进一个C函数在C函数里做循环空转。这个过程会把第2章的启动链条变成肌肉记忆。三个文件就够了; entry.asm —— 多引导头 32位入口栈放到BSS段 [BITS 32] section .multiboot align 4 dd 0x1BADB002 dd 0x00 dd -(0x1BADB002 0x00) section .text global start extern kmain start: mov esp, stack_top call kmain cli hlt section .bss align 16 stack_bottom: resb 4096 stack_top:/* kmain.c —— C世界入口这里就是你的“内核” */ void kmain(void) { for (;;) { /* 以后进程调度、内存管理都从这里开始 */ } }/* linker.ld —— 把入口段放到1MB处栈放在BSS */ ENTRY(start) SECTIONS { . 1M; .text : { *(.multiboot) *(.text) } .bss : { *(COMMON) *(.bss) } }编译并启动nasm -f elf32 entry.asm -o entry.o gcc -ffreestanding -m32 -c kmain.c -o kmain.o ld -m elf_i386 -T linker.ld entry.o kmain.o -o kernel.elf qemu-system-i386 -kernel kernel.elf-ffreestanding告诉GCC不依赖宿主系统库-m32生成32位代码-T linker.ld指定链接脚本。如果启动后QEMU没有报错并进入死循环说明你已经亲手复现了“从引导到保护模式再到C程序”这条老系统最喜欢走的路径。6.2 验证习惯比代码量更重要跑起来之后别急着写更多功能先把这条链路的验证习惯固定下来。每次改动引导代码都要确认三件事多引导头有没有被正确识别、栈指针有没有落在BSS段、C入口有没有真正被调到。前两个看QEMU启动输出第三个最简单在kmain里改一个全局变量再用调试器读它的值。这套习惯和你在老源码里读到的“先验证启动再验证功能”是同一个思路。我自己读老系统源码最大的教训是不要空读每一段机制都要有一个可以运行的验证载体。读完调度器就写一个最小调度器读完分页就写一个页表切换demo读完消息循环就写一个窗口消息示例。哪怕实现粗糙只要能在QEMU里跑起来你对源码的理解就是实打实的。ReactOS也好Windows 98残片也好最终都应该成为你构建自己系统的原材料而不是阅读成就感的来源。希望这篇能帮到你。本文还有配套的精品资源点击获取