系列简介
本系列的目标是:从零开始,一步步构建一个完整的操作系统——从引导程序到 Shell 命令行。
编写操作系统是理解计算机底层原理最直接的方式。市面上虽然有不少操作系统教材,但往往因篇幅限制或面向特定体系结构,让初学者难以找到一条循序渐进、可动手实践的路径。本系列试图弥补这一不足。
市面上大多数操作系统课程从概念出发,先讲进程调度、内存管理、文件系统这些高层抽象,再回头看底层实现。这种路径适合建立宏观认知,但对想真正理解「计算机从上电到运行程序究竟发生了什么」的人来说,总觉得少了点什么。本系列反其道而行,从按下电源键的那一刻开始,逐行代码地走完从硬件到软件的整条链路。
完成本系列后,你将亲手构建出一个完整的操作系统:从 512 字节的 MBR 引导程序,到能切换保护模式、开启分页的 Loader,再到加载 ELF 格式内核并跳转到 C 代码执行,最终实现中断处理、内存管理、进程调度、文件系统、系统调用和 Shell——这些不是模拟或伪代码,而是在 QEMU 和真实硬件上都能运行的真实程序。
为什么要学习操作系统开发
你可能会问:我又不打算写一个生产级操作系统,为什么要花时间学这个?
第一,消除知识的黑盒。 每天都在用的 printf、malloc、fork,它们背后究竟发生了什么?虚拟地址是怎么变成物理地址的?进程切换时 CPU 做了哪些事?理解这些原理,能让你从「会用」变成「真正理解」。
第二,提升调试能力。 当程序出现段错误、内存泄漏、死锁时,理解底层机制的人能更快定位问题。你知道缺页异常的来龙去脉,就知道为什么某些内存访问模式会导致性能骤降。
第三,面试与职业发展。 互联网大厂的基础架构、数据库、云原生等岗位,操作系统原理是核心考察点。手写过引导程序和内存管理代码,比单纯背诵概念有说服力得多。
第四,纯粹的技术乐趣。 当你第一次看到自己写的 MBR 在屏幕上打印出 Hello World,那种从无到有创造出一个能被 CPU 执行的程序的感觉,是任何高级语言框架都给不了的。
系列大纲
引导篇:从加电到内核
| 章节 | 标题 | 主要内容 |
|---|---|---|
| 第 1 章 | 从系统加电到 Shell | BIOS/UEFI 启动流程,GRUB 引导,内核初始化,第一个用户进程 |
| 第 2 章 | 手写 MBR 引导程序 | 实模式内存布局,BIOS 磁盘读写 INT 13h,MBR 与 Loader 的结构设计 |
| 第 3 章 | 开启保护模式与虚拟内存 | GDT 描述符表,CR0 切换保护模式,页目录/页表结构,开启分页 |
| 第 4 章 | 虚拟内存 | 虚拟地址空间布局,MMU 工作原理,多级页表结构,缺页处理与 TLB |
| 第 5 章 | 正式加载内核 | ELF 格式解析,内核进入,跳转到 C 代码 |
内核篇:中断、内存与调度
| 章节 | 标题 | 主要内容 |
|---|---|---|
| 第 6 章 | 代码重构:模块化架构设计 | 基础类型定义、VGA 驱动封装、格式化输出、端口 I/O 抽象 |
| 第 7 章 | 中断系统:IDT、PIC 与定时器 | IDT 构建、PIC 编程、时钟中断、键盘中断 |
| 第 8 章 | 内存管理:PMM、VMM 与内核堆 | 物理内存管理器、虚拟内存管理器、kheap 分配器 |
| 第 9 章 | 任务调度系统:task_t 与上下文切换 | task_t 结构体、上下文保存与恢复、协作式调度 |
| 第 10 章 | 调度器:运行队列与 Round-Robin | 运行队列、RR 调度算法、定时器驱动的抢占式调度 |
同步与特权篇
| 章节 | 标题 | 主要内容 |
|---|---|---|
| 第 11 章 | 同步原语:自旋锁、互斥锁与信号量 | 自旋锁、互斥锁、信号量的实现与使用 |
| 第 12 章 | 用户空间:TSS 与 Ring 3 切换 | TSS 结构、Ring 0→Ring 3 切换、用户态栈与内核态栈 |
| 第 13 章 | 设备驱动:键盘与 ATA 硬盘 | 键盘驱动、ATA PIO 硬盘读写、中断驱动的 I/O |
文件与进程篇
| 章节 | 标题 | 主要内容 |
|---|---|---|
| 第 14 章 | 文件系统:VFS 与 SimpleFS | VFS 抽象层、SimpleFS 磁盘布局、文件读写操作 |
| 第 15 章 | 进程管理:fork 与 exec | fork 进程复制、exec 程序加载、进程生命周期管理 |
| 第 16 章 | 系统调用:20 个 POSIX 接口 | int 0x80 系统调用、POSIX 兼容接口实现 |
用户空间篇
| 章节 | 标题 | 主要内容 |
|---|---|---|
| 第 17 章 | Shell:命令解析与执行 | 命令行解析、管道与重定向、内置命令 |
| 第 18 章 | 用户工具:ls、cat、cp 等命令 | 用户态工具实现、文件操作命令 |
| 第 19 章 | 启动镜像:系统集成与自检 | 完整系统镜像构建、启动自检、集成验证 |
各章重点概念
第 1 章建立全貌认知。你将了解 CPU 上电后第一条指令从哪里来,BIOS POST 自检做了什么,Legacy BIOS 和 UEFI 有什么区别,GRUB 是如何把内核加载进内存的,以及 init 进程如何成为所有用户进程的祖先。这章不写代码,但它是后续所有章节的知识地基。
第 2 章是动手的起点。你将编写一个 512 字节的 MBR 程序,它会被 BIOS 加载到内存 0x7C00 处执行。核心技能包括:实模式下通过 BIOS INT 13h 读取磁盘扇区,将 Loader 程序从磁盘加载到内存 0x8000,然后跳转过去执行。同时你会深入理解实模式 1MB 内存的布局,知道哪些区域可用、哪些被硬件占用。
第 3 章是技术难度最高的一章。你需要做三件关键的事:构建 GDT(全局描述符表)定义代码段和数据段,将 CR0.PE 位置 1 切换到 32 位保护模式,然后搭建二级页表并开启分页。每一步都有严格的顺序要求和陷阱,比如 GDT 必须在切换保护模式之前加载,页表必须在开启分页之前初始化好。
第 4 章从理论层面深入虚拟内存。在上一章实际开启分页的基础上,这章会解释虚拟地址到物理地址的完整转换过程,为什么需要多级页表,TLB 如何加速地址翻译,以及缺页异常的处理流程。理解这些对后续学习 Linux 内核的内存管理至关重要。
第 5 章完成引导链的最后一环。你将解析 ELF 文件的头部和程序头表,把内核的各个段加载到正确的内存位置,最终从汇编代码跳转到 C 语言的 kernel_main 函数。到此,从按下电源键到内核开始执行 C 代码的完整链路就打通了。
第 6 章是代码质量的关键转折点。内核从「能运行」到「可维护」,需要模块化重构:定义基础类型(u8/u16/u32)、封装 VGA 文本模式驱动、实现 printk 格式化输出、抽象端口 I/O 操作。好的架构让后续功能开发事半功倍。
第 7 章构建中断基础设施。你将设置 IDT(中断描述符表)、编程 8259A PIC、注册时钟中断和键盘中断处理程序。中断是操作系统响应外部事件的根本机制,也是实现抢占式调度的前提。
第 8 章实现内存管理三大组件:物理内存管理器(PMM,位图法分配物理页)、虚拟内存管理器(VMM,管理进程地址空间)、内核堆分配器(kheap,提供 kmalloc/kfree 接口)。没有内存管理,后续的进程和文件系统都无从谈起。
第 9 章定义任务结构和上下文切换机制。task_t 结构体保存进程的寄存器状态、页目录、栈指针等信息,上下文切换通过保存/恢复寄存器实现。这是多任务操作系统的核心。
第 10 章实现 Round-Robin 调度器。运行队列管理所有就绪任务,时钟中断驱动调度决策,每个任务获得相等的时间片。从此内核具备了多任务并发的能力。
第 11 章实现同步原语。自旋锁用于 SMP 短临界区、互斥锁用于可睡眠的长临界区、信号量用于资源计数。没有同步,多任务系统就会数据竞争、死锁。
第 12 章实现用户空间。通过 TSS 结构和 iret 指令从 Ring 0 切换到 Ring 3,为用户进程建立独立的栈空间。这是操作系统安全隔离的基础。
第 13 章编写设备驱动。键盘驱动通过中断接收按键、ATA 硬盘驱动通过 PIO 模式读写扇区。设备驱动是内核与硬件交互的桥梁。
第 14 章实现文件系统。VFS 提供统一抽象层,SimpleFS 定义磁盘布局(超级块、inode、数据块),支持文件的创建、读写和删除。
第 15 章实现进程管理。fork 复制进程地址空间和文件描述符、exec 加载新程序替换进程映像。这是 UNIX 进程模型的精髓。
第 16 章实现系统调用。通过 int 0x80 从用户态进入内核态,实现 20 个 POSIX 兼容接口(read/write/open/close/fork/exec/waitpid 等)。系统调用是用户与内核的唯一通道。
第 17 章实现 Shell。命令行解析、管道(|)和重定向(>/<)、内置命令(cd/exit)。Shell 是用户与操作系统交互的界面。
第 18 章实现用户工具。ls、cat、cp、mv、rm、mkdir 等常用命令,它们通过系统调用操作文件,是用户空间程序的最佳范例。
第 19 章系统集成与自检。将 MBR、Loader、内核、文件系统、用户程序打包为完整的启动镜像,启动后进行自检,验证各子系统协同工作。至此,一个完整的操作系统从零构建完成。
从加电到 Shell:全局视角
在深入每一章之前,先对整个系统构建流程建立一个整体认知。下图展示了从 CPU 上电到 Shell 运行的关键步骤和各步骤之间的衔接关系:
这个流程图揭示了几个关键事实:
- 整个启动过程是一个接力赛,每一棒都把 CPU 从一个状态推进到下一个状态
- 模式切换是单向的:实模式 → 保护模式 → 开启分页,没有回头路
- 每一步都必须在前一步正确完成的基础上才能进行,顺序不能打乱
- 最终目标是从裸机到用户可交互的 Shell,完成一个完整操作系统的构建
将构建什么
本系列的最终产出是一个完整的操作系统,从引导程序到 Shell 命令行。涉及的核心组件如下:
引导链的三个程序各司其职:
| 程序 | 大小 | 加载位置 | 运行模式 | 核心职责 |
|---|---|---|---|---|
mbr.bin | 512 字节 | 0x7C00 | 实模式 | 读取磁盘,将 Loader 加载到内存,跳转执行 |
loader.bin | 数 KB | 0x8000 | 实→保护 | 构建GDT,切换保护模式,开启分页,加载内核 |
kernel.bin | 数十 KB | 0x100000 | 保护模式 | C 语言内核入口,从此进入操作系统开发阶段 |
内核启动后,将依次初始化以下子系统:
| 子系统 | 对应章节 | 核心功能 |
|---|---|---|
| 中断系统 | 第 7 章 | IDT、PIC、时钟中断、键盘中断 |
| 内存管理 | 第 8 章 | PMM 物理页分配、VMM 地址空间管理、kheap 堆分配 |
| 任务调度 | 第 9-10 章 | task_t 结构、上下文切换、RR 调度器 |
| 同步原语 | 第 11 章 | 自旋锁、互斥锁、信号量 |
| 用户空间 | 第 12 章 | TSS、Ring 3 切换、用户态栈 |
| 设备驱动 | 第 13 章 | 键盘驱动、ATA 硬盘驱动 |
| 文件系统 | 第 14 章 | VFS 抽象层、SimpleFS 实现 |
| 进程管理 | 第 15 章 | fork、exec、进程生命周期 |
| 系统调用 | 第 16 章 | int 0x80、20 个 POSIX 接口 |
| Shell | 第 17 章 | 命令解析、管道、重定向 |
| 用户工具 | 第 18 章 | ls、cat、cp 等命令 |
学习路径
本系列采用「自底向上」的学习路径,每一章都建立在前一章的基础上:
引导篇(第 1-5 章)
- 第 1 章:建立对启动流程的整体认知,理解 BIOS、Bootloader、内核、init 各层的职责
- 第 2 章:动手实践,编写第一个能运行的引导程序,掌握实模式下的内存和磁盘操作
- 第 3 章:突破实模式限制,进入 32 位保护模式,搭建虚拟内存的基础设施
- 第 4 章:深入理解虚拟内存机制,为后续内核开发打下理论基础
- 第 5 章:完成引导链的最后一步,加载并运行真正的 C 语言内核
内核篇(第 6-10 章)
- 第 6 章:模块化重构,让内核代码从「能跑」变成「可维护」
- 第 7 章:构建中断系统,让内核能响应外部事件
- 第 8 章:实现内存管理,为进程和文件系统提供基础
- 第 9 章:定义任务结构,实现上下文切换
- 第 10 章:实现调度器,让多个任务轮流执行
同步与特权篇(第 11-13 章)
- 第 11 章:实现同步原语,解决多任务并发问题
- 第 12 章:进入用户空间,实现特权级隔离
- 第 13 章:编写设备驱动,让内核与硬件交互
文件与进程篇(第 14-16 章)
- 第 14 章:实现文件系统,支持持久化存储
- 第 15 章:实现 fork/exec,支持进程创建和程序加载
- 第 16 章:实现系统调用,建立用户态与内核态的桥梁
用户空间篇(第 17-19 章)
- 第 17 章:实现 Shell,用户可以通过命令行与系统交互
- 第 18 章:实现常用命令工具,验证系统调用和文件系统
- 第 19 章:集成所有组件,构建完整的启动镜像
知识依赖关系
各章之间的依赖不只是「读完上一章再读下一章」那么简单。下图展示了核心知识点之间的依赖关系:
注意几个关键分支点:
- GDT 是保护模式的入口:不理解段描述符就无法正确切换保护模式
- 分页建立在保护模式之上:必须先进入保护模式,才能开启分页
- ELF 解析依赖保护模式:内核通常被链接到 1MB 以上的地址,实模式无法直接访问
- 中断是调度和驱动的基础:时钟中断驱动调度,键盘中断驱动 Shell 输入
- 内存管理是进程和文件系统的前提:PMM 为 fork 提供物理页,VMM 为 exec 提供地址空间
- 系统调用是用户态的桥梁:Shell 和用户工具都通过系统调用与内核交互
前置知识
阅读本系列之前,建议先了解:
汇编语言基础(x86 指令集)
本系列使用 NASM 语法编写汇编代码。你需要理解以下概念:
- 常用寄存器的用途:
EAX(累加器)、EBX(基址)、ECX(计数器)、EDX(数据)、ESP(栈指针)、EBP(基址指针)、ESI/EDI(源/目标索引) - 内存寻址方式:立即寻址、寄存器寻址、直接寻址、寄存器间接寻址、基址变址寻址
- 常用指令:
mov、jmp、call、ret、push、pop、int、lgdt、lidt - 16 位实模式与 32 位保护模式下指令的差异
可参考 汇编语言:从机器码到现代应用的底层探索。
CPU 工作模式
x86 处理器有三种工作模式,本系列会用到前两种:
| 模式 | 地址宽度 | 最大寻址空间 | 特点 |
|---|---|---|---|
| 实模式 | 20 位 | 1MB | 段地址 × 16 + 偏移地址,无权限检查 |
| 保护模式 | 32 位 | 4GB | 段选择子 + GDT 描述符,支持特权级与分页 |
| 长模式 | 64 位 | 256TB | 64 位寻址,兼容模式,本系列不涉及 |
可参考 CPU 工作模式。
C 语言编程
从第 5 章开始,内核代码将使用 C 语言编写。你需要熟悉:
- 指针与内存操作
- 结构体与位域(解析 ELF 头部时会用到)
- 链接器的基本概念(段、符号表、重定位)
计算机组成原理
理解以下概念会让学习过程更顺畅:
- CPU 流水线与指令执行周期
- 内存层次结构:寄存器 → Cache → 内存 → 磁盘
- 中断机制:硬件中断与软件中断的区别
- I/O 端口与内存映射 I/O
开发环境搭建
本系列在 Linux 环境下开发,使用以下工具链:
必装工具
# 汇编器:NASM(Netwide Assembler)# NASM 是 x86 平台最流行的汇编器之一,语法简洁,支持多种输出格式sudo apt install nasm
# 模拟器:QEMU(用于无需真实硬件的调试)# qemu-system-x86 可以模拟完整的 x86 计算机sudo apt install qemu-system-x86
# 调试器:GDB(可选,用于断点调试引导程序)sudo apt install gdb
# 十六进制查看工具sudo apt install hexdump项目目录结构
建议按以下结构组织项目代码:
my-os/├── boot/│ ├── mbr.asm # MBR 引导程序源码│ └── loader.asm # Loader 程序源码├── kernel/│ ├── entry.asm # 内核入口(汇编,设置栈后调用 C 函数)│ └── main.c # 内核主函数├── build/│ ├── mbr.bin # 编译输出的 MBR 二进制│ ├── loader.bin # 编译输出的 Loader 二进制│ ├── kernel.bin # 编译输出的内核二进制│ └── os.img # 最终的磁盘映像├── Makefile # 构建脚本└── run.sh # QEMU 运行脚本Makefile 模板
下面是一个完整的 Makefile,可以一键完成编译、链接、生成磁盘映像和启动 QEMU:
# Makefile for OS development
ASM = nasmQEMU = qemu-system-i386CC = gccLD = ld
# 编译选项ASM_FLAGS = -f binC_FLAGS = -m32 -ffreestanding -fno-pie -nostdlib -nostdinc \ -fno-builtin -fno-stack-protector -nostartfiles \ -nodefaultlibs -Wall -Wextra -cLD_FLAGS = -m elf_i386 -Ttext 0x1000 --oformat binary
# 源文件MBR_SRC = boot/mbr.asmLOADER_SRC = boot/loader.asmKERNEL_ENTRY = kernel/entry.asmKERNEL_C = kernel/main.c
# 输出文件BUILD_DIR = buildMBR_BIN = $(BUILD_DIR)/mbr.binLOADER_BIN = $(BUILD_DIR)/loader.binKERNEL_BIN = $(BUILD_DIR)/kernel.binOS_IMG = $(BUILD_DIR)/os.img
.PHONY: all clean run debug
all: $(OS_IMG)
$(BUILD_DIR): mkdir -p $(BUILD_DIR)
# 编译 MBR$(MBR_BIN): $(MBR_SRC) | $(BUILD_DIR) $(ASM) $(ASM_FLAGS) $< -o $@
# 编译 Loader$(LOADER_BIN): $(LOADER_SRC) | $(BUILD_DIR) $(ASM) $(ASM_FLAGS) $< -o $@
# 编译内核$(KERNEL_BIN): $(KERNEL_ENTRY) $(KERNEL_C) | $(BUILD_DIR) $(ASM) -f elf32 $(KERNEL_ENTRY) -o $(BUILD_DIR)/entry.o $(CC) $(C_FLAGS) $(KERNEL_C) -o $(BUILD_DIR)/main.o $(LD) $(LD_FLAGS) $(BUILD_DIR)/entry.o $(BUILD_DIR)/main.o -o $@
# 生成磁盘映像$(OS_IMG): $(MBR_BIN) $(LOADER_BIN) $(KERNEL_BIN) | $(BUILD_DIR) cat $(MBR_BIN) > $(OS_IMG) # 用零填充 MBR 到一个完整磁道(通常 1 磁道 = 18 扇区) dd if=/dev/zero bs=512 count=17 >> $(OS_IMG) 2>/dev/null # 写入 Loader dd if=$(LOADER_BIN) bs=512 seek=1 of=$(OS_IMG) conv=notrunc 2>/dev/null # 写入内核(从第 10 扇区开始) dd if=$(KERNEL_BIN) bs=512 seek=10 of=$(OS_IMG) conv=notrunc 2>/dev/null
# 启动 QEMUrun: $(OS_IMG) $(QEMU) -drive format=raw,file=$(OS_IMG) -m 128M
# 启动 QEMU 并等待 GDB 连接debug: $(OS_IMG) $(QEMU) -drive format=raw,file=$(OS_IMG) -m 128M \ -s -S -serial stdio
clean: rm -rf $(BUILD_DIR)使用 dd 手动构建磁盘映像
如果不使用 Makefile,也可以手动用 dd 命令构建磁盘映像:
# 创建 1MB 的空白磁盘映像dd if=/dev/zero of=os.img bs=512 count=2048
# 写入 MBR 到第 1 个扇区(扇区编号从 0 开始)dd if=mbr.bin of=os.img bs=512 count=1 conv=notrunc
# 写入 Loader 从第 2 个扇区开始dd if=loader.bin of=os.img bs=512 seek=1 conv=notrunc
# 写入内核从第 10 个扇区开始dd if=kernel.bin of=os.img bs=512 seek=10 conv=notruncconv=notrunc 参数非常重要,它告诉 dd 不要截断输出文件。如果省略这个参数,dd 会把目标文件截断到写入的数据长度,后续写入的内容就会丢失。
QEMU 常用启动参数
# 基础启动qemu-system-i386 -drive format=raw,file=os.img
# 指定内存大小(默认 128MB,建议显式指定)qemu-system-i386 -drive format=raw,file=os.img -m 128M
# 启用串口输出(方便查看 print 调试信息)qemu-system-i386 -drive format=raw,file=os.img -serial stdio
# 等待 GDB 远程连接(端口 1234)qemu-system-i386 -drive format=raw,file=os.img -s -S
# 同时启用 GDB 和串口qemu-system-i386 -drive format=raw,file=os.img -s -S -serial stdio
# 无图形界面模式(纯串口输出)qemu-system-i386 -drive format=raw,file=os.img -nographic
# 使用 KVM 加速(如果宿主机支持)qemu-system-i386 -drive format=raw,file=os.img -enable-kvm关键参数说明:
| 参数 | 含义 |
|---|---|
-s | 在 TCP 1234 端口监听 GDB 连接 |
-S | 启动时暂停 CPU,等待 GDB 发出 continue |
-serial stdio | 将串口输出重定向到终端标准输出 |
-nographic | 禁用图形窗口,仅使用串口终端 |
-m 128M | 分配 128MB 内存给虚拟机 |
conv=notrunc | dd 写入时不截断目标文件 |
调试技巧
操作系统开发中的调试比普通程序困难得多,因为你写的就是最底层的代码,没有操作系统帮你打印错误信息。掌握以下调试手段至关重要。
QEMU Monitor
QEMU 运行时,图形窗口中按 Ctrl+Alt+2 可以切换到 QEMU Monitor 控制台(-nographic 模式下没有此功能)。常用命令:
# 查看寄存器状态info registers
# 查看内存内容(x/Nx 表示以十六进制显示 N 个字)x/16x 0x7c00
# 查看页表信息info tlbinfo mem
# 单步执行singlestep on
# 暂停/继续stopcont
# 保存虚拟机快照savevm snapshot_name
# 恢复快照loadvm snapshot_nameGDB 远程调试
这是调试引导程序最强大的方式。启动 QEMU 时加 -s -S 参数,然后在另一个终端启动 GDB:
# 终端 1:启动 QEMU,暂停等待 GDB 连接qemu-system-i386 -drive format=raw,file=os.img -s -S
# 终端 2:启动 GDB 连接到 QEMUgdb(gdb) target remote :1234(gdb) break *0x7c00 # 在 MBR 入口处设断点(gdb) break *0x8000 # 在 Loader 入口处设断点(gdb) continue # 继续执行到断点(gdb) info registers # 查看寄存器(gdb) x/16x $esp # 查看栈内容(gdb) si # 单步执行一条汇编指令(gdb) ni # 单步执行(跳过函数调用)调试 16 位实模式代码时,GDB 需要特殊设置:
# 设置为 16 位反汇编模式set disassembly-flavor intelset architecture i8086
# 实模式下查看物理内存x/16xb 0x7c00
# 查看段寄存器info registers cs ds es fs gs ss调试保护模式代码时,需要切换回 32 位模式:
set architecture i386Bochs 调试器
Bochs 是另一个 x86 模拟器,内置了强大的调试器,特别适合调试引导程序:
# 安装 Bochs(需要带调试器的版本)sudo apt install bochs bochs-sdl
# 启动 Bochs 调试模式bochs -dbgBochs 调试器的优势在于它对实模式的支持更好,而且可以方便地查看 GDT、IDT、页表等数据结构:
# Bochs 调试器命令info gdt # 查看 GDT 内容info idt # 查看 IDT 内容info page # 查看页表info tss # 查看 TSScreg # 查看控制寄存器(CR0, CR2, CR3, CR4)sreg # 查看段寄存器vb 0x7c00 # 在物理地址设断点十六进制检查
当引导程序行为异常时,第一步通常是检查生成的二进制文件是否正确:
# 查看整个 MBR 的十六进制内容hexdump -C mbr.bin
# 只看最后两个字节(应该是 0x55 0xAA,即 MBR 签名)hexdump -C mbr.bin | tail -1
# 用 od 查看od -A x -t x1z mbr.bin
# 检查 MBR 签名是否正确# 偏移 510 和 511 处应该是 55 AAxxd -s 510 -l 2 mbr.bin常见陷阱与排错
操作系统开发中有些错误几乎是每个初学者都会遇到的,提前了解可以节省大量调试时间。
MBR 签名缺失
现象:QEMU 启动后没有任何输出,黑屏。
原因:MBR 的最后两个字节必须是 0x55 0xAA,BIOS 在加载 MBR 后会检查这个签名。如果签名不存在,BIOS 不会跳转到 0x7C00 执行。
解决方法:在 NASM 源码末尾添加:
; MBR 签名(必须位于第 510-511 字节)times 510 - ($ - $$) db 0dw 0xAA55 ; 小端序,磁盘上为 55 AAtimes 510 - ($ - $$) db 0 这行的作用是用零填充到第 510 字节,确保签名恰好位于正确位置。
org 指令与加载地址不匹配
现象:字符串打印乱码或跳转到错误地址。
原因:org 指令告诉汇编器程序将被加载到哪个地址,所有地址计算都基于这个值。如果 org 0x7C00 但实际加载到别的地址,所有偏移量都会错位。
解决方法:MBR 必须使用 org 0x7C00,Loader 必须使用 org 0x8000(或其他你设定的加载地址),保持与实际加载位置一致。
实模式段寄存器未初始化
现象:使用 esi、edi 等寄存器访问内存时数据错误。
原因:实模式下,内存访问使用 段地址:偏移地址 的方式。ds、es 等段寄存器必须正确设置,否则即使偏移地址正确,实际访问的物理地址也是错的。
解决方法:在 MBR 入口处初始化段寄存器:
[bits 16][org 0x7C00]
start: xor ax, ax mov ds, ax ; DS = 0 mov es, ax ; ES = 0 mov ss, ax ; SS = 0 mov sp, 0x7C00 ; 栈指针设在 MBR 下方,向下增长 ; ... 后续代码保护模式切换后忘记远跳转
现象:设置 CR0.PE = 1 后程序崩溃或行为异常。
原因:修改 CR0 后,CPU 的预取队列中仍然有实模式的指令。需要通过远跳转(far jump)清空预取队列,让 CPU 真正以保护模式取指令。
解决方法:
; 设置 CR0.PE = 1mov eax, cr0or eax, 1mov cr0, eax
; 远跳转清空流水线,跳转到 32 位代码段jmp dword 0x08:protected_mode_entry
[bits 32]protected_mode_entry: ; 现在才真正在保护模式下执行 mov ax, 0x10 ; 加载数据段选择子 mov ds, ax mov es, ax mov ss, ax ; ...其中 0x08 是代码段选择子(GDT 中第 1 个描述符,RPL=0),0x10 是数据段选择子(GDT 中第 2 个描述符,RPL=0)。
分页开启后地址映射错误
现象:开启分页后立即三重故障重启。
原因:开启分页(CR0.PG = 1)的一瞬间,CPU 开始通过页表翻译地址。如果页表没有正确映射当前正在执行的代码,CPU 就取不到下一条指令,触发缺页异常。如果缺页处理程序也无法访问(IDT 还没设置),就会触发双倍故障,最终三重故障重启。
解决方法:在开启分页前,确保页目录和页表已经正确初始化,并且当前执行的代码地址已经被映射。常见做法是建立恒等映射(identity mapping),让虚拟地址等于物理地址:
; 假设页目录在 0x100000,页表在 0x101000; 恒等映射:虚拟地址 = 物理地址
; 设置页目录的第 0 项,指向第一个页表mov dword [0x100000], 0x101003 ; 页表地址 + Present + R/W 位
; 设置页表的各项,映射前 4MB(1024 个 4KB 页面)mov ecx, 0mov edi, 0x101000.fill_page_table: mov eax, ecx shl eax, 12 ; eax = ecx * 4096(物理地址) or eax, 0x003 ; Present + R/W 位 mov [edi], eax add edi, 4 inc ecx cmp ecx, 1024 jl .fill_page_table
; 加载页目录地址到 CR3mov eax, 0x100000mov cr3, eax
; 开启分页mov eax, cr0or eax, 0x80000000 ; CR0.PG = 1mov cr0, eax扩展学习方向
完成本系列后,你已经有了一个能进入保护模式并加载内核的基础平台。接下来的扩展方向包括:
- 进入长模式:从 32 位保护模式切换到 64 位长模式,支持更大的地址空间
- 中断与异常处理:设置 IDT,处理硬件中断和 CPU 异常,这是实现键盘中断、时钟中断的基础
- 内核内存管理:实现物理内存分配器(buddy system、slab 分配器)和虚拟内存区域管理
- 进程管理:实现上下文切换、调度器、进程间通信
- 文件系统:实现简单的文件系统驱动,支持从磁盘读写文件
- 用户态:实现特权级切换(Ring 0 → Ring 3),系统调用接口
每个方向都值得深入研究。本系列的目标不是构建一个完整的操作系统,而是打通从硬件到内核的这条最关键的路。走通这条路之后,其他的扩展就是在这个基础上的增量开发。
参考资料
- OSDev Wiki — 操作系统开发的百科全书,几乎所有问题都能在这里找到线索
- Intel 64 and IA-32 Architectures Software Developer’s Manual — x86 架构的权威参考手册,Vol.3A 中对保护模式和分页的描述是必读内容
- 南京大学:操作系统设计与实现 — 南京大学 jyy 老师的操作系统课程,理论与实践结合
- Linux 内核文档 — 官方内核文档
- The little book about OS development — 简洁的操作系统开发入门小书
- os-tutorial by cfenollosa — 从零创建操作系统的步骤化教程
- 《操作系统真象还原》郑钢著 — 中文著作中实践性很强的操作系统开发书,从零实现一个简易 OS
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






