news 2026/9/30 8:30:40

手写MBR:从实模式到BIOS中断的裸机引导实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写MBR:从实模式到BIOS中断的裸机引导实践

在手写操作系统之前,我建议你先搞清楚一件最基础也最容易被忽略的事:按下电源键之后,CPU到底在做什么,又是谁把硬盘里的代码搬进内存的。我当年啃《操作系统真象还原》前两章时,最大的感受就是“掌权”这两个字实在太形象了——第二章的MBR主引导记录,就是我们从硬件手里接管电脑的第一个程序。它不依赖任何操作系统,纯裸机环境,直接在实模式下运行,代码量很小但每一行都有讲究。这篇文章就围绕第二章的核心实践来展开,我会带你手写一个能打印字符的MBR,同时把实模式、内存布局、BIOS中断调用、引导扇区这些概念一次讲透。

这篇内容很适合两类人看:一类是正在跟书同步做实验的初学者,想知道“为什么这样写”而不只是“怎么写”;另一类是搞过Linux、看过不少启动流程,但没亲手在裸机上写过代码的开发者。看完之后,你能独立写出一份512字节的MBR,并且清楚它为什么可以开机自举,也能自己设计简单的引导程序打印信息甚至加载后续内核。

1. 开机到MBR之间发生了什么

1.1 CPU启动瞬间的实模式真相

只要你接触过PC底层开发,一定听过“实模式”这个词。但很多人对它的理解停留在“一段历史遗留状态”,其实它决定了你写MBR时所有的内存访问方式。

x86 CPU从加电那一刻起,会先进入实模式。实模式的本质是16位寻址环境,寄存器宽度16位,指令默认按16位解析,寻址方式为“段寄存器左移4位 + 偏移地址”。这种模式下,CPU最多只能访问1MB内存空间,也就是0x00000到0xFFFFF。其中很多地址还被硬件设备、BIOS数据区、视频缓冲区占用,能让你自由折腾的内存非常有限。

为什么CPU要这样设计?为了保证向前兼容,现代x86处理器为了能启动老旧的16位引导程序,必须保留实模式作为硬件初始状态。比如你用的是酷睿i9,启动瞬间也会先进入实模式,跑一段16位代码,再切换进32位或64位保护模式。这个切换动作往往就发生在MBR或引导加载器内部。

真正让新手困惑的是:为什么MBR要用16位汇编写?因为CPU此刻就在实模式,指令集天然是16位的,你写32位代码它根本不认。除非你在MBR开头就执行切换到保护模式的指令,但那样代码量会急剧膨胀,而且也没有必要。第二章的目的是“先掌权”,做一个最简的引导,所以老老实实用16位汇编就对了。

1.2 BIOS中断:没有操作系统时唯一的外设接口

刚开机时,内存里除了BIOS固件映射的数据,什么都没有。你要访问硬盘、屏幕、键盘,不能像在Linux里一样通过系统调用,只能靠BIOS提供的硬件中断服务程序。这套服务通过软中断指令触发,最常见的就是int 0x10用于显示,int 0x13用于磁盘读写,int 0x16用于键盘输入。

很多初学者不理解“中断”在这个语境下的含义。它不是硬件主动打断CPU,而是你主动发起的一个软中断调用,类似于一个固定入口的系统服务。BIOS在启动时已经把这些服务例程安装到了内存低端,并配置好中断向量表。CPU执行int 0x10时,会根据中断号查表,跳转到对应的BIOS功能函数。

在MBR阶段,我们最常用的就是int 0x10的显示功能和int 0x13的磁盘读取功能。前者用于输出字符到屏幕,后者用于把硬盘上的后续扇区加载进内存。第二部分代码实现在Linux下用qemu跑MBR,打印文字;第三部分是练习读取磁盘扇区的流程。

2. MBR的使命与磁盘布局

2.1 为什么是512字节,为什么是0x7C00

MBR不是一段任意大小的代码,它必须放在硬盘的第一个扇区,也就是0号扇区。这个扇区有固定的512字节大小,最后两个字节必须是0x55和0xAA,作为引导扇区的合法标志。

