从引导扇区到中断处理:自制操作系统的实战构建与调试指南
发布时间:2026/9/9 4:07:10 作者:尧图编辑部 阅读量:1,286

简介一套与《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语法需要大量改写完全没必要。gcc32位支持书里从第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_errorsQEMU会打印出一些内部错误信息。这一步帮我确认了问题不是出在“镜像没生成”而是代码逻辑上。第二步把最近改动回退。我用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语言段错误时我也会下意识地先去想栈指针是不是被踩了、内存越界是不是发生在某个数组边界上这种敏感度是真金白银的“底层直觉”。如果你正准备开始我的建议是别追求完美复刻也别想着把每一行代码都注释得明明白白才开始动手。先把书里的源文件敲到能跑跑通了再回头问自己“为什么这一步要这样写”带着问题去翻书效率远高于先啃完原理再动手。当你自己的系统在虚拟机上亮起来、键盘敲出的字符出现在屏幕上那一瞬间你会觉得前面所有熬夜都值了。本文还有配套的精品资源点击获取