拓冰建站拓冰建站
首页 / 资讯中心 / 正文

自己动手写操作系统:从引导扇区到中断分页的实践指南

简介压缩包内是一套完整的《自己动手写操作系统》教程源代码与配套资料由于渊编写面向希望从底层理解操作系统原理的开发者。资源覆盖启动、内存管理、文件系统、进程调度等核心模块并提供可直接编译调试的完整工程。整个压缩包共2000个文件以C语言源码、头文件和汇编文件为主辅以启动镜像、Bochs虚拟机配置、Makefile构建脚本等整体约121.84MB已有197人学习下载。通过阅读源码并配合书中讲解可以亲手搭建并修改一个可运行的系统雏形代码中可看到中断处理、系统调用的具体实现还能进一步探索多核、虚拟化、安全机制等扩展主题。该书强调动手实践适合系统学习操作系统开发的初学者和进阶者也是计算机专业学生理解底层机制的实用参考资料。 读代码和写代码完全是两回事这个道理我在折腾 Linux 内核源码的时候体会特别深。源码摆在那里你觉得自己看懂了某个函数、某条链表可关掉屏幕一回想脑子里只剩下一堆碎片。直到我跟着于渊的《自己动手写操作系统》把整个流程亲手走了一遍那些碎片才真正串成了一条完整的因果链。这本书加上随书代码本质上不是让你学会写一个操作系统而是让你亲手重建一台计算机从通电到运行用户程序的完整逻辑链。它能解决的最核心问题就是帮你建立操作系统到底是怎么跑起来的这个心智模型。如果你是一个想理解底层原理的应用开发者或者一个觉得操作系统课讲得太抽象、听完还是不会用的大学生又或者单纯想挑战一下自己的程序员这个项目都值得你投入两到三周时间。1. 为什么我建议每个写代码的人都亲手做一遍操作系统先说一个我在学习过程中反复验证过的结论操作系统这门课靠看是学不会的。学校里的操作系统原理课会把大量的时间花在信号量、死锁、页面置换算法这些抽象概念上但很少有人告诉你屏幕上那个光标到底是怎么亮起来的键盘敲下去的字母经历了什么才变成终端里的一行字符。你可能会觉得这些是底层细节跟你写业务代码没关系但实际上你写的每一个程序都在依赖这些机制运行只是平时被操作系统替你藏着掖着了。于渊这本书的核心思路是从按下电源键的瞬间开始一步一步地带你构造一个能跑、能显示字符、能响应时钟中断的小系统。它不是教你写一个 Linux 那样的庞然大物而是带你走一遍从0到能显示字符、能响应键盘输入的全过程。这个过程里你会亲手写引导扇区、切换 CPU 到保护模式、加载内核、配置中断描述符表、开启分页、实现最简单的进程调度。每一步都不复杂但每一步都精确、每一步都不可跳过。这和看源码、看视频最大的区别在于源码是别人思考的最终产物你看到的是结果而不是演进过程。视频教程更是如此看完你觉得全懂了一动手就懵。但写操作系统不一样每一行代码都需要你亲自敲出来每一个 bug 都需要你自己去查。当程序跑不通的时候你只能回到原理层面去推理哪里出了问题。这个过程才是真正的学习。我记得自己第一次成功在屏幕上打印出字符的时候那个成就感比完成任何业务需求都强因为那行字符背后没有依赖任何现成的库函数它是我从 CPU 通电那一刻开始一行一行搭出来的成果。这本书的技术栈是 NASM 汇编加 GCC 的 C 语言配合 Bochs 这样的模拟器来调试。很多人一听到汇编就吓退了但书里的汇编其实非常有限只用在引导扇区和内核最开始的初始化部分一旦进入 C 语言的世界后面大部分代码读起来就没有那么大的心理压力了。2005 年前后的技术栈放到今天来看有点年头但计算机底层的原理并没有变这套做法放到现在依然完全适用。2. 动手前的准备用 Bochs 搭一个可控的试验场学写操作系统第一件事不是写代码而是搭环境。写操作系统不像写应用初期 99% 的概率你会让 CPU 跑飞、内存越界、甚至整个环境直接卡死。你要是拿真机来试光是反复重启就能让你崩溃。所以于渊书里的方案是使用 Bochs 这款模拟器。Bochs 和常见虚拟机最大的不同在于它是纯软件模拟的可以单步调试、可以设置断点、可以随时查看 CPU 寄存器和内存内容甚至能在代码里写入调试输出。对于一个要看清 CPU 内部状态的项目来说这是无可替代的优点。在 VMware 或 VirtualBox 里跑引导扇区你根本不知道 CPU 跑到哪里了只能看个寂寞。而用 Bochs你可以在任何一条指令上停下来看看寄存器里的值是否符合预期这种能力在操作系统开发调试中价值极高。你需要准备的工具有这几个Bochs主调试环境推荐装 2.6.x 版本配置方式比较稳定。NASM汇编编译器用来把 book 里的 .asm 文件编译成二进制机器码。GCC 加 ld 链接器用来编译内核的 C 语言部分注意要能生成 32 位代码。dd 等磁盘工具用来把编译好的引导扇区和内核写入软盘镜像文件。这里有一个很多新手都会忽略的点就是交叉编译。现代 Linux 发行版默认的 GCC 生成的是 64 位代码但于渊书里的内核是跑在 32 位保护模式下的所以你需要让 GCC 加上 -m32 参数才能编译出 32 位代码。我当时就在这一步折腾了挺久以为代码写错了结果只是编译参数不对。在正式开始写代码之前你还需要理解两个基础概念实模式和保护模式。CPU 上电之后先运行在 16 位的实模式这个模式下 CPU 最多只能访问 1MB 的内存地址通过段地址左移4位加偏移的方式计算。而我们现在运行的所有现代操作系统都工作在 32 位或 64 位的保护模式下可以访问更大的内存空间也支持内存分页和特权级保护。你的内核要干的第一个大事就是让 CPU 从实模式切换到保护模式。这个过程虽然只涉及几条指令但背后的机制值得花时间去弄透。还有一个关键概念是启动流程。当我们把编译出来的软盘镜像挂到 Bochs 里启动时模拟器会模拟 BIOS 的行为先把磁盘的第一个扇区512 字节读入内存的 0x7C00 地址处然后跳转到这个地址开始执行。这个扇区就是引导扇区它的最后两个字节必须写入 0x55 和 0xAA否则 BIOS 会认为这个磁盘不可引导。搞清楚这一段你就知道为什么所有操作系统教程都是从写一个引导扇区开始的——因为这是硬件强制规定的起点。3. 从引导扇区到保护模式让 CPU听你的话引导扇区只有 512 字节还要留两个字节放启动标志所以真正能放代码的空间只有 510 字节。这点空间别说写操作系统了写个稍微复杂点的功能都费劲。所以引导扇区的职责非常单一把真正的内核代码从磁盘读入内存然后跳转过去执行。在于渊书里引导扇区做三件事读磁盘、加载 loader、跳转。磁盘读取的部分直接调 BIOS 的 13 号中断服务例程。这里有个小知识点实模式下调用 BIOS 中断和我们现在写系统调用有点像都是通过软中断实现的。你只需要把参数放进寄存器然后执行 int 指令BIOS 就会帮你完成磁盘读取。第一次看到这段代码的时候我觉得不可思议——原来读磁盘这么简单但后来才明白这只是因为我们还停留在实模式BIOS 的服务还能用。等你进了保护模式这些 BIOS 中断服务全都会失效所有硬件都得自己操作。引导扇区做完自己的事情后会跳转到 loader 的入口地址。这个跳转标志着 CPU 正式离开引导阶段进入内核加载阶段。loader 的代码量明显比引导扇区多了它要完成的核心任务就是切换到保护模式。这里要先说清楚一个问题为什么要切换到保护模式因为实模式只能访问 1MB 内存而现代内核需要管理远远超过这个大小的物理内存。保护模式通过段描述符和分页机制能让 CPU 访问更大的地址空间同时提供内存保护能力不会因为一个野指针就把整个系统搞崩。切换到保护模式的代码是这本书里第一个真正考验人的地方。它涉及 GDT、段描述符、控制寄存器 CR0 的修改。GDT 的全称是全局描述符表本质上是给 CPU 准备了一张内存区域登记表每一个表项登记了一个段的基地址、长度和访问权限。CPU 在保护模式下访问内存时先通过段选择子去这张表里查相应的描述符拿到段的基地址再加偏移量才得到最终的物理地址。这个机制有点像你办理入住登记后拿到房卡去房间时刷一下卡前台系统会告诉你房间在哪一层哪个位置。切换的关键步骤于渊书里写得比较清楚关中断、加载 GDT、把 CR0 寄存器的第 0 位保护模式位置 1、然后执行一条远跳转指令刷新 CPU 流水线。很多人会漏掉最后这一步因为看起来前面的步骤已经完成了但实际上如果不跳转刷新流水线CPU 里已经预取的指令还是按实模式方式解码的程序立刻就会跑飞。我第一次跑到这里始终黑屏排查了很久才发现是远跳转的段选择子写错了。实模式下的地址计算方式也值得提一下。0x7C00 这个地址在实模式下通常表示成 0x07C0:0x0000也就是段地址 0x07C0 左移4位后加偏移 0。很多新手在网络上的问答里看到这种写法会懵其实就是段加偏移的等价表示。这类细节问题在后续调试中经常出现建议还是亲手敲一遍代码才能真正记住。4. 把控制权交给 C内核加载与第一行输出一旦 CPU 进入保护模式接下来的工作基本可以交给 C 语言了。为什么不用汇编写整个内核因为汇编的可读性和可维护性太差了。C 语言能直接操作内存地址又比汇编抽象一个层次写起来效率高得多。于渊书里的做法是用一段极其精简的汇编代码 head 做环境初始化然后把控制权转交给 C 语言写的内核主函数。head 要做的事情看名字也能猜出几分重新设定 GDT、设置分页、建立栈空间最后调用内核的 C 入口函数。这段代码量不大但每一行都值得细读。尤其是分页机制的开启这是现代操作系统内存管理的基石。分页的作用是把虚拟地址映射到物理地址让每个进程都觉得自己独占了一整块连续的内存空间但实际上物理内存可能是分散的。于渊书里用了一个比较经典的做法先让虚拟地址等于物理地址也就是所谓的身份映射等内核跑稳之后再切换到高地址空间。这种先映射、再切换、最后回收的思路你在后面的学习里还会反复遇到。C 语言内核的第一个任务是在屏幕上输出字符。很多人以为输出字符必须调用什么库函数但实际上在内核这个阶段没有任何现成的库可以用。你要直接往显存地址写数据。在文本模式下显存的映射地址是 0xB8000每两个字节对应屏幕上的一个字符低字节是字符的 ASCII 码高字节是显示属性比如黑底白字、黑底红字。所以在 C 语言里你只需要定义一个指向这个地址的指针把要显示的字符和属性写进去屏幕就会立刻发生变化。我在这个阶段第一次体会到没有操作系统也能控制硬件的感觉。往 0xB8000 写一个字符屏幕上就亮一个字符没有任何中间层帮你兜底但也不需要任何中间层。这一刻硬件向程序员完全敞开了你的代码就是硬件的上帝。当然真实的内核不可能像这样直接操作显存因为现在的显示模式早就不是文本模式了但是理解这个模型对后续学习驱动程序非常有帮助。在内核编译这个环节有一个大家比较容易踩坑的点是链接脚本。C 语言编译出来的目标文件默认情况下按 ld 的默认规则链接入口地址可能不是你期望的位置。于渊书里给了一个链接脚本规定了内核各段的布局和入口地址编译内核时必须通过 -T 参数引用这个脚本否则做出来的内核文件根本没法被 loader 正确加载。这个细节单独看书可能一晃而过自己动手做的时候会卡很久因为看起来代码逻辑都对就是跑不起来。当你在屏幕上看到自己写的内核输出第一行字符时你的小系统已经具备了最基本的生命力。但这个阶段的系统还没有响应外部事件的能力一切都要进入下一步——处理中断。5. 中断、分页与特权级三个决定系统命运的机制如果说引导和内核加载解决的是系统怎么活起来的问题那么中断、分页和特权级解决的是系统怎么稳健地活下来的问题。这三块内容也是操作系统原理课上最抽象、学生最难理解的部分但在这里它们都是你亲手写出来的代码。先看中断。操作系统的所有响应能力都建立在中断之上。时钟中断让系统有了时间概念键盘中断让系统能感知用户输入缺页中断则让虚拟内存机制可以延迟加载页面。CPU 为每个不同类型的中断分配了编号系统启动时要建立一张中断描述符表也就是 IDT。这张表记录了每个中断号对应的处理函数地址。当某个中断发生时CPU 会自动跳转到对应的处理函数去执行。于渊书里会带你写时钟中断和键盘中断的处理程序你只要在 Bochs 里按一下键盘就会看到系统的中断处理函数被触发这种感觉比看任何原理图都直观。分页机制前面提过但它的意义远比虚拟地址到物理地址的映射要深远。分页让操作系统可以实现内存的按需分配也可以实现进程间的内存隔离。同一个虚拟地址在不同进程的页表里可以映射到不同的物理地址你写代码时以为自己在一个连续的内存空间里运行实际上物理内存里的数据可能分布在各个角落。这种看得见摸不着的抽象是所有现代操作系统能够同时运行多个互不干扰的进程的前提。于渊书里的分页实现比较基础但足以让你理解页表项的结构、页目录和页表的层级关系以及为什么大页、透明页这些优化手段是必要的。接着是特权级。CPU 把运行级别分为 0 到 3 四级操作系统内核运行在最高权限的 0 级用户程序运行在最低权限的 3 级。特权级的意义在于隔离。用户程序不能直接执行 in/out 指令不能随便修改 CR0 寄存器也不能访问只有内核才能访问的内存区域。任何需要特权级权限的操作都必须通过系统调用陷入内核由内核代为执行后返回结果。这个机制和我们日常生活中进机房要申请的逻辑是一样的只有被授权的管理员才能碰核心设备。于渊书里把用户程序切换到低特权级运行的过程叫作跳到更低特权级当你第一次真正实现从内核态到用户态的切换时那种感觉不是学会了而是看见了——看见了操作系统的边界。当任务调度器和系统调用在你的系统里真正跑起来你会发现自己写的这个小系统已经具备了一个完整操作系统的骨架有加载、有输出、有中断、有进程、有用户态。剩下的文件系统、网络协议栈都是在这个骨架上添砖加瓦。而框架已经立住了加上去的东西只是时间问题。6. 踩过的坑和给后来者的建议写到这一章我翻了一下当初的调试记录挑了三个最有代表性的坑都是那种看起来怎么都对跑起来就是不行的类型。第一个坑是编译出来的二进制文件带了额外的文件头。NASM 默认输出的格式可能是你指定的格式但如果你写的是汇编源码在某种配置下会把调试信息或者目标文件格式头写进去直接当作引导扇区写进软盘镜像Bochs 根本不认。解决方法是先用 hexdump 之类的工具查看生成的二进制确认文件末尾有没有 0x55 0xAA以及文件大小是否恰好 512 字节或更少。这种基础的检查能帮你省掉大把排查时间。第二个坑是内核链接成了 ELF 格式但 loader 已经按裸二进制的方式把它读入内存了。如果编译 C 代码后没有用 ld 的 --oformat binary 参数指定输出原始二进制格式链接出来的文件带有 ELF 文件头loader 把这堆文件头当作代码跳进去CPU 立刻执行了无意义的指令现象就是系统一进内核入口就直接死机。这类问题光看代码很难发现除非你用 hexdump 看内核文件的开头是不是以 ELF 的魔数开头而不是直接以代码指令开头。第三个坑是段描述符的 base 和 limit 算错了。保护模式下段描述符里的 base 和 limit 各有其编码规则每个 bit 的放置位置有讲究。如果你把 limit 写错一个字节系统能进保护模式但访问某段区域时会触发异常表现为系统运行几秒后无故重启。这类问题只能靠 Bochs 的单步调试去定位所以建议你把 Bochs 的调试功能学好至少会用断点和查看寄存器、查看内存这几个基础操作。给后来者的建议我的核心体会是三句话。第一句跟着书把代码敲一遍不要直接复制光盘或压缩包里的最终代码。自己敲的过程其实就是在建立肌肉记忆和心智模型很多细节是眼睛看不会的。第二句每敲完一个章节就立刻跑一遍编译和运行不要攒到最后。操作系统模块之间的依赖非常紧密如果前面的环境出了问题后面的代码跑不起来排查难度会成倍增加。第三句遇到原理暂时不懂的地方先标记下来跑通了再回来看。操作系统知识点是网状的不是线性的顺序不必强求重要的是有一根能跑的主线把知识点串起来。当你把这个项目完整做完再回来看 Linux 内核源码你会有一种完全不同的感觉。你不再是从零开始面对一个陌生的庞大系统而是带着一份原来内核的每个部分是这样分工的地图走进代码里。你会发现那些看起来高不可攀的调度器、内存管理器、虚拟文件系统其核心思想都在这本书里出现过雏形。操作系统并没有那么神秘它只是一堆精确的细节每个细节都不难难的是把它们组织成一张网。而这本书恰恰就是帮你织好这张网的起点。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门