BIOS自检完成后,会读取0号扇区到内存中的物理地址0x7C00,然后跳转到这个地址执行。为什么偏偏是0x7C00?这是历史原因:早期操作系统需要为BIOS自身数据区保留足够空间,同时也要给后续加载的引导代码留出足够连续的可用区域,0x7C00恰好是在系统最低内存区上方一个相对安全的位置。我们只需要知道这是一条规则,不用过度纠结,反正所有的MBR代码都会在0x7C00处运行。

注意,0x7C00不是一个“默认选择”,而是BIOS固件写死的加载地址。所以你在写MBR时,如果涉及到绝对内存地址的访问,必须基于0x7C00做偏移计算。比如你的代码中定义了一个标签main,它在代码段里的位置是0x7C00加偏移,但你不能直接引用这个物理地址,还是需要用段选择子和偏移组合来定位。

2.2 MBR与GPT:都叫主引导,能互相替代吗

热词里经常出现“MBR和GPT分区的区别”,其实它们是两个层面的东西。MBR是一种磁盘分区表和引导代码共存的布局方案,它位于0号扇区。GPT是另一种更新的磁盘布局方案,全称是GUID分区表,它把分区表信息放到多个扇区,并且支持2TB以上磁盘。

在UEFI时代,GPT是主流,但MBR依然没有完全消失。你按传统BIOS模式启动时,固件还是会尝试读取0号扇区的MBR代码并跳转执行。而UEFI模式下,固件不再执行MBR代码,而是直接读取EFI系统分区里的EFI\BOOT\BOOTX64.EFI文件。所以如果你在做裸机实验,建议还是用MBR+BIOS模式,因为UEFI会绕过你的MBR代码,让你觉得“明明写对了怎么没反应”。

《操作系统真象还原》第二章写的MBR,就是传统BIOS启动路径下最原始的那段代码。你可以把它理解成“操作系统之前的第一块跳板”。它的使命不是完成所有工作,而是把后续更复杂的程序(比如加载器、内核)从磁盘调入内存,然后跳过去执行。

2.3 引导扇区结构的完整视图

一个标准的MBR扇区包含三个部分:引导代码、磁盘分区表、结束标志。

磁盘分区表从偏移量0x1BE开始,一共64字节,每16字节描述一个分区,最多记录4个主分区。结束标志是0x55AA,位于偏移量0x1FE。剩下的0到0x1BD之间大约446字节,才是引导代码的可用空间。

我们在第二章实操时,并不需要真正构建一个带分区表的MBR,因为qemu测试可以用裸镜像文件直接加载0号扇区,不关心分区表。但你要理解,真实硬盘上的MBR,那64字节分区表是不能随便覆盖的,否则磁盘就“分区丢失”了。这也是为什么很多引导程序会主动避开分区表区域的写入。

我自己实验时,用了最简单的nasm编译,将代码写到512字节镜像文件里,后两个字节手动补上0x55和0xAA。如果分区表区域是空白,也没有问题,因为BIOS不检查分区表是否有内容,只认结束标志。

3. 搭建最小开发环境:nasm + qemu

3.1 为什么选nasm和qemu

编写MBR可以使用任意一款x86汇编器,常见的有nasm、as、masm。我强烈推荐nasm,因为它的语法简洁直接,支持16位模式,而且能生成纯二进制文件(-f bin),非常适合写引导扇区。不需要链接器,不需要目标文件,直接一条命令就能输出512字节的镜像。

qemu是另一个核心工具。它是一个开源的机器模拟器,能模拟完整的x86硬件环境,包括BIOS、磁盘、显示器、键盘。你甚至可以在qemu里单步调试MBR代码,配合gdb看到CPU内部寄存器的实时变化。这比拿真实机器反复重启测试高效太多。

实验环境我建议用Linux终端,如果是在Windows上,也可以用WSL,或者直接装一个虚拟机跑Linux。MBR开发本身不依赖GUI,所以在纯命令行环境下做最舒服。

