简介:面向操作系统底层学习者的DIY实战资料包,围绕于渊《自己动手写操作系统》展开,适合具备基础编程能力、希望理解系统启动、内存管理、文件系统与进程调度等核心机制的开发者,也适合作为高校操作系统课程的课外拓展材料。压缩包内共2000个文件,以c和h源码文件为主体,辅以asm汇编代码、inc包含文件、img磁盘映像、makefile构建脚本与bochsrc虚拟机配置,整体约121.84MB,结构清晰便于对照书中章节逐模块研读。完整源码覆盖操作系统内核编写、系统调用实现、中断处理程序设计等关键环节,同时附有shell脚本、pdf/doc文档及少量html学习材料,能够支撑从引导装载到内核扩展的完整动手路径。目前已有197人学习下载,适合通过编译、运行和修改示例来加深理解,并可进一步探索多核处理、虚拟化技术、安全机制等现代操作系统主题,在实验与调试中逐步建立系统级开发能力。
1. 从一本书和一包代码聊聊自制操作系统这件事
提到“操作系统”,多数人的第一反应是 Windows、Linux、macOS 这些庞然大物,觉得那是几千人团队花几十年才能做出来的东西。但我在操作系统开发这条路上摸爬滚打这些年,最大的体会是:操作系统的核心机制——引导、内存管理、中断、进程调度——每一个单点拆开看,都没有想象中那么神秘。关键是你要有一份能跑起来的参考代码,再配合一条清晰的讲解路径,才能真正把那些抽象概念落到实处的掌握。
于渊的《自己动手写操作系统》正好补上了这块短板。这本书最大的价值不是把原理念给你听,而是带着你从一个裸机引导扇区开始,一步步写出一套能在真机或虚拟机上运行的操作系统雏形。压缩包里带的那份完整代码,更是可以直接编译、调试、运行,省掉了大量“对着书上代码手敲却敲错”的时间浪费。文章后面我会按照我自己学习时的路径,把这套代码适合什么人、怎么跑起来、每个核心模块背后到底在做什么、以及我踩过的那些坑,一次说清楚。如果你正打算入坑操作系统开发,这篇文章应该能帮你少走不少弯路。
2. 这个项目到底在做什么
2.1 核心路线:从实模式一步一步走向保护模式
x86 架构的 CPU 上电后,会先进入一种叫“实模式”的状态。这个模式下,CPU 只有 20 位地址线可用,最大只能访问 1MB 内存,而且没有硬件级别的内存保护,任何程序都能随意改写内存里的任何位置——包括操作系统自身的代码。这显然不是现代操作系统该有的运行环境。
于渊这本书一步步带你做的事情,本质上就是完成两条路线:一是从实模式切换到保护模式,打开 32 位寻址和内存分页机制;二是在保护模式就绪之后,搭建起中断、任务调度、系统调用这些真正属于“操作系统”的骨架逻辑。代码中的每个里程碑阶段,都对应着这套从底层到高层的演进过程。你跟着跑的时候会发现,不是先学一堆理论再“设计”一个系统,而是每补一个模块,就能立刻看到系统多了一个真实能力。
2.2 代码包的真实定位:学习样例而非生产系统
很多初学者拿到代码包后,第一反应是“这能跑什么程序、能不能装软件”。坦率说,这套代码并不是为了替代 Linux 或 Windows,它的核心定位是学习样例。你在里面能看到 A 轮 Bochs 下运行的图形界面、键盘输入处理、时钟中断、优先级调度,但它刻意保持精简,让你在几千行代码内就能理解操作系统最核心的执行流。
定位搞清楚之后,学习方式就完全不一样了。不要试图往里面加一堆花哨功能,而是每读一段代码,就问自己“这里如果不这样做,系统会不会崩、为什么”。这个追问的过程,比单纯跑通代码重要得多。我的建议是:第一遍按照书里的章节顺序读代码,理解整体脉络;第二遍直接改代码,尝试去掉某个机制看系统的反应。
2.3 哪个阶段的读者最适合上手
我自己带过一些初学者,发现这套内容最适合的群体有两类。第一类是有过 C 语言和汇编基础、但没接触过底层开发的在校学生,想通过一个完整项目把“操作系统原理”这门课的知识真正理解。第二类是工作中想做驱动开发、嵌入式开发或者虚拟化方向的工程师,需要快速补充对系统启动流程和内核机制的直观认知。
如果你的 C 语言还停留在“能写链表、能做排序”的阶段,建议先把基础语法过一遍再上手。汇编部分不需要精通,只要看得懂寄存器、内存寻址、跳转指令就行。于渊在书里对汇编指令的实际使用场景解释得很详细,并不会让你因为汇编而卡壳。
3. 环境搭建:让代码在你机器上跑起来
3.1 编译工具的选型与版本搭配
这份代码是基于 NASM 汇编器和 GCC 编译器构建的。NASM 是 x86 汇编的事实标准之一,语法简洁,跨平台支持好;GCC 负责把 C 语言部分编译成目标文件,再用 GNU ld 链接成最终的二进制镜像。
我在实际搭建时,踩过最大的坑就是“版本漂移”。现代 Linux 发行版自带的 GCC 已经是 12.x 甚至 13.x,老版本代码在编译时容易因为链接脚本语法、内嵌汇编格式差异而出错。网上很多人问“为什么编译报错”,八成都是这个原因。我的建议是:不要用太新的工具链跑老代码,优先选择 Debian 11、Ubuntu 20.04 这类自带 GCC 9/10 的环境,或者直接用 Docker 拉一个带旧工具链的镜像,专门用于这套代码的编译。版本稳定了,后面的问题能少一半。
3.2 虚拟机调试环境的搭建:QEMU 是首选
运行这套系统的虚拟机我推荐 QEMU,而不是 Bochs。原因很直接:QEMU 调试功能更强,比如-monitor可以实时查看寄存器、内存,-d int可以记录每次中断触发;而且 QEMU 启动速度更快,对多核支持更好,和真机行为也更接近。
启动命令大致是这样:
qemu-system-i386 -fda boot.img -boot a这里-fda表示把 boot.img 当作软盘镜像,-boot a指定从软盘启动。如果你按照书里的 makefile 步骤执行,代码包里的Makefile会自动生成 boot.img 并调用 QEMU,不需要手动敲这个命令。
需要注意的是,代码包本身可能年代久远,有些示例依赖了老版 QEMU 的默认行为。如果发现运行结果跟书上描述不一致,先检查 QEMU 版本,不要怀疑是自己的代码写错了。我用的 QEMU 版本是 6.2.0,整体兼容性很稳定。
3.3 Windows 和 macOS 下的环境差异
这套代码的开发背景是 Linux,但并不意味着 Windows 或 macOS 下就完全跑不了。Windows 上可以用 WSL2 挂一个 Ubuntu 环境,在 WSL 里安装 NASM、GCC、QEMU 之后,编译和运行体验和原生 Linux 几乎没有差别。macOS 上则要注意,QEMU 的安装需要经过 Homebrew,并且由于 macOS 对二进制文件的执行限制,首次启动 QEMU 时可能需要在“系统设置-隐私与安全性”中允许来自未知开发者的程序运行。
如果你习惯在 Windows 下直接跑,也可以试试 MSYS2 环境,但我不太推荐,因为 Makefile 里的路径分隔符和 shell 语法在 Windows 原生环境下经常出问题。换个角度说,把开发环境放到虚拟机上本身也是一种隔离,系统崩了不会影响宿主机,这对学习操作系统来说反而是件好事。
4. 核心机制拆解:代码里那些值得反复咀嚼的部分
4.1 引导扇区:从 BIOS 手里接管控制权
当虚拟机加电启动时,BIOS 会完成硬件自检,然后把启动介质第一个扇区加载到内存地址0x7C00,并跳转过去执行。这个扇区只有 512 字节,最后两个字节必须是0x55AA,否则 BIOS 不认为它是合法引导扇区。
代码里引导扇区部分做的事其实很简单:设置段寄存器、调整栈指针、把后续代码从磁盘读入内存、然后跳转执行。但就是这个只有几百字节的程序,让我第一次真正理解了“程序是如何跑起来的”——不是在某个操作系统里双击一个可执行文件,而是 CPU 从头开始执行你写的第一条指令,中间没有任何东西替你接盘。
学到这里,建议你用xxd或者hexdump看一下编译后的 boot.bin 文件,确认最后两字节确实是55 AA。这个小小的验证,能帮你把整个编译链接流程串起来理解。
4.2 进入保护模式:内存寻址的质变
实模式下的段地址:偏移地址寻址方式,最大只能访问 1MB 空间。进入保护模式后,内存寻址变成了“段选择子 + 全局描述符表(GDT)”的机制。保护模式下每个段的基地址、长度和访问权限都由 GDT 中的描述符决定,CPU 每次访问内存都会检查是否越界、是否有访问权限,这种硬件级的内存保护是现代多进程系统的基础。
代码里设置 GDT 的过程本身就是一次对内存结构清晰度的考验。你需要把各个描述符的字节按规则拼出来,其中任何一位错位,CPU 在切换到保护模式后轻则死机,重则触发三重故障直接复位。我第一次调试时,因为把段界限的高位写错,QEMU 直接黑屏重启,后来靠info registers一条条对比 GDT 内容才发现问题。
4.3 分页机制:让每个进程拥有独立地址空间
分页是保护模式之后的下一步。它的核心思路是:CPU 不直接使用程序给出的线性地址访问物理内存,而是通过页目录和页表做一层映射,把逻辑地址转换成物理地址。有了分页,每个进程都可以认为自己拥有从 0 开始的完整地址空间,操作系统只需要在切换进程时更换页目录基地址寄存器(CR3),就能实现进程间地址隔离。
代码中的分页初始化逻辑相对精简,只建立了几个页目录项和页表项。但当你理解到“CR3 这一条指令就完成了整个地址空间的切换”时,再回头看 Linux 的 fork/exec 就会轻松很多。这也是我认为这本书代码最值得深读的地方之一——它用最小代码量讲清楚了地址空间隔离的本质。
4.4 中断机制:操作系统处理异步事件的入口
中断是操作系统“活着”的关键。键盘按键、时钟 tick、硬盘读写完成都是通过中断通知 CPU 的。代码里通过设置 IDT(中断描述符表)把每个中断向量绑定到对应的处理函数。时钟中断每 10 毫秒触发一次,在处理函数里执行任务调度;键盘中断负责把扫描码写入缓冲区。初看这些代码似乎只是几个函数,背后的机制却很值得体会:一个操作系统的所有实时响应能力,都依赖中断,没有中断的系统只是一个死循环而已。
调试中断时,我强烈建议你打开 QEMU 的-d int,cpu_reset输出,会让你看到每次中断发生时的寄存器现场和 CPU 状态。看多了之后,你对“中断是如何打断正在执行的代码并跳转到处理程序的”这个问题的理解会远超教科书。
4.5 进程调度:多任务只是定时的现场切换
进程调度在这套代码里做得很直白。每个进程有自己独立的内核栈,保存着上下文(寄存器、栈指针、程序计数器)。时钟中断触发时,调度函数检查当前进程的时间片是否用尽,如果耗尽就把当前寄存器压入当前进程的内核栈,再从下一个进程的内核栈恢复寄存器,然后通过一条iret指令跳回用户态继续执行。
我当时看完这段代码后,最大的震撼是“原来进程切换并不需要什么魔法”。它本质上就是保存场景、恢复场景。Linux 那套复杂的 CFS、红黑树,都是在这个基本的切换框架上不断优化公平性和效率,但核心语义没有变化。
5. 运行调试中的疑难杂症与排查方法
5.1 编译链接阶段最常见的报错模式
代码包里的 Makefile 对依赖关系做了处理,但你在实际操作中仍可能遇到几个高频问题。首先是 “binfmt” 报错,通常是因为链接生成的中间格式不是纯二进制,需要在 ld 命令中加上--oformat binary选项。其次是 “relocation truncated to fit” 报错,这往往意味着某个符号的地址超出了当前段可表示的范围,常见解决方案是调整链接脚本中的内存布局。
如果遇到这些报错,不要急着去网上复制大段命令,先冷静下来看 Makefile 里到底执行了什么。掌握了 Makefile 的每个步骤,你才能明白报错出现的原因是格式、地址还是符号解析问题。
5.2 运行时卡死或无输出的排查思路
系统启动后如果 QEMU 黑屏无输出,优先检查是不是引导扇区没有被正确写入镜像。可以用dd if=boot.bin of=bochs.img bs=512 count=1 conv=notrunc手动写入并确认。其次,检查 GDT 和 IDT 的加载指令lgdt、lidt是否在进入保护模式后被执行,如果跳过了这两条指令,系统会在访问内存或触发中断时直接死机。
另一个很隐蔽的问题是由于代码段里启用了 A20 地址线,但 QEMU 默认可能已经开启或关闭。如果你发现在访问 1MB 以上内存时地址错乱,可以在代码初始化的早期把 A20 线强制打开,用in al, 0x92和or al, 0x02的方式写回端口。
5.3 调试输出:给开发者的“眼睛”
这套代码在早期阶段还没有完整的显存驱动,所以很多调试信息不能像普通程序那样用 printf 输出。我自己摸索出来的最可靠方案是使用串口输出:把调试信息通过outb指令发送到串口端口,然后在 QEMU 启动时用-serial file:serial.log把串口内容重定向到文件中。这样每一步执行了什么、寄存器值是多少,都可以在日志里看到。
在代码里加一个简单的串口输出函数并不复杂:
void serial_putc(char c) { outb(0x3F8, c); }关键是com1端口(0x3F8)需要先完成初始化(设置波特率、帧格式),然后才能发送数据。我通常会在每个阶段的入口都加一行serial_puts("stage 1 done\n"),这样一旦系统崩了,能快速定位崩在哪个阶段,比盲目看反汇编高效得多。
6. 我把这代码包拆开之后带来的三个后续扩展方向
当主体代码已经能稳定运行之后,很多人会问“然后呢”。这里我整理了三个我自己觉得收益很大的方向,你可以按兴趣选择。第一个方向是给系统加上简易的文件系统,比如在一个虚拟软盘中实现 FAT12 格式的读写,这样就能让系统真正“加载”一个外部程序,而不是只跑内核自带的代码。第二个方向是引入用户态和内核态的切分,目前这套代码的大多数逻辑还跑在统一的特权级下,一旦你开始划分特权级,就要考虑系统调用表、用户栈切换等,这个过程能让你更深刻理解 Linux 的 Syscall 机制。第三个方向是网络协议栈,比如在系统上实现一个最简单的 ARP 和 ICMP,让 QEMU 的虚拟网卡能和宿主机通信。这一步虽然难度陡增,但那种从物理层一路走到 IP 层的掌控感,是任何现成教程都替代不了的。
这三个方向背后都有一个共同点:你必须先彻底吃透现有代码的数据结构和控制流,才知道在哪里“下刀”。于我而言,比快速写出新功能更重要的,是养成了“每次改动前先在脑海里画出数据流”的习惯。
用这个思路去做操作系统开发,你不会只成为一个“代码搬运工”,而是在底层逻辑上获得了真正的解释力。如果这套代码里的某个模块让你卡了一个礼拜也别灰心,我当年在分页那一节连续调试了三个晚上,每天晚上都带着“这次肯定能通”的信念睡去,最终搞通的那一刻,一整年的成就感都补回来了。
本文还有配套的精品资源,点击获取