简介:一套与《30天自制操作系统》同步的完整源文件包,面向操作系统学习者、计算机专业学生及对底层原理感兴趣的开发者,也可作为课程设计、自学项目或面试准备的重要参考资料。内容涵盖启动加载器、内核开发、进程管理、内存管理、文件系统与设备驱动等关键模块,包括任务调度、中断处理等细节,读者可配合书籍从零搭建可运行的小型操作系统,理解汇编与C语言如何协同驱动硬件。资源包共2000个文件,以C源码(1711个c文件)为主体,辅以149个h头文件与140个txt说明文档,整体压缩后约20.93MB,目录结构与书籍章节一一对应,便于快速检索模块。各阶段代码按学习路径组织,便于对照章节逐日推进,txt文档提供必要注释与思路梳理。目前已有679人学习下载,适合希望深入操作系统底层、提升系统编程能力的读者作为实践素材。 很多学操作系统的人都会有这样一个困惑:教材翻了好几遍,进程调度、虚拟内存、死锁这些概念背得滚瓜烂熟,但真让你写一个能跑的操作系统,完全不知道第一行代码该落在哪里。《30天自制操作系统》这本书的价值就在于把这件事倒了过来——不跟你讲大道理,直接扔给你一套能跑起来的操作系统源文件,让你从第一天开始就看到屏幕上出现自己写的像素点,然后一天一点,把它喂大成一个有鼠标、有窗口、能响应键盘输入的图形界面系统。
这篇文章不是这本书的读书笔记,而是我完整把书里配套源文件跑通、读透、改造之后的一份实战记录。我会把环境怎么搭、源文件怎么组织、哪些代码是整个系统的骨架、哪些地方最容易踩坑、以及排查问题时的完整思路都摊开来讲。无论你是刚学完C语言和汇编、想看看操作系统内部长什么样的学生,还是工作几年后想补底层这块短板的开发者,这篇内容都能给你一条可以直接走的路。
1. 这本书和它的源文件到底厉害在哪
先说结论:这本书的教学思路几乎和所有操作系统课程相反。大学课堂通常先讲进程、线程、锁,再讲内存管理、文件系统,最后才在幻灯片里放几张Linux源代码的截图,学生听完依然不知道操作系统是怎么“启动”的。而这本书从一块虚拟软盘的引导扇区开始,256字节的汇编代码,一点一点把系统喂起来,每章结束时都能看到可见的进展,这种正反馈是“读书”完全给不了的。
另一个关键点是它的源文件结构非常干净。全书几十个章节,每一个阶段对应一套独立可编译的代码目录,从第1天的helloos到最后几天的带窗口GUI系统,每一版都保留了完整的构建脚本和镜像文件。这意味着你不需要一开始就理解全部代码,只需要照着当天的源文件动手编译、运行、观察现象,然后再反过来看代码为什么要这么写。这个路径比直接丢给你一个几万行的Linux内核源码要友好太多。
有人可能会说,Linux 0.11、xv6这些也是很好的学习材料。我不否认,但它们的定位完全不同:xv6是给你一个完整但精简的内核去读,重点在“分析”;而这本30天的书重点在“构建”。前者是解剖课,后者是手工课。对于大多数人来说,先手工做出一个能响应的系统,再回去读xv6,那种“原来这个机制是干这个用的”的顿悟感会强烈得多。
2. 把源文件跑起来:环境准备与构建链路
这本书原版发布时还是软盘时代,现在真实软驱早就绝迹了,但好在QEMU模拟器可以完美模拟当时的硬件环境。我在Ubuntu 22.04上完整跑通了所有章节,整个过程没遇到什么无法逾越的障碍。
2.1 需要准备的工具
- NASM:这本书的汇编源文件统一使用NASM语法,所以必须用NASM编译,换成GAS语法需要大量改写,完全没必要。
- gcc(32位支持):书里从第5天左右开始引入C语言,编译时需要生成32位代码,所以要确保gcc支持
-m32参数。 - make:书里的源文件自带Makefile,用make管理构建是最省事的。
- QEMU:用
qemu-system-i386模拟整机。
在一台干净的Ubuntu上,执行下面这一条命令就能把环境全部装好:
sudo apt install -y nasm gcc gcc-multilib make qemu-system-x86gcc-multilib这个包特别容易漏装。如果没装,编译C文件时会报一堆找不到stdio.h之类的头文件错误,那可跟你的代码没有半毛钱关系。
2.2 一天的典型构建流程
以第1天helloos为例,源码目录里通常包含一个helloos.nas汇编文件和一个Makefile。构建的核心逻辑是:用NASM把汇编源码编译成原始二进制文件,然后把它塞进一个虚拟软盘镜像的前512字节,剩下的空间填0。
我稍微简化了一版Makefile,但核心逻辑不变:
# Makefile TOOLPATH = ../tolset/z_tools MAKE = make NASM = nasm QEMU = qemu-system-i386 helloos.img : helloos.nas Makefile $(NASM) helloos.nas -o helloos.img -l helloos.lst run : $(QEMU) -fda helloos.img这里最关键的是NASM的输出必须是bin格式,也就是纯二进制,而不是Linux默认的elf格式。如果你的汇编代码里没有显式写[BITS 16]和[ORG 0x7c00]这两个指令,跑出来的镜像大概率是黑屏,这点我在后面调试部分会说。
对了,运行QEMU时要留意:如果是在没有图形界面的服务器环境下,QEMU会报无法打开显示的错误。这时候可以加一个-nographic选项,或者用-display curses在终端里直接显示字符界面。虽然体验打折,但验证前几章的字符输出完全够用。
2.3 环境问题清单
我在跑这套环境时遇到过几个典型问题,列出来供大家对照:
- 现象一:QEMU窗口一闪而过,什么输出都没有。原因通常是镜像文件没生成,或者Makefile里
run目标的依赖不对,先手动执行一次编译命令,确认helloos.img确实存在。 - 现象二:编译时报错
non-constant expression in 'org'。NASM的ORG指令必须接常量,如果你写成了ORG 0x7c00 + 某变量,就会报这个错。书里的源码不会出现这种问题,你自己改代码时要小心。 - 现象三:Windows下用cmd执行make出现编码乱码。书自带的工具链是在Windows命令行下使用的,如果你跟我一样在Windows环境下载了源码,建议直接用WSL跑,一路顺畅。
3. 源码核心机制拆解:从引导扇区到中断处理
这本书的源码如果按机制分层,其实就四大块:引导与实模式输出、C语言与汇编的混合编程、保护模式与内存管理、中断与设备驱动。把这四块吃透,整个系统的骨架就清晰了。
3.1 引导扇区:一切开始的0x7c00
电脑通电后,BIOS会做自检,然后把启动设备(这里就是虚拟软盘)的第一个扇区加载到内存的0x7c00地址,并检查扇区最后两个字节是否为0x55 0xaa,是则跳转执行。这就是整个操作系统的起点。
书里第1天的代码核心大概长这样:
; 这是伪代码,仅保留最核心逻辑 ORG 0x7c00 entry: MOV AX, 0x0000 MOV SS, AX MOV SP, 0x7c00 MOV DS, AX MOV ES, AX ; 清屏 MOV AX, 0x0003 INT 0x10 ; 显示字符串 MOV SI, msg putloop: MOV AL, [SI] ADD SI, 1 CMP AL, 0 JE fin MOV AH, 0x0e INT 0x10 JMP putloop fin: HLT JMP fin msg: DB "Hello, OS!" DB 0 TIMES 510 - ($ - $$) DB 0 DW 0xaa55ORG 0x7c00告诉汇编器,这个程序运行时的基址在0x7c00,这样所有标签和内存访问的地址计算才能正确。TIMES 510 - ($ - $$) DB 0是填充指令,让整个引导扇区刚好512字节,最后的DW 0xaa55是启动标志。
这段代码不建议直接抄,因为不同版本的书细节略有出入,但理解了这个流程,你就知道了“操作系统启动”在最底层就是这么朴素的一件事。
3.2 C语言的“非法入境”
从第5天开始,书上开始引入C语言。这中间有个非常关键的机制转换:怎么让C语言的main函数能被汇编调用?答案是:把C文件编译成一个二进制的目标文件,然后在汇编代码里用call指令调用它。
但这里有个隐蔽的坑:C语言编译器生成的代码默认会假设栈已经设置好,还会生成对_main、_printf等符号的引用。所以汇编端必须做几件事:
- 设置栈指针
SP,一般指向一块专门分配的内存区域,否则C函数一调用就栈溢出,直接死机。 - 用
extern _main声明C函数入口,然后在汇编里call _main。 - 链接时把C编译出的目标文件和汇编目标文件合并,再用
objcopy等工具提取纯二进制。
这本书的后期提供了一套巧妙的方案:用GCC的-fno-pic、-c等参数生成32位目标文件,再用工具把__main、_printf这些符号替换成自定义的实现,或者干脆在Makefile里通过链接脚本把段排布得干净利落。
我自己第一次引入C语言时最常踩的坑是:C代码里用了printf,但自制OS里根本没有printf这个库函数,链接时报undefined reference。解决办法很简单——书中的OS有自己的字符输出函数,需要把C代码里的printf替换成sprintf或者直接调用自制的putfont一类的输出函数。也就是说,一旦离开BIOS,你自己就要当那个“标准库”。
3.3 保护模式:为什么突然指针都不好使了
大约到第6、7天,书里开始进入32位保护模式。这个阶段最大的感受是:以前实模式下随便访问的内存地址,进入保护模式后全都变了含义。原因在于CPU的寻址方式从“段基址+偏移量”变成了“段选择子查表得到段基址+偏移量”。
这里引入了一个核心概念:GDT(全局描述符表)。可以把它类比成一个“酒店房间登记表”,每个段描述符里记录了该内存段的基地址、长度上限、访问权限。CPU拿到一个段选择子后,会去GDT里查这张表,拿到真正的段基址,再和偏移量相加得到物理地址。
书里的源码在进入保护模式前会定义几个段描述符,代码大概长这样(只是结构示意):
typedef struct { unsigned short limit_low; unsigned short base_low; unsigned char base_mid; unsigned char type; unsigned char limit_high; unsigned char base_high; } SEGMENT_DESCRIPTOR; void set_segmdesc(SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { if (limit > 0xfffff) { ar |= 0x8000; /* G_bit = 1 */ limit /= 0x1000; } sd->limit_low = limit & 0xffff; sd->base_low = base & 0xffff; sd->base_mid = (base >> 16) & 0xff; sd->type = ar & 0xff; sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); sd->base_high = (base >> 24) & 0xff; return; }这个结构体就是GDT表项的真实模样。很多人在这一步容易困惑:为什么要搞得这么复杂?直接给线性地址不就行了吗?答案是为了硬件层面的权限控制和内存保护。可惜的是早期书里对这块着墨不多,很多人照着敲完代码,却不知道这几行结构体定义其实撑起了整个内存管理体系的根基。
你只需要记住一件事:进入保护模式后,你的代码里所有段寄存器(DS、ES、SS等)都必须指向一个合法有效的段选择子,否则CPU会抛出保护异常,系统直接重启。所以每次切换段时,记得重新加载一遍段寄存器。
3.4 中断处理:让键盘动起来的那张IDT
操作系统要响应键盘、鼠标、定时器,靠的是中断机制。CPU收到中断信号后,会暂停当前工作,跳转去执行中断处理程序,执行完再返回。而这张“中断向量表”在保护模式下叫IDT(中断描述符表)。
书里的键盘驱动部分,核心是注册IRQ1(键盘中断)的处理函数,逻辑大致是这样:
- 初始化8259A中断控制器,把IRQ映射到IDT表中合适的位置。
- 在IDT里设置键盘中断对应的门描述符,指向用汇编写的中断入口。
- 中断入口里先压入寄存器现场,调用C写的
inthandler21函数,处理完再恢复现场,执行iretd返回。
这里最容易被忽略的细节是:中断处理函数执行完必须向8259A发送EOI(结束中断通知)指令,否则后续中断都会被屏蔽,表现为键盘只响应第一次按键。书里的源码会写类似outp(PIC0_OCW2, 0x61)这样的代码,那一条指令就是给硬件“解锁”的关键。
我调试键盘响应时,就曾卡在“按一下能显示,按第二下失灵”的诡异现象上,查了半天才发现是中断控制器没被正确通知。这种硬件层面的时序问题,教材里基本不会提,但自己写OS时早晚会遇到。
4. 黑屏到亮屏的完整排查链路:一次经典事故复盘
那段时间我每天都在改代码。某天早上,我把内存管理的部分加了几行页表初始化代码,重新make、运行QEMU,结果屏幕一片黑,连引导字符都没出来。这是自制OS调试中最常见也最让人头疼的场景——系统没跑起来,你连个报错都看不到。
下面是我当时的完整排查过程,也算是一个可复用的思路框架。
第一步,确认镜像本身是不是有问题。我先检查了make命令的输出日志,确认NASM和GCC都成功执行、镜像文件时间戳是刚才的。然后单独执行qemu-system-i386 -fda helloos.img -d guest_errors,QEMU会打印出一些内部错误信息。这一步帮我确认了问题不是出在“镜像没生成”,而是代码逻辑上。
第二步,把最近改动回退。我用git做版本管理,直接把源码git stash回退到前一天能跑的状态,编译运行,发现系统恢复正常。这就锁定了问题范围,肯定是新加的页表初始化代码引起的。
第三步,缩小范围。我把新增代码用条件编译包裹起来,先注释掉页目录赋值那段,只保留GDT相关的初始化,运行后能看到字符了,说明问题出在页目录部分。
第四步,用反汇编和寄存器检查深入定位。用objdump -D反汇编编译出来的二进制文件,对比内存地址范围,发现我的页目录指针(CR3)被赋成了一个不存在的物理地址——因为我当时图省事,直接拿了一个尚未初始化的变量去当页目录物理地址。CPU一查页表就崩,自然连字符都输出不了。
第五步,修复后验证。我把页目录指向一块已清零的静态内存区域,重新编译运行,字符正常显示,系统恢复。
这整个链路里最有价值的不是最后那个修复,而是第三步“二分定位”的思路。自制OS不像普通应用,没有core dump可看,没有堆栈追踪可用,出问题只能通过缩小范围+反复实验来定位。而QEMU的-d参数、反汇编工具、以及一些最基本的调试打印(在屏幕某个固定位置输出一个字符)就是你的全部武器。所以很多人问做这个项目到底能学到什么,我觉得最大的收获就是这种“在极端受限环境下排查问题”的能力。
5. 把30天的代码继续往前推:值得做的几个扩展方向
把书里最后一章跑通,看着自己写的OS在QEMU里弹出窗口、移动鼠标时,快感确实强烈。但冷静下来看,这个系统离“能用”还差得远。它没有用户态和内核态的隔离,没有文件系统,没有网络。不过正因为骨架清晰,它特别适合做扩展实验。
我个人的建议是,按下面的顺序往里面加东西:
- 加一个用户态模式。这本书的系统从始至终都跑在内核态,你可以尝试用TSS实现用户态/内核态切换,把某个应用放到用户态运行。这样能真正理解系统调用和特权级的意义。
- 加一个简单的FAT16文件系统读写。书后面会涉及简单的软盘读写,但没有完整的文件系统层。你可以写扇区级的读取函数,实现ls、cat命令,这比直接用标准库要透彻得多。
- 对照xv6源码,把书里的某个机制升级成更通用的版本。比如书里的内存管理是定长分页,你可以改成空闲链表分配,或者实现一个简单的buddy system。
- 加上定时器多任务。书里有简易的多任务切换,你可以把调度算法改成时间片轮转,再给每个任务加上独立的栈,这样对进程上下文切换的理解会更立体。
顺带说一句,做过这套OS之后,再回去看那些热搜词里常见的“操作系统期末复习”“计算机操作系统慕课版”这类内容,会有一种“原来如此”的通透感。教材上那些抽象概念,终于在你脑子里有了具体的代码形象。
6. 一些个人体会和最后的建议
最后说点实在的。三十天的周期,对在校学生来说可能刚刚好,但如果是一边上班一边学,大概率要拖到两个月。我就是在这样的节奏中断断续续完成的,而且中间不止一次想放弃——尤其是键盘中断失灵那几天,整个人都在怀疑人生。
但熬过去之后,我对操作系统和计算机底层的理解确实脱胎换骨了。以前写应用代码,崩溃了就下意识怀疑是不是编译器问题;做完这个项目之后,反而会对程序运行的硬件上下文多一个心眼。调试C语言段错误时,我也会下意识地先去想栈指针是不是被踩了、内存越界是不是发生在某个数组边界上,这种敏感度是真金白银的“底层直觉”。
如果你正准备开始,我的建议是:别追求完美复刻,也别想着把每一行代码都注释得明明白白才开始动手。先把书里的源文件敲到能跑,跑通了,再回头问自己“为什么这一步要这样写”,带着问题去翻书,效率远高于先啃完原理再动手。
当你自己的系统在虚拟机上亮起来、键盘敲出的字符出现在屏幕上那一瞬间,你会觉得前面所有熬夜都值了。
本文还有配套的精品资源,点击获取