3.2 安装与验证工具链

以Ubuntu/Debian为例,安装只需要两条命令:

sudo apt update sudo apt install nasm qemu-system-x86

如果你的发行版是Fedora或CentOS,对应命令是:

sudo dnf install nasm qemu-system-x86

安装完成后,验证一下nasm版本:

nasm -v

能正常输出版本信息就说明工具链可用。qemu验证稍麻烦一些,你不需要马上加载镜像,可以先跑一个最简单的命令看看是否报错:

qemu-system-x86_64 --version

如果显示出版本,那就没问题了。

这里要提醒一个细节:我们在实验中使用的是传统BIOS启动方式,不是UEFI。qemu默认对qemu-system-x86_64用的是SeaBIOS模拟固件,它支持传统MBR启动。所以只要你的镜像文件最后有0x55AA,qemu就会尝试从它的0号扇区引导。如果你用了-bios参数指定了UEFI固件,那MBR代码就不会被执行,这一点不要搞混。

3.3 编译MBR的命令行细节

nasm编译MBR的命令非常简单:

nasm -f bin -o mbr.bin mbr.S

-f bin表示输出纯二进制,mbr.S是汇编源码。生成的mbr.bin默认不会自动补齐512字节,如果代码不足512字节,你需要手动填充到512;如果超过512字节,就会得到一个大于512字节的文件,这种文件是不能作为MBR镜像使用的。

为什么nasm不自动填充到一个扇区?因为它只是把代码按二进制打包,不会理解“这是MBR”这个语义。所以填充工作得你自己在源码里完成。最常见的做法是在汇编文件末尾用times 510-($-$$) db 0这种伪指令,把当前偏移量填充到510字节,然后写入两个字节的结束标志。

$$在nasm里表示当前段起始地址,$表示当前地址,$-$$就是代码已经占用的字节数。510减去这个数,就是要填充的0字节数量。这个技巧几乎是MBR框架的标准写法,每个手写引导程序的人都会遇到。

4. 手写第一版MBR:向屏幕输出字符

4.1 核心代码逐行拆解

现在进入正题。我们要实现的功能很简单:MBR在屏幕上打印一串字符,然后死循环在这。这个程序虽然短,但已经包含了MBR最关键的元素:实模式初始化、BIOS中断调用、字符串存储、填充逻辑。

先看完整代码:

; mbr.S ; 主引导记录:打印字符串后进入死循环 SECTION MBR vstart=0x7c00 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 ; 清屏(使用 BIOS 0x06 功能) mov ax, 0x0600 mov bx, 0x0700 mov cx, 0x0000 mov dx, 0x184f int 0x10 ; 打印字符串 mov ax, 0x1301 mov bx, 0x0002 mov cx, msg_end - msg_start mov dx, 0x0000 mov bp, msg_start int 0x10 jmp $ msg_start: db "Hello, MBR!" db 0x0d, 0x0a, "Welcome to OS world!" msg_end: times 510-($-$$) db 0 db 0x55, 0xaa

别急着抄代码,先理解每一部分为什么这样写。

SECTION MBR vstart=0x7c00是一个关键定义,vstart告诉nasm,代码的虚拟起始地址是0x7C00。这样当代码访问msg_start这样的标签时,nasm计算出的地址就是基于0x7C00的绝对地址。如果你不加vstart,标签地址可能会从0开始算,那么mov bp, msg_start就会把错误的地址传给BIOS,导致打印出乱码甚至死机。

接着是初始化寄存器。mov ax, cs然后把ax赋给ds、es、ss,目的是让数据段、附加段、堆栈段都指向当前代码段。cs的值在BIOS跳进MBR时是多少?通常是0x0000,因为BIOS会执行jmp 0x0000:0x7c00,即段地址0,偏移0x7C00。这种写法相当于把段地址统一,无论cs是多少,都能让后续的内存访问基于0x7C00进行。

