简介:《30天自制操作系统》配套源码包,适合正在研读该书、或对操作系统底层实现感兴趣的计算机专业学生和自学者。内容按学习进度组织,覆盖启动加载器、内核开发、进程调度、内存管理、文件系统和设备驱动等关键模块,从零开始逐步构建一个可运行的系统,帮助读者把书本理论转化为可调试、可修改的真实代码。压缩包共20.93MB,包含2000个文件,以1711个C源文件为主,辅以149个头文件和140个文本说明,C文件对应各阶段功能实现,头文件提供模块接口,文本文件则记录关键思路或操作笔记,目录结构清晰便于按天查阅。目前已有680人学习使用,适合想要对照书籍亲手验证操作系统原理、提升系统编程与排错能力的读者。
1. 30天自制操作系统源文件:照着书敲代码不可能跑通,这套源码包才是唯一入口
很多人是冲着「30天」这两个字买书的,结果翻开第三天就在引导扇区上卡了一周。真正的问题是:书里印刷的汇编代码和纸盘(CD-ROM)里的源文件并不完全一致,书是排版后的版本,纸盘才是能跑的版本。这份《30天自制操作系统》的源文件包,就是把每一章的完整工程代码、中间产物和作者自带的工具链整理在一起的东西。对照着书敲代码敲不下去的人、想跳到后面看内存管理和多任务的人、以及想在自己机器上完整复现一遍从引导扇区到窗口系统启动过程的人,都适合先把这套源文件包下载下来再谈写代码。说白了这份资源就是你的后悔药,敲坏了随时能回退到能跑的版本。
2. 先拆压缩包:源文件包目录结构与每章工程的对应关系
拿到压缩包的第一件事不是打开阅读文档,而是搞清楚目录结构。这个项目的源文件包和普通开源工程不太一样,它不是一个单一仓库,而是把 30 天里每一天的成果做成了相对独立的工程目录。目录组织方式直接决定了你后面复现时去哪个文件夹找代码、改哪个文件、跑哪条命令。
2.1 从 helloos0 到 haribote:理解目录命名的演进逻辑
前两天的代码规模还很小,基本是「一个汇编文件 + 一个 Makefile」就撑起一天的内容,所以目录名直接体现版本迭代:helloos0 是最初的「什么也不做只显示一行字符」的版本,helloos1 开始加入读盘循环,helloos2 进一步调整了启动区和操作系统主体之间的分工。到第三天引入 C 语言后,工程代号变成了 haribote 系列,之后就一直沿用这个命名直到第三十天。
不同整理版的目录命名略有差异,常见的有两种:一种是按天数拆成 01、02、03 这样的编号目录,下面再放当天的工程文件夹;另一种是直接把每天的工程文件夹平铺在根目录下。无论哪种,每个工程文件夹内部的结构是一致的,拿第三天的工程举例:
| 文件/目录 | 作用 |
|---|---|
| ipl.nas | 引导扇区汇编源码,负责加载操作系统主体 |
| asmhead.nas | 从实模式切换到保护模式的汇编代码 |
| bootpack.c | C 语言入口,调用绘图、显存操作等函数 |
| Makefile | 定义了从 .nas 到 .img 的完整构建流程 |
| haribote.img | 最终生成的软盘镜像,可用 QEMU 直接启动 |
注意第二天的 helloos1 与第一天的 helloos0 之间,变化集中在ipl.nas里读盘循环的若干条指令,其他文件几乎没动。用对比工具去看这几天代码的 diff,比闷头敲一遍更有价值。我自己复现时习惯把相邻两天的工程目录做一次递归对比,用diff -r day01 helloos0 day02 helloos1这种命令快速锁定当天新增的代码段,效率比从头读到尾高得多。
2.2 藏在工具集目录里的专用工具链:为什么不能用通用 binutils 直接替代
30 天工程最容易被忽略的部分是工具集目录(常见名是 z_tools 或 tolset),它装着作者自己开发的一整套构建辅助工具。网上的整理版一般都会附带这个目录,因为它才是整套工程能在 Windows 上跑起来的关键。核心工具包括:
| 工具 | 作用 |
|---|---|
| make.exe | Windows 下的 make 程序,负责解析 Makefile 并调度编译 |
| edimg.exe | 把二进制文件打包成软盘镜像 img 的工具,等价于手工拼装 FAT12 磁盘结构 |
| bim2bin.exe | 把作者自定义的二进制目标格式 BIM 转换成普通可执行二进制 |
| bin2obj.exe | 把 .bin 文件包装成目标文件,供链接阶段使用 |
| bimlink.exe | 作者自己的链接器,按自定义规则把多个目标文件拼成可执行程序 |
| makefont.exe | 根据字体定义文件生成汉字和英文字模的二进制数据 |
这里有个新手容易踩的思维惯性:觉得自己装了 NASM、GCC 和通用 ld,就可以把 Makefile 里的工具全部替换成标准工具链,结果一编译就翻车。原因是这套工程的链接走的是作者自研的 BIM 格式,通用链接器根本不认识这种中间格式。你当然可以自己写脚本把三步并两步,但最终生成的镜像布局和书里描述的「偏移 0x4200 处加载操作系统主体」就对不上了。想完整复现书中的行为,最稳妥的做法是沿用工具集里的原始工具,不要去替换它们。
需要特别说明的是,工具集里的edimg.exe是个黑匣子,它既负责构造 FAT12 的引导扇区、文件分配表和根目录区,也负责把编译好的操作系统二进制写入指定位置。只看文件名根本猜不到它有这么多职责,所以初次接触的人往往花了半小时折腾镜像却不知道问题出在 edimg 的参数上。后续第 5 章我会专门讲这块的坑,现在你只需要记住一件事:不要在没搞懂 Makefile 之前手动去调 edimg 的参数。
3. 搭出可复现的开发环境:MSYS2 下把 helloos0 编译成可启动镜像
原理讲完了,接下来进入动手环节。这个项目的构建环境窗口期比较特殊——它诞生在 Windows 主导的时代,工具集里全是 Windows 可执行文件,但书的最后又用到了不少 Linux 风格的脚本。我实测下来的最佳组合是 Windows + MSYS2 + QEMU,一次配置完后面 29 天都能用。
3.1 为什么建议用 MSYS2 而不是 Cygwin 或纯 Linux
先说结论:MSYS2 的包管理比 Cygwin 省心,而且对 Windows 原生 exe 的调用兼容性比纯 Linux 好得多。纯 Linux 环境不是不能跑,但工具集里的 edimg.exe 和 makefont.exe 是 Windows PE 格式,Linux 下必须靠 wine 才能执行,每跑一次 make 都要被 wine 的初始化拖慢十几秒,而且 wine 的路径映射偶尔会抽风,属于给自己加难度。
Cygwin 能用,但你要手动选包、改 PATH、处理符号链接,配置成本比 MSYS2 高。MSYS2 则一条pacman -S就能装齐需要的编译器和模拟器,并且它的根目录天然把 Windows 的 C 盘映射成了/c,这意味着 Makefile 里写../z_tools/make.exe这种相对路径也能正常解析,和书里的构建逻辑完全兼容。
我用的是 64 位 MSYS2,安装完成后先做三件事:换镜像源(国内直连官方源很慢)、更新核心包、安装必要工具。第一次安装会等比较久,后面就顺了。
3.2 安装 nasm、QEMU 并接通工具链路径
打开 MSYS2 终端,执行下面这组命令安装编译器和模拟器:
# 安装汇编器与模拟器 pacman -S nasm qemu # 确认安装版本 nasm -v qemu-system-i386 --version逻辑说明:nasm 用于把 ipl.nas 这类汇编源码编译成扁平二进制,QEMU 用于启动生成的镜像。注意这里安装的是 qemu 包,它提供qemu-system-i386,而不是qemu-img或qemu-system-x86_64。qemu-system-x86_64也能跑 16 位引导程序,但默认启动 CPU 模式与书里的 i386 设定有差异,个别中断行为可能不一致,所以建议直接用 i386 版。
接下来把工具集目录接进 PATH:
# 假设工具集在 /c/project/30days/z_tools export PATH=/c/project/30days/z_tools:$PATH # 验证 make 和 edimg 是否可用 make -v edimg --help 2>&1 | head -5逻辑说明:Makefile 内部用的是相对路径引用的工具,理论上不设 PATH 也能跑,但设了之后至少有两个好处:一是单独执行edimg调试镜像时不用打全路径,二是后续写的辅助脚本可以直接复用这些命令。edimg --help这一步只是为了确认这个 exe 能在 MSYS2 环境里正常运行,它实际版本的帮助信息多半不是标准输出,用head截取前几行就够了。
3.3 进入第 1 天的工程目录跑通 make
环境就绪,现在进入第一天的工程目录执行构建:
cd /c/project/30days/day01/helloos0 # 看一下 Makefile 内容,确认目标文件 cat Makefile # 执行构建,生成 helloos.img make # 启动 QEMU 验证镜像可引导 make run逻辑说明:helloos0 的 Makefile 只有两个关键目标,helloos.img由ipl.bin经过 edimg 包装生成,run目标则调用 QEMU 以软盘方式加载镜像。make执行后如果没有任何报错,当前目录会出现helloos.img和临时文件ipl.bin。
参数说明:make run本质上是qemu-system-i386 -fda helloos.img,-fda表示把这个 img 当作第一个软盘驱动器接入。你可以手动执行这条命令替代make run以便加调试参数。如果想换用硬盘方式启动,改成-drive file=helloos.img,if=ide,format=raw也能跑,但后续几天的读盘中断会依赖软盘接口,非必要不要换。
提示:如果 make 报错提示找不到命令,先确认你是在 MSYS2 终端里执行而不是 Windows 自带的 cmd,
make在 cmd 里是找不到的。
4. 深读前三天代码:FAT12 引导扇区与保护模式切换的真实边界
环境跑通只是起点,真正决定你能不能改代码改对的是对前三天源码的理解。前三天是整本书的基石:第一天搭引导扇区,第二天让引导扇区学会从软盘读数据,第三天从 16 位实模式切换到 32 位保护模式并进入 C 语言入口。这三天的任何一个字节写错,都会以「黑屏、白屏、无限重启」这类看似玄学的方式表现出来。
4.1 ipl.nas 的 BPB 参数表:哪些字节改错会直接白屏
第一天的ipl.nas开头是一段标准的 FAT12 引导参数。我简化后的关键片段如下:
; 入口 ORG 0x7c00 JMP entry DB 0x90 DB "HELLOIPL" DW 512 DB 1 DW 1 DB 2 DW 224 DW 2880 DB 0xf0 DW 9 DW 18 DW 2 DD 0 DD 2880 DB 0,0,0x29 DD 0xffffffff DB "HELLO-OS " DB "FAT12 " RESB 18 entry: MOV AX, 0 MOV SS, AX MOV SP, 0x7c00 MOV AX, 0x0a00 MOV DS, AX MOV ES, AX ; 显示字符串 MOV SI, msg ...(省略显示循环)这段代码核心是ORG 0x7c00和 BPB 参数。ORG 0x7c00告诉汇编器,这段代码将来会被 BIOS 加载到内存地址 0x7c00 执行,所以所有标签和跳转地址都要按这个基址计算。BPB 表则是软盘的文件系统身份证,BIOS 读取软盘时依赖这段数据判断扇区大小、FAT 表位置、根目录位置。
BPB 表里最容易改错的是两个字段:DW 18表示每个磁道的扇区数,DW 2表示磁头数。这两个值必须和实际软盘几何参数一致,任何一边改错,第二天的读盘循环就会在某一个磁道上读到错误位置,表现是启动后卡死或反复读盘。很多人在第一天就碰到「镜像能启动但显示乱码」,多半是动了DB "HELLOIPL"这个卷标却忘了它正好占了 8 字节空间,后续字段全部位移。
另一个关键字节是最后两字节的引导标志0x55 0xAA。BIOS 检查这 510、511 两个偏移处的值必须是这两个字节,否则直接拒绝引导。如果你在代码里加了变量导致启动区超过 512 字节,edimg 会在生成镜像时报错,但如果你手动改了 ORG 地址,生成的镜像可能超出 510 字节却不报错,启动时直接黑屏。
4.2 读盘循环:INT 0x13 的寄存器协作顺序
第二天的核心是让引导扇区把软盘上其他扇区的内容读进内存。关键代码是:
read_sector: MOV AX, 0 MOV ES, AX MOV BX, 0x7e00 ; 目标缓冲区地址 MOV AH, 0x02 ; 读盘功能号 MOV AL, 1 ; 读取扇区数 MOV CH, 0 ; 柱面号 0 MOV CL, 2 ; 扇区号 2(0 为引导扇区) MOV DH, 0 ; 磁头号 0 MOV DL, 0 ; 驱动器号 0(软盘A) INT 0x13 JC error ; 进位标志=1 表示读失败这里的寄存器配合顺序是个经典易错点:AH 是功能号,AL 是扇区数,CH 是柱面,CL 是扇区号,DH 是磁头,DL 是驱动器。初学者最常见的错误是把 CL 写成柱面号、CH 写成扇区号,结果是每次读到的数据都是错位的。INT 0x13返回时通过进位标志报告成功失败,所以JC error这个判断不能省,在实际软盘上失败是常态,没有这个判断你会看着黑屏完全不知道发生了什么。
另一个值得注意的细节是MOV CL, 2而不是MOV CL, 1。软盘的第一个扇区即扇区号 1 是引导扇区,已经被 BIOS 加载到 0x7c00,这里要读的是第二个扇区的内容,所以从 2 开始。后面的工程里扇区号经常会加上「偏移量」,因为 FAT 12 格式下第 2 个扇区往往是 FAT 表的起始位置,操作系统主体备份放在更靠后的位置。
4.3 asmhead.nas 的实模式到保护模式跳转:GDT、A20 与内存布局
第三天的asmhead.nas完成了从 16 位实模式到 32 位保护模式的切换,这中间涉及三个动作:关闭中断、设置 GDT、开启 A20 地址线。整个序列的顺序不能乱,任何一步提前都会导致后续 C 代码无法正确执行。精简后的关键流程:
; 进入保护模式前必须先关闭中断 CLI ; 设置 GDT 寄存器 LGDT [GDTR0] ; 开启 A20 地址线 MOV AL, 0xd1 OUT 0x64, AL MOV AL, 0xdf OUT 0x60, AL ; 把 CR0 的第 0 位 PE 置 1,切换到保护模式 MOV EAX, CR0 OR EAX, 0x00000001 MOV CR0, EAX参数说明:CLI先关闭可屏蔽中断,否则在切换过程中来一个外部中断,CPU 状态不完整很容易崩溃。A20 地址线是 8086 时代遗留的历史包袱,不开启的情况下内存地址超过 1MB 会被自动回卷,现代操作系统和应用程序基本都要求先解开 A20。OUT 0x64, AL是向键盘控制器发送命令,0xd1 表示「准备写输出端口」,随后OUT 0x60, AL把 0xdf 写进去,就把 A20 打开了。
GDT 的设置同样关键。书中用GDTR0定义了一个只有两个段描述符的表:一个代码段一个数据段,基地址都设为 0,段限长设为 4GB。这是扁平内存模型,C 语言编译器按这个模型生成代码时,任何指针访问都直接对应物理地址,不需要额外的段偏移计算。如果这里段描述符设置错误,跳转到 C 代码后要么直接死机,要么访问到的全是错位数据,而且这种错位极其隐蔽,C 代码里看逻辑完全正常,打印变量却全乱。
| 阶段 | 代码位置 | 加载地址 | 说明 |
|---|---|---|---|
| 引导扇区 | ipl.nas | 0x7c00 | BIOS 加载,负责读后续扇区 |
| 操作系统主体 | asmhead.nas / bootpack.c | 0xc200(从 0x8000 复制后) | 作者的工具链把编译结果安排在特定偏移 |
| 栈 | 汇编内设置 | 0x1ff000 附近 | 保护模式后栈空间够大,不易溢出 |
上面这个内存布局表是三天的总纲。引导扇区负责把操作系统主体从软盘读到内存,然后跳转执行;操作系统主体先执行 asmhead 完成模式切换,再由 C 入口接管。理解了这张表,后面 20 多天不管加了多少功能,万变不离其宗。
5. 避坑指南:编译、运行、改代码过程中最容易翻车的五个问题
复现这套工程的过程中,我见过的翻车点高度集中。下面五条是按出现频率从高到低排的,每一条都按「现象 → 原因 → 解决」展开,都是我实际踩过或帮别人排查过的。
5.1 黑屏或花屏
现象:make run后 QEMU 窗口弹出来,但屏幕是黑的或者花屏,没有预期的字符显示。
原因:最常见的是引导扇区超过 512 字节导致0x55 0xAA标志丢失,或 BPB 参数区被改动。另一个隐蔽原因是 ORG 地址不匹配,代码在 0x7c00 但汇编器按其他地址算标签,跳转目标全错位。
解决:先把ipl.nas恢复成工具集附带的原因文件再编译,确认能跑通再逐步改动。然后检查生成后的ipl.bin文件大小是否正好 512 字节,用十六进制编辑器看最后两个字节是否为 55 AA。如果大小不正确,排查代码里是否定义了超过 512 字节的数据区,把数据移到引导扇区之后。
5.2 make 报错 missing separator
现象:执行make时提示类似Makefile:3: *** missing separator,而后退出。
原因:Makefile 里规则行开头必须是 Tab 字符,任何文本编辑器把 Tab 替换成空格或制表符宽度设置错误,都会触发这个错误。这是 Windows 下编辑 Unix 风格文件的老问题。
解决:用支持「保留 Tab」的编辑器打开 Makefile,把规则行前的空格全部重新打一次 Tab。如果是在 MSYS2 终端里直接用 vim,确认:set noexpandtab并且:set tabstop=4。改完后用cat -A Makefile看行首是否显示为^I,这才是 Tab 的正确形态。
5.3 到了读取磁盘这一步永远失败
现象:第一天的显示正常,第二天加入了读盘循环后,启动后反复重启或卡在读取阶段,屏幕上可能重复显示错误提示。
原因:寄存器值设置不一致。最常见的是 CL 扇区号与 CH 柱面号对调,以及 ES:BX 指向的缓冲区与后续 ima 文件实际写入位置不一致。软盘的几何参数在 BPB 里定义,而 INT 0x13 读盘时用的几何参数来自 BIOS,两边不匹配就会读失败。
解决:确认 CH/CL/DH 与镜像的几何参数一致。书中软盘是 1440KB,柱面 80、磁头 2、每磁道 18 扇区。读盘循环里每读完一个磁道要重算 CHS,不能靠单一 INT 0x13 连续读多轨。把 CHS 换算成线性扇区号写一份对照表,排查边界条件会快很多。
5.4 haribote.img 写进 U 盘后真机不启动
现象:QEMU 里跑得好好的镜像,用工具写进 U 盘后插到实机上无法引导,BIOS 直接跳过。
原因:U 盘和软盘的引导方式是两回事。这套工程生成的是软盘镜像,FAT12 格式,BIOS 按软盘方式引导。而多数主板对 U 盘默认按 USB-HDD 方式引导,需要第一扇区有 MBR 布局,与软盘镜像的引导扇区结构不兼容。
解决:如果你只是想在真机上看效果,最省事的方案是用带「USB-FDD」或「USB-ZIP」引导模式的主板,并把 U 盘格式化为 FAT12。但说实话现在大多数主板对 USB-FDD 支持已经退化,不如直接用 QEMU 验证逻辑,把精力放在改代码上。真要在实机跑,需要额外加一个加载链或改造成 USB-HDD 布局,这是另一个话题了。
5.5 升级了编译器版本后莫名的编译失败
现象:代码一行没改,把 nasm 从旧版本升级到新版后,make报错或生成的镜像启动异常。
原因:nasm 版本间对某些伪指令和默认格式的处理有差异,尤其是ORG、[BITS 16]的默认值以及RESB是否在段内生效这类行为。工具集自带的环境里工具的版本和书出版时一致,你本机装的太新反而会偏移。
解决:不要在系统全局装最新版 nasm 并放进 PATH。要么沿用工具集里自带的 nasm,要么在工程目录下手动指定一个与书接近的版本。我一般会在 Makefile 里写死NASM = /path/to/nasm而不是依赖 PATH 里的全局命令,这样换机器也能稳定复现。
6. 进阶技巧:用自动化链路 + 字节级校验把三十天工程的复现成本降下来
三十天工程最大的成本不是编译,而是「改一行代码 → 不知道改坏没坏 → 重新跑一遍启动 → 肉眼盯着屏幕看正不正确」。等做到十几天以后,每章的功能越来越多,输出一多肉眼根本看不出来哪里错了,所以我最后再加一个技巧:给每个工程加一条自动校验链。
先写一个辅助脚本,把「编译 → 生成镜像 → 校验结构 → 启动」合成一步:
#!/bin/bash # build_check.sh set -e make clean make check_img.py haribote.img # 字节校验 make run逻辑说明:set -e保证前面任何一步失败都直接退出,make clean避免旧产物干扰,check_img.py在启动前先做字节级校验,有问题就不启动。这三行看着简单,实际上帮你把「编译-验证-运行」的反馈闭环缩短到 10 秒以内。
再看字节校验器。对于第三十天那种功能完备的镜像,一段精简的 Python 脚本足够识别大部分被改坏的情况:
# check_img.py import sys def check_boot(img): data = open(img, "rb").read() # 1. 引导标志 if data[510] != 0x55 or data[511] != 0xAA: print("引导标志缺失,启动区可能越界") return 1 # 2. FAT12 卷标 if data[3:11] != b"HELLOIPL" and data[3:11] != b"HARIBOTE": print("卷标异常,BPB 区可能被破坏") return 1 # 3. 根目录区存在系统文件条目 root_dir = 19 * 512 # FAT12 结构下根目录从第 19 扇区开始 found = any(data[root_dir+i*32: root_dir+i*32+8] == b"OS " for i in range(224)) if not found: print("根目录找不到 OS 文件条目") return 1 print("校验通过") return 0 if __name__ == "__main__": sys.exit(check_boot(sys.argv[1]))参数说明:第一项校验引导标志,确认启动区没越界;第二项校验 BPB 卷标,确认 BPB 区没有被后面的代码覆盖;第三项检查根目录区里是否存在操作系统主体文件条目,根目录在 FAT12 结构下通常位于第 19 扇区偏移,扇区大小 512 字节,所以偏移是19 * 512。这三个检查和 QEMU 里肉眼看的结论基本一致,但快得多。
从那以后我每次改完ipl.nas或bootpack.c,都强制走一遍build_check.sh,确认校验通过才进 QEMU。这个习惯让我在后半段的调试里省掉了大量重复启动的等待时间——哪一步出问题脚本会直接说,不用再盯着屏幕猜了。希望帮到你。
本文还有配套的精品资源,点击获取