ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

手写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字节为什么是0x7C00MBR不是一段任意大小的代码它必须放在硬盘的第一个扇区也就是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文件。所以如果你在做裸机实验建议还是用MBRBIOS模式因为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 qemu3.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启动。所以只要你的镜像文件最后有0x55AAqemu就会尝试从它的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 vstart0x7c00 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 vstart0x7c00是一个关键定义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号功能也就是“上滚窗口”。AH0x06, AL0表示清空整个窗口。BH0x07是显示属性0x07表示黑底白字。CH0, CL0是窗口左上角行和列DH0x18, DL0x4F是右下角0x18是24、0x4F是79正好覆盖80x25的整个文本屏幕。第二次打印字符串用的是0x13号功能。AH0x13AL0x01表示在写字符串的同时更新光标位置。BH0是页码BL0x02是字符串属性0x02也是绿字黑底。CX是字符串长度DX是起始行和列这里都是0。ES:BP指向字符串首地址这就是为什么我们前面把ES设置为CS然后让BP等于msg_start。这里有个容易踩的坑mov bp, msg_start之后BP里的地址是基于SECTION MBR vstart0x7c00计算出来的偏移。由于段寄存器ES的值是CS而CS在BIOS跳入时可能是0所以ES:BP指向的实际地址就是0:0x7C00offset。如果你之前乱改了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 filembr.bin,formatraw -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。而如果设置了vstart0x7c00nasm会生成0x7C13作为msg_start此时只要段基址是0地址就正确。我的建议是不要用段地址0x07C0 小偏移的写法这虽然经典但容易算错。直接用vstart0x7c00让汇编器替你算绝对地址然后保持DS0、ES0。这样所有标签的地址值都是0x7C00offset符合直觉。如果你在调试时看到打印出来的字符是乱码多半是bp或ds:bp指向了错误地址。用gdb单步调试可以确认但最快的检查方法是临时在打印前加一条mov bx, 0x0007用int 0x10的0x0E功能单个打印一个已知字符比如A看看能否正常显示以此判断段寄存器设置是否正确。5.2 用qemu的调试接口查看寄存器qemu默认启动的窗口看起来像一个黑盒子其实它内置了调试功能只是需要额外加参数。我们可以在启动时开启gdb服务qemu-system-x86_64 -drive filembr.bin,formatraw -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开始算但不会在生成二进制时添加任何头部信息。二者者在纯二进制模式下效果几乎一样。推荐用vstart0x7c00因为它更明确地表达“这个段的代码会被加载到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-7DH磁头号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习惯把后续代码放到0x1000064KB处或0x10004KB处。具体看你的代码量。第二章的加载器比较简单放到0x10000是一个不会和BIOS数据区冲突的地址同时在实模式可寻址1MB范围内。读取完成后直接用jmp 0x1000:0x0000跳转过去执行段地址0x1000偏移0。如果你的后续代码是32位代码还需要先切换保护模式那就复杂多了。但在第二章加载的还是16位代码直接跳转即可。6.4 读盘失败的高频原因我用qemu测试LBA读取时遇到过最多次的问题就是dl设置错了。dl必须设为0x80表示第一块硬盘。如果你在调试时把dl设成0BIOS会认定是软驱设备读取结果很可能直接失败。另外DAP结构体必须放在某个可访问的内存地址且保证读取期间不会被覆盖。我习惯把DAP放在MBR代码段之后的空间比如0x7E00这样它既在1MB内又不会与栈冲突。要注意ds:si指向的是DAP而不是磁盘缓冲区很多人把这两个地址弄混导致读出来全是0。最后启动qemu时指定镜像要加上formatraw否则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只负责加载LOADERLOADER负责加载内核各司其职解耦清晰。这种结构也被真实的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 ifmbr.bin of/dev/sdX bs512 count1 convnotrunc把/dev/sdX换成你的U盘设备名千万别写错。这是最经典也最危险的命令之一搞错目标盘会清空数据。在没有十足把握之前先用qemu模拟足够久。8.3 书上的实验与我的扩展实践第二章的例题一般只要求打印字符串但我建议你把它扩展一下增加读取硬盘扇区的功能增加一个独立的小程序放在第2个扇区让MBR把它加载到0x10000并打印信息用LBA方式读扇区对比CHS方式的复杂度。我自己做扩展时发现最头疼的是生成一个“多扇区镜像”。通常的做法是单独汇编MBR生成mbr.bin单独汇编第二段程序生成loader.bin然后用dd把两个文件拼接成一个大镜像。拼接命令dd if/dev/zero ofdisk.img bs512 count2052 dd ifmbr.bin ofdisk.img bs512 count1 convnotrunc dd ifloader.bin ofdisk.img bs512 seek1 convnotrunc第一条命令创建一个约1MB的空白镜像第二条把MBR写入0号扇区第三条把loader写到1号扇区。qemu启动时用disk.img作为磁盘。这种组合方法在后面章节里几乎是标准操作。8.4 独立完成第二章项目的小结标准你可以用下面几个标准检查自己是否真正掌握了第二章能不看代码默写一个打印字符串的MBR框架并解释每行作用能解释为什么MBR的加载地址是0x7C00为什么结束标志是0x55AA能调试一个读取扇区失败的问题而不是只会重写代码能说明MBR和GPT引导方式的区别以及为什么实验环境用MBR。如果这些都能做到那你可以放心进入第三章开始写加载器和保护模式了。我个人的经验是操作系统的学习最容易卡在“代码明明能跑但不知道它为什么能跑”的阶段。《操作系统真象还原》第二章的价值恰恰在于把最基本的启动链路掰开揉碎让你亲眼看到自己写的代码在实模式下的运行效果。不要急着往后翻把这一章里的每个寄存器都“亲手摸一遍”后面才会顺。最后再分享一个小技巧不要把MBR实验只跑一遍就扔掉。你可以试着在打印字符串的基础上把字符串内容改成彩色显示或者用BIOS中断读取键盘输入再决定是否跳转到另一个扇区的代码。这样的进阶实验会让你对中断调用的熟练度大幅提升而这也正是后续Loader开发中最需要的技能。
返回列表