mov sp, 0x7c00是设置栈顶位置。实模式下栈向下增长,栈顶设为0x7C00,那么栈区会从0x7C00往下生长,不会覆盖MBR代码。这个值其实就是一个经验值,保证栈空间够用且安全。

4.2 清屏和打印用的BIOS中断

代码里用了两次int 0x10。

第一次清屏,用的是0x06号功能,也就是“上滚窗口”。AH=0x06, AL=0表示清空整个窗口。BH=0x07是显示属性,0x07表示黑底白字。CH=0, CL=0是窗口左上角行和列,DH=0x18, DL=0x4F是右下角,0x18是24、0x4F是79,正好覆盖80x25的整个文本屏幕。

第二次打印字符串,用的是0x13号功能。AH=0x13,AL=0x01表示在写字符串的同时更新光标位置。BH=0是页码,BL=0x02是字符串属性,0x02也是绿字黑底。CX是字符串长度,DX是起始行和列,这里都是0。ES:BP指向字符串首地址,这就是为什么我们前面把ES设置为CS,然后让BP等于msg_start。

这里有个容易踩的坑:mov bp, msg_start之后,BP里的地址是基于SECTION MBR vstart=0x7c00计算出来的偏移。由于段寄存器ES的值是CS,而CS在BIOS跳入时可能是0,所以ES:BP指向的实际地址就是0:0x7C00+offset。如果你之前乱改了ES,那打印就会失败。

还有一点需要说明:0x13中断要求ES:BP指向字符串,它只认BP不认SI或DI。所以千万别写成mov si, msg_start,那样打印出来的是乱码。

4.3 编译、写入镜像、运行验证

把上面的代码保存为mbr.S,然后执行:

nasm -f bin -o mbr.bin mbr.S

查看文件大小:

ls -l mbr.bin

正常应该是512字节。然后再查看二进制内容:

xxd mbr.bin

确认最后两个字节是55 aa。

如果文件不足512字节,说明你的times填充逻辑有问题,检查$和$$的用法。如果你写的代码超过了512字节,那说明你在字符串部分放太多内容了,需要精简。

运行qemu验证:

qemu-system-x86_64 -drive file=mbr.bin,format=raw -machine pc

不出意外,你会在弹出的窗口中看到绿色的Hello, MBR!以及第二行Welcome to OS world!。程序会卡在jmp $这条死循环上,不会退出,这是对的,因为MBR的使命暂时完成了。

如果屏幕黑屏或者花屏,先别怀疑代码。可能是qemu没跑到你的MBR代码,比如镜像文件不是512字节,或者结束标志没写对。用xxd检查之后逐个排查。

5. MBR中的陷阱与调试技巧

5.1 地址计算错误是头号杀手

我修改了很多次这个实验,最终发现90%的问题都出在内存地址上。实模式的“段:偏移”双分量设计,给了程序灵活性,也带来了理解门槛。

你写的MBR代码位于内存地址0x7C00,但nasm在给你的标签生成地址时,完全取决于你写的vstart还是org。如果不加任何指示,nasm默认从0开始算,那么msg_start就是0x0013之类的偏移。此时你需要人为设置ds为0x07C0,才能让ds:si组合出0x7C13。而如果设置了vstart=0x7c00,nasm会生成0x7C13作为msg_start,此时只要段基址是0,地址就正确。

我的建议是:不要用段地址0x07C0 + 小偏移的写法,这虽然经典但容易算错。直接用vstart=0x7c00,让汇编器替你算绝对地址,然后保持DS=0、ES=0。这样所有标签的地址值都是0x7C00+offset,符合直觉。

如果你在调试时看到打印出来的字符是乱码,多半是bp或ds:bp指向了错误地址。用gdb单步调试可以确认,但最快的检查方法是临时在打印前加一条mov bx, 0x0007,用int 0x10的0x0E功能单个打印一个已知字符,比如'A',看看能否正常显示,以此判断段寄存器设置是否正确。

5.2 用qemu的调试接口查看寄存器

qemu默认启动的窗口看起来像一个黑盒子,其实它内置了调试功能,只是需要额外加参数。我们可以在启动时开启gdb服务:

qemu-system-x86_64 -drive file=mbr.bin,format=raw -machine pc -s -S

这里的-S让CPU在启动后立刻暂停,-s表示在本地1234端口开启gdb调试服务。

然后另开一个终端,执行:

gdb target remote :1234 b *0x7c00 c info registers

在gdb里打断点可以直接打在0x7C00地址上,因为这个地址是BIOS跳转到MBR的固定入口。运行到断点后,查看寄存器和内存,就能确认CS、DS等寄存器是否符合预期。

我实习时最常用的命令是x/16xb 0x7c00查看内存里的机器码是不是我们编译出来的那几条。如果机器码对不上,说明加载的不是我们的MBR。这时再检查镜像文件是不是512字节,或者结束标志是不是没有放对位置。

5.3 不要踩的坑:栈溢出与死循环

MBR代码中设置堆栈是必要的,因为BIOS中断调用会压栈。如果你不设栈,SP的值在BIOS跳转时可能是任意的,中断调用会把返回地址写到不可预测的内存位置,轻则程序崩溃,重则破坏BIOS数据。

我在默认没有设置sp的情况下第一次跑,结果打印完字符串就花了屏。后来改成mov sp, 0x7c00就好了。栈向下增长,从0x7C00往低地址写,不会影响MBR代码本身,这是一个安全的选择。

另一个坑是死循环的位置。如果你在主流程末尾写jmp $,程序会永远停留在这个循环里。如果你是写一个真实的引导程序,这个位置通常会放加载内核的跳转指令,跳到0x10000之类的地址继续执行。但在第二章的实验里,死循环反而是我们期望的终止状态,表示“引导代码已经运行完毕,进入待机”。

5.4 用org还是vstart,给你一个结论

网上很多老教程直接用org 0x7c00,这个也很好。org告诉nasm代码被加载到0x7C00,所有标签按绝对地址计算。而vstart是NASM针对“虚拟起始地址”的伪指令,它让段内偏移从0x7C00开始算,但不会在生成二进制时添加任何头部信息。二者者在纯二进制模式下效果几乎一样。

推荐用vstart=0x7c00,因为它更明确地表达“这个段的代码会被加载到0x7C00,且偏移按此计算”。它也方便你后续把同一份代码放到不同段里复用,不过MBR阶段不需要这种能力。只要你选一个,并保持一致,就不会出错。

5.5 常见问题速查表

现象可能原因排查方法
qemu黑屏镜像非512字节,或0x55AA位置错误xxd检查最后两字节,用ls -l确认大小
屏幕显示乱码ES:BP指向错误,或字符串长度不对检查vstart设置,检查CX的取值是否等于字符串实际长度
打印后死机未设置栈或栈顶不正确初始化SS和SP,设置SP为0x7C00
运行无反应qemu用了UEFI模式确认启动命令不带-biosUEFI固件,使用默认SeaBIOS
编译报错标签地址超出16位检查是否误用了32位指令,确认代码量在512字节内

6. 从打印字符串到读硬盘扇区

6.1 为什么要读硬盘

只打印一行字,MBR还没发挥真正作用。操作系统的代码不可能都塞进512字节的MBR里,所以MBR要做的事情其实是“加载后续的代码”。

最常见的设计是:MBR读取硬盘上特定扇区的内容到内存的某个位置,然后跳过去执行。这个“特定扇区”通常是2号扇区或更多连续扇区,取决于你要加载的内容大小。

BIOS为读取磁盘提供了int 0x13的0x02功能,也就是读扇区。它的输入参数很多,核心是按下表设置寄存器:

寄存器含义
AH0x02 表示读扇区
AL读多少个扇区
CH柱面号低8位
CL扇区号(位0-5)和柱面号高2位(位6-7)
DH磁头号
DL驱动器号(0x80表示第一个硬盘)
ES:BX数据缓冲区地址

这个接口是CHS寻址方式,而不是LBA。硬盘在古老的CHS模式下,用“柱面-磁头-扇区”三个值定位。它的地址转换比较绕,CL寄存器既有扇区号又有柱面号高位,非常容易写错。好消息是,很多BIOS支持扩展读功能,int 0x13的0x42号功能能直接传LBA逻辑块地址,更符合直觉。

6.2 用LBA模式读硬盘扇区

LBA读取方式需要构造一个“磁盘地址包”(Disk Address Packet, DAP),放在内存里,然后把它的地址传给DS:SI。结构体如下:

struct DAP { uint8_t size; // 16 uint8_t reserved; // 0 uint8_t count; // 扇区数 uint8_t reserved2; // 0 uint16_t buffer_offset; uint16_t buffer_segment; uint64_t lba; // 起始LBA } __attribute__((packed));

在汇编里,我们可以用times和db伪指令构造这个数据结构,然后调用int 0x13的0x42功能。

一个简单的读取示例(只展示关键部分):

mov si, dap mov ah, 0x42 mov dl, 0x80 int 0x13 jc read_error

如果进位标志被置位,表示读取失败。这时可以读取AH寄存器里的错误码来定位问题。这个错误码很有用,比如0x01表示无效命令,0x04表示扇区未找到。

在《操作系统真象还原》第二章里,作者并没有深入LBA,而是先用CHS读取。但对新手来说,直接用LBA扩展更省心,因为现代硬盘和qemu虚拟硬盘都支持LBA。你在实验时不妨直接写一个基于LBA的读扇区版本,加载一个小程序,比CHS版本更简单也更容易调试。

6.3 加载到哪个内存地址最合适

读出来的扇区不能乱放。如果放到0x7C00,就会覆盖正在运行的MBR代码,虽然你已经不再需要MBR了,但覆盖过程中可能发生指令取指错误,所以最好放到别的安全地址。

传统Boot Loader习惯把后续代码放到0x10000(64KB处)或0x1000(4KB处)。具体看你的代码量。第二章的加载器比较简单,放到0x10000是一个不会和BIOS数据区冲突的地址,同时在实模式可寻址1MB范围内。

读取完成后,直接用jmp 0x1000:0x0000跳转过去执行,段地址0x1000,偏移0。如果你的后续代码是32位代码,还需要先切换保护模式,那就复杂多了。但在第二章,加载的还是16位代码,直接跳转即可。

6.4 读盘失败的高频原因

我用qemu测试LBA读取时,遇到过最多次的问题就是dl设置错了。dl必须设为0x80表示第一块硬盘。如果你在调试时把dl设成0,BIOS会认定是软驱设备,读取结果很可能直接失败。

另外,DAP结构体必须放在某个可访问的内存地址,且保证读取期间不会被覆盖。我习惯把DAP放在MBR代码段之后的空间,比如0x7E00,这样它既在1MB内,又不会与栈冲突。要注意ds:si指向的是DAP,而不是磁盘缓冲区,很多人把这两个地址弄混,导致读出来全是0。

最后,启动qemu时指定镜像要加上format=raw,否则qemu可能按其他格式解析文件格式头,导致扇区内容错位。这一点在《操作系统真象还原》的实验里也反复提醒过。

7. 让MBR看起来更专业:字符串与状态反馈

7.1 打印加载状态信息

一段不打印任何信息的加载过程,调试起来非常痛苦。你根本不知道代码卡在哪个环节。所以我建议你在MBR里加上简单的状态打印。

比如:

  • 开机后打印[MBR]
  • 读取磁盘前打印[Loading...]
  • 读取成功打印[OK]
  • 失败打印[Fail]

实现并不复杂,写一个通用的字符串打印子程序:

print: lodsb or al, al jz .done mov ah, 0x0e int 0x10 jmp print .done: ret

这个子程序要求ds:si指向以0结尾的字符串,使用int 0x10的0x0e方式逐个输出字符。使用0结尾的好处是不用每次手动计算字符串长度。

在实模式下,函数调用没有保护现场的概念,全是裸寄存器。所以调用print之前要把si设置好,如果子程序里用了其它寄存器,你就要在调用前保存或者接受可能被修改的事实。我通常会在print内部保存要用到的寄存器:

print: push ax push si .loop: lodsb or al, al jz .done mov ah, 0x0e int 0x10 jmp .loop .done: pop si pop ax ret

这样调用方就不用担心ax和si被破坏。

7.2 扇区读取结果的可视化验证

除了打印字符串,你还可以在读取完扇区后,打印缓冲区里第一个字的十六进制值,用来验证数据对不对。但实模式下没有现成的整数转字符串函数,你需要自己写一个十六进制打印函数。

一个简单的思路:一个字节分成高4位和低4位,分别查表转成字符,然后调用int 0x10输出。代码大概长这样:

print_hex: mov cx, 4 .loop: rol dx, 4 mov al, dl and al, 0x0f add al, '0' cmp al, '9' jbe .digit add al, 'A' - '0' - 10 .digit: mov ah, 0x0e int 0x10 loop .loop ret

这种调试辅助代码虽然占空间,但能让你在裸机环境下最快定位问题。调试完再删掉也不迟。毕竟MBR只有446字节可用,放太多调试代码会影响后续功能,但这些技巧在写更大的Boot Loader时同样有效。

8. 第二章之后的路:MBR还能做什么

8.1 从MBR到加载器

《操作系统真象还原》第二章只是起步,下一章就要写加载器,把内核从硬盘读到内存,再进入保护模式。MBR只是512字节的引导点,真正的操作系统加载逻辑通常放在后续扇区。

在后续章节中,你通常会写一个名为LOADER的第二个引导程序,它被MBR读取到0x100或0x10000等地址,然后负责更多工作:A20地址线开启、GDT设置、进入保护模式、加载内核。MBR本身保持极简,这有实际原因——512字节实在写不了复杂功能,即使能硬塞进去,也会让维护变得灾难。

从第二章到第三章,你会慢慢体会到“分别加载”这种架构思路:MBR只负责加载LOADER,LOADER负责加载内核,各司其职,解耦清晰。这种结构也被真实的GRUB等Boot Loader沿用。

8.2 真机启动的不一样之处

使用qemu调试非常顺利,但你最终可能会想在一块真实主板上启动你的MBR。真机环境有更多差异:

  • 有些BIOS会校验MBR的磁盘签名(偏移0x1B8处的4字节),不合法时会拒绝引导,但实际上多数老BIOS只认0x55AA。
  • 某些主板的BIOS在引导时会修改dl,所以你的MBR代码必须把dl视为当前引导驱动器号,而不是写死0x80。
  • 实模式下硬盘可能以CHS方式访问,旧机器不支持LBA扩展,你得写两套读取方案。

如果在真实机器上测试,推荐用U盘或者SD卡,使用dd写入镜像。写入命令:

sudo dd if=mbr.bin of=/dev/sdX bs=512 count=1 conv=notrunc

把/dev/sdX换成你的U盘设备名,千万别写错。这是最经典也最危险的命令之一,搞错目标盘会清空数据。在没有十足把握之前,先用qemu模拟足够久。

8.3 书上的实验与我的扩展实践

第二章的例题一般只要求打印字符串,但我建议你把它扩展一下:

  • 增加读取硬盘扇区的功能;
  • 增加一个独立的小程序放在第2个扇区,让MBR把它加载到0x10000并打印信息;
  • 用LBA方式读扇区,对比CHS方式的复杂度。

我自己做扩展时,发现最头疼的是生成一个“多扇区镜像”。通常的做法是:单独汇编MBR生成mbr.bin,单独汇编第二段程序生成loader.bin,然后用dd把两个文件拼接成一个大镜像。拼接命令:

dd if=/dev/zero of=disk.img bs=512 count=2052 dd if=mbr.bin of=disk.img bs=512 count=1 conv=notrunc dd if=loader.bin of=disk.img bs=512 seek=1 conv=notrunc

第一条命令创建一个约1MB的空白镜像,第二条把MBR写入0号扇区,第三条把loader写到1号扇区。qemu启动时用disk.img作为磁盘。这种组合方法在后面章节里几乎是标准操作。

8.4 独立完成第二章项目的小结标准

你可以用下面几个标准检查自己是否真正掌握了第二章:

  • 能不看代码默写一个打印字符串的MBR框架,并解释每行作用;
  • 能解释为什么MBR的加载地址是0x7C00,为什么结束标志是0x55AA;
  • 能调试一个读取扇区失败的问题,而不是只会重写代码;
  • 能说明MBR和GPT引导方式的区别,以及为什么实验环境用MBR。

如果这些都能做到,那你可以放心进入第三章,开始写加载器和保护模式了。

我个人的经验是,操作系统的学习最容易卡在“代码明明能跑,但不知道它为什么能跑”的阶段。《操作系统真象还原》第二章的价值恰恰在于把最基本的启动链路掰开揉碎,让你亲眼看到自己写的代码在实模式下的运行效果。不要急着往后翻,把这一章里的每个寄存器都“亲手摸一遍”,后面才会顺。

最后再分享一个小技巧:不要把MBR实验只跑一遍就扔掉。你可以试着在打印字符串的基础上,把字符串内容改成彩色显示,或者用BIOS中断读取键盘输入,再决定是否跳转到另一个扇区的代码。这样的进阶实验会让你对中断调用的熟练度大幅提升,而这也正是后续Loader开发中最需要的技能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:30:30

Spring Initializer与Spring Boot实战:从项目生成到AI集成架构

很多人在学习 Spring Boot 时,第一步都是去 start.spring.io 或者 IDE 里点几下那个 Initializer,然后项目就生成了,看起来很简单。但我在实际带团队和写项目的过程中发现,绝大多数人对这一步的理解是严重不足的,尤其是…

作者头像 李华
网站建设 2026/9/30 8:29:11

AMHR2信号轴解析:AMH发挥作用的关键开关与实验策略

做生殖医学研究的人,对AMH(抗穆勒氏管激素)应该再熟悉不过——它是临床评估卵巢储备功能最常用的血清标志物之一,在辅助生殖门诊里几乎是必开项目。但我发现一个很有意思的现象:很多人把AMH当作一个单纯的"检测指…

作者头像 李华
网站建设 2026/9/30 8:28:13

Java微信小程序树洞论坛系统设计与实现:匿名社区核心技术解析

1. 这个树洞项目到底解决了什么问题 上个月帮一个学弟捋毕设开题报告,他拟的题目里同时出现了“Java”“微信小程序”“树洞论坛系统”这几个词组。我一看就明白,这又是一个典型的匿名社区类项目:前端做微信小程序,后端用 Java 提…

作者头像 李华
网站建设 2026/9/30 8:27:43

影刀RPA成就值监控实战:从定时采集到告警推送

1. 项目概述:为什么要用影刀去监控一个“成就值”先说结论:成就值监控这件事,听起来是个小需求,但它背后藏着一个非常典型的RPA落地场景——定时采集、状态比对、异常告警。我这次用影刀(也叫影刀AI Ware)把…

作者头像 李华
网站建设 2026/9/30 8:27:32

微电网日前经济调度Matlab实现:风光储能与需求响应联合优化

微电网日前经济调度这几年确实火,尤其是把风光储能和需求响应揉进同一个优化模型里,既能体现新能源消纳,又能展示需求侧管理的价值。我最早接触这个方向是在做园区微电网项目时,当时被“如何用Matlab快速搭建一个可复现的调度模型…

作者头像 李华
网站建设 2026/9/30 8:27:00

Ubuntu 20.04 WiFi连接故障排查与Netplan配置实战

简介:本资源是一份面向Ubuntu 20.04初学者与系统运维人员的WiFi连接实战指南,聚焦解决新装系统无WiFi图标、无法识别无线网卡等典型联网问题。内容系统梳理两种高适配性方案:一是针对Broadcom网卡缺失驱动的场景,通过有线网络安装…

作者头像 李华