ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从MBR到GPT:手写主引导记录,掌控计算机启动第一棒

从MBR到GPT:手写主引导记录,掌控计算机启动第一棒 大学时第一次读到《操作系统真象还原》第二章看到“编写MBR主引导记录让我们开始掌权”这句话时我整个人是有点被击中感觉的。因为在之前所有编程学习里我们写的东西都跑在操作系统之上你调用printf、你new一个对象、你处理异常所有动作背后都有人替你兜底那个人就是操作系统。但这章不一样MBR是计算机通电后、操作系统接管之前第一段完全由你控制的代码。你写它不是在一个成熟的系统里“做应用”而是亲手从BIOS手里接过机器的控制权——这就是“掌权”两个字的真实分量。这篇文章我就沿着《操作系统真象还原》第二章的主线把MBR从原理到实操完整捋一遍。你会看到我用的最小可运行代码、怎么编译成512字节的引导扇区、怎么让Bochs把它跑起来以及我当年在这个环节踩过的几个典型坑。除了书本上的内容我还会延伸聊聊MBR和GPT分区的区别以及为什么现在装Windows 11必须要GPT——这些热搜问题其实都指向同一个底层知识点。适合正在啃这本书的读者也适合对“电脑开机后到底发生了什么”感到好奇的人。1. 为什么说第二章是“掌权”的开始而不是从内核开始1.1 计算机通电后到见操作系统的“换乘时刻表”先把整个启动链条放在桌面上。按下电源键之后CPU首先进入的是实模式CS:IP被硬件强制设置为0xF000:0xFFF0这个地址对应BIOS固件中的复位入口。BIOS做完POST上电自检、初始化关键硬件、建立中断向量表之后会做一件决定后续命运的事读取启动介质的第一扇区也就是0号扇区偏移0处加载到内存0x7C00处然后跳过去执行。这一跳就是“掌权”的交接仪式。BIOS把控制权交给你写的MBR了。之后屏幕怎么显示、磁盘怎么读写、CPU往哪执行全是你的代码说了算。哪怕你写的是疯狂往显存里刷乱码的垃圾代码机器也会照做。这种“没有人在你上面兜底”的感觉第一次体会时很微妙它既是自由的也是需要负责任的。1.2 为什么第一站是MBR而不是直接写内核很多人会问既然是学操作系统为什么不直接从内核开始这就要回到MBR在整个体系里的生态位。MBR是启动链上的第一棒它足够小、足够底层、也足够“脏”但它恰恰是理解操作系统一切后续机制的钥匙。你写内核首先要面对内存分段、分页、中断、特权级这些复杂话题这些东西在没有一个能跑起来的环境之前全都是抽象概念学了也记不牢。但MBR不同。它的目标非常具体在屏幕上打出一行字符或者从磁盘再加载一段代码然后跳过去。你马上能看到反馈。而且MBR工作在一个相当原始的环境里16位实模式、没有操作系统服务、能用的只有BIOS中断。这种“原始感”会让你被迫去搞清楚寄存器、内存地址、段地址这些最底层的机制而这些恰恰是后面一切的基础。等你在MBR上把“从磁盘加载代码到内存并跳转”这件事做明白了后面写Loader、进保护模式、开启分页都是一个循序渐进的自然延伸。《操作系统真象还原》把“编写MBR”放在第二章也正是这个用意先让你掌一次权感受一下这台机器的脾气后续才有资格去改造它。所以这一章不是热身而是整个操作系统的“任职培训”。2. 动手前的准备工作汇编器、虚拟磁盘和模拟器选型2.1 NASM、Bochs、dd这套组合拳怎么配写MBR没有任何操作系统库可用只能用汇编。汇编器我推荐NASM原因是语法简单适合教学可以自由控制section的虚拟起始地址vstart这对引导扇区至关重要跨平台Windows/Linux/macOS都能装。MASM和GAS也不是不行但MASM太绑定Windows生态GAS的ATT语法对于刚接触底层的人来说可读性差很多。既然是学习选最顺手、最容易出结果的工具就好。模拟器我强烈建议Bochs而不是QEMU。Bochs的调试能力是专门为操作系统开发设计的它自带一个调试器可以单步执行、查看寄存器、查看内存、设置断点甚至能反汇编。你在写MBR、Loader、内核的过程中90%以上的“为什么没生效”问题都需要靠调试器去看实时的CPU状态而QEMU虽然启动快但调试体验远不如Bochs直观。官方提供的bochsdbg版本就是带调试器的。另外还要准备一个磁盘镜像写入工具Linux/macOS下直接用ddWindows下可以用dd的移植版或者winhex手工写入。我个人经验是完全不用纠结这个因为虚拟磁盘创建好了之后Bochs自带一个叫做bximage的小工具可以交互式创建镜像文件后面的写入动作直接dd命令完成非常顺手。2.2 创建虚拟磁盘的完整过程在Linux或者macOS终端里我按这样的顺序操作。首先创建虚拟磁盘# 交互式创建按提示选择 # 1. 选择创建硬盘镜像hd # 2. 模式选择 flat # 3. 大小填 10 即可单位MB也可以输入60等 # 4. 问你镜像文件名时直接回车默认 disk.img bximagebximage完成后会在当前目录生成一个disk.img文件这就是待会儿要写入MBR的虚拟磁盘。如果你更习惯命令行一步到位也可以用dd先创建一个固定大小的空文件# 创建一个10MB的空白磁盘镜像 dd if/dev/zero ofdisk.img bs512 count20480这两种方式效果本质一样。唯一要记住的是这是“裸”磁盘镜像里面没有文件系统、没有任何分区表待会儿我们直接把MBR写到它的第一个扇区。不要用真实U盘或者真实硬盘来做这个实验。我在学习阶段见过有人图省事直接拿U盘试结果写错位置整个U盘的分区表被覆盖数据全没了这是一个完全可以通过虚拟化规避的风险没必要以身试法。3. 手写最简MBR每一行汇编都在干什么3.1 最小可运行代码512字节的“掌权仪式”为了不被书里的loader加载、磁盘读取等逻辑干扰我写了一个最精简的MBR。它的目标只有一个在屏幕上打印一行MBR is running...然后原地死循环。这份代码不是原书代码的堆砌而是我自己重写的最小教学版逻辑更短方便逐行拆解。; ; 最简MBR打印一行字符后自旋 ; 编译nasm -o mbr.bin mbr.asm ; 产物恰好512字节 ; SECTION MBR vstart0x7c00 ; 初始化段寄存器和数据段 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov fs, ax mov sp, 0x7c00 ; 清屏BIOS 0x10中断功能号0x06 mov ax, 0x0600 mov bx, 0x0700 xor cx, cx mov dx, 0x184f int 0x10 ; 打印字符串BIOS 0x10中断功能号0x13 mov ax, 0x1301 mov bx, 0x000f mov cx, msg_len mov bp, msg int 0x10 ; 主循环 jmp $ msg db MBR is running..., 0x0d, 0x0a msg_len equ $ - msg ; 填充空白直到第510字节 times 510 - ($ - $$) db 0 ; 最后两个字节是引导签名 db 0x55, 0xaa3.2 寄存器初始化为什么每个段寄存器都要手动设一遍BIOS跳进MBR时CPU处于16位实模式。此时段寄存器的值并不一定符合我们的预期不同的BIOS实现可能把CS设置成0x0000也可能设置成0x07C0对应物理地址都是0x7C00——0x07C0 4 0x0000 和 0x0000 4 0x7C00 是等价的。但DS、ES、SS这些段寄存器是残留状态根本不可控。所以在做任何内存访问之前先把段寄存器统一设置好是最基础也最容易漏掉的一步。mov ax, cs这一行尤其关键。我们让数据段、附加段、栈段都跟随代码段走这样本文件里的标签地址比如msg才能被正确解析访问。因为NASM在编译时会把msg的偏移地址直接编码到指令里而CPU是根据DS:偏移来寻址数据的如果DS的值跟CS不一致访问到的就是内存中完全错误的位置。这也是很多初学者犯了半天错找不到原因的地方。栈指针sp设置为0x7c00也有讲究。栈在x86里是向下增长的MBR通常只有这一个扇区向上还有后续可能加载的Loader和数据向下则是BIOS的数据区。把栈顶设在0x7C00意味着栈从0x7C00往下生长正好落在MBR自己占据的内存之下互不干扰。这个地址在操作系统启动早期是一块被约定俗成使用的“无主之地”大家写引导代码时都默认这么干已成惯例。3.3 清屏和打印BIOS中断是唯一的系统调用在实模式下我们没有操作系统服务可依赖但BIOS固件给我们预留了一组中断调用接口这就是16位汇编时代的“系统调用”。这里我用了两个功能号清屏用的是int 0x10的0x06号功能它按指定窗口滚动屏幕。AL0x06表示向上滚动AL0才是清屏但滚动整个窗口的效果等价于清屏。BH0x07是空白行属性黑底白字CH0, CL0, DH24, DL79定义了滚动区域为整个屏幕。所以mov ax, 0x0600实际是AH6, AL0清屏配合mov dx, 0x184fDH0x18即24DL0x4f即79区域设为全屏。打印字符串则是int 0x10的0x13号功能。AL0x01表示打印完字符串后自动移动光标到末尾BH0x00是页码BL0x0f是字符属性0x0f即黑底白字CX传入字符串长度DX是起始光标位置不过我在这个代码没显式设置DX所以它会沿用清屏后的默认值通常是左上角。BP指向字符串首地址注意这里用的是BP而不是SI这是0x13号功能自己的约定别记错了。3.4 512字节和0x55AA签名BIOS凭什么认你最后两行是引导扇区的最核心硬性规定times 510 - ($ - $$) db 0 db 0x55, 0xaatimes 510 - ($ - $$) db 0的意思是从当前汇编位置填充0直到文件总长度为510字节。$是当前行的汇编地址$$是当前section的起始地址两者相减就是已经生成的字节数。这样就保证了前510字节被填满。0x55, 0xaa就是大名鼎鼎的引导签名。BIOS在检查一个扇区是不是合法引导扇区时只看最后两个字节是不是0x55AA小端序写入即字节顺序是0x55低字节在前、0xAA高字节在后文件末尾呈现55 AA。如果是BIOS就认为这个介质是可引导的加载它并跳转执行如果不是BIOS会报出“Missing operating system”或者“Boot failed”之类的错误。这里有一个经常被问到的误区为什么必须是0x55AA这个约定来自IBM PC最初的设计0x55和0xAA是交替的位模式01010101b和10101010b如果总线和数据线之间存在短路或断路读取这组字节天然容易暴露硬件问题——因为相邻位全是相反电平。这算是那个硬件可靠性远不如今天的年代留下的“硬件自检式设计”一直沿用到今天。说白了这既是一个规范也是一个经验。4. 编译、写盘、启动从bin文件到Bochs屏幕上的字符4.1 编译和检查产出必须是512字节一个都不能多在终端里执行nasm -o mbr.bin mbr.asm ls -l mbr.bin如果一切正常mbr.bin的大小应该是512字节。如果不是512字节说明你的代码不小心超过了510字节或者填充公式写错了。这里有一个我在早期经常做的事用xxd查看文件末尾确认最后两个字节确实是aa55注意xxd输出里小端序显示为aa55但实际上文件字节是55 aa这两个说法是一回事因为文件按字节存储时0x55在前0xAA在后内存中按字读取才是0xAA55。xxd mbr.bin | tail -1 # 输出类似00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa看到结尾55aa就可以放心进入下一步了。没看到的话先回头检查填充公式或者签名是否有拼写错误。4.2 写入虚拟磁盘dd命令里最容易忽略的notrunc将编译好的二进制写入disk.img的第一个扇区dd ifmbr.bin ofdisk.img bs512 count1 convnotrunc这行命令的意思是从mbr.bin读取数据每次读写512字节只操作1块写到disk.img并且带上convnotrunc。notrunc这个参数非常容易忽略我当年就在这栽过。如果不加notruncdd在Linux上默认会在写完后把输出文件截断到写入结束的位置也就是说整个disk.img会被截成512字节文件系统结构全没了Bochs启动时会因为镜像太小或者格式异常而报错。notrunc就是告诉dd写完指定的块之后不要把目标文件截断。如果你用的是10MB的磁盘镜像加上这个参数磁盘还是10MB只有前512字节被覆盖了。4.3 Bochs配置文件每个参数都关系到能否启动在disk.img旁边新建一个文件bochsrc内容如下# Bochs启动配置 memory: guest64, host64 # 从磁盘启动 boot: disk # 配置ATA通道0的主盘为我们的虚拟磁盘 ata0: enabled1, ioaddr10x1f0, ioaddr20x3f0, irq14 ata0-master: typedisk, pathdisk.img, modeflat, cylinders20, heads16, spt63 # 显示输出 display_library: sdl # 启用调试器如果你用的是bochsdbg # gdbstub: enabled1, port1234, text_base0, data_base0, bss_base0逐个看关键项memory: guest64给虚拟机分配64MB内存MBR阶段用不了多少但后续章节写内核时会用到更大的内存这里设一个合理值即可。boot: disk告诉Bochs启动介质是磁盘不是软盘也不是光驱。如果这一项写错Bochs会尝试从其他介质启动自然看不到效果。ata0-master这是虚拟磁盘的挂载位置。typedisk表示这块盘是硬盘path指向我们创建的镜像文件modeflat表示这个镜像是一个裸的、无任何额外头信息的平铺文件cylinders/heads/spt这三个几何参数是我用bximage创建10MB磁盘时它自动计算出来的几何值不同大小的磁盘这三个值不一样原样复制需要匹配你的实际情况。display_library: sdl图形显示库Bochs用它弹出窗口展示屏幕效果。如果SDL没装好也可以换成x或gtk等取决于你的平台。启动命令bochs -f bochsrc如果用的是bochsdbg启动后会先进入调试器界面在调试提示符下输入ccontinue的缩写再回车虚拟机才会继续运行。这时Bochs窗口弹出你会看到屏幕左上角显示一行MBR is running...这就说明你的MBR已经被BIOS正确识别、加载并执行了。第一次看到这行字的时候那种“机器真的在跑我的代码”的实感是后面写多少应用都换不来的。5. 踩坑实录MBR没跑起来时我是怎么排查的5.1 报错“Boot failed: not a bootable disk”的完整排查链路这个报错是新手最常碰到的。它的直接含义是BIOS从磁盘第一个扇区读回了512字节但最后两个字节不是0x55AA。可问题是你明明已经在代码里写了db 0x55, 0xaa啊。这里我建议按顺序排查第一步确认编译产物。ls -l mbr.bin看大小是否512字节xxd看末尾是否是55aa。这一步能排除绝大多数编译期问题。第二步确认写入位置。MBR是磁盘的0号扇区也就是最开头512字节。dd指令要用bs512 count1不能写错偏移量。很多人用dd ifmbr.bin ofdisk.img却忘记了bs512 count1结果MBR整个被写到磁盘开头如果文件大小超过512字节就会覆盖后面的扇区而BIOS读取的0号扇区内容就乱了。第三步确认磁盘前512字节确实是你想要的内容。可以这样检查dd ifdisk.img bs512 count1 | xxd如果输出末尾能看到55aa说明数据和签名都在如果看不到那就是前面两步出了问题。第四步也是容易被忽略的确认Bochs启动时载入的是你刚写的那个磁盘。Bochs配置里path要写对如果写成了别的旧镜像文件当然还是启动失败。这一步属于“关掉电脑发现插错U盘”的级别但真发生的时候很容易让人怀疑人生。5.2 屏幕上没字符但程序确实运行了还有一种情况Bochs不报错窗口也弹出来了但屏幕上什么都不显示。这个坑比较隐蔽它通常不是MBR逻辑错误而是字符串打印前后状态不对。我遇到过的情况是mov bp, msg写成了mov bp, 0x7c00msg。因为MBR被加载到0x7c00如果NASM编译时用了vstart0x7c00那msg标签本身已经被计算成物理地址了0x7c00 文件内偏移再加一遍就错到天边去了。判断方法是在Bochs调试器里用x /16bx 0x7c00offset查看内存里字符串到底在不在再看bp寄存器的值和它是否一致。另一个常见原因是字符属性问题。如果BL里的属性是0x00全黑那字符确实打印了但是在黑底上显示黑色字肉眼完全看不见。所以调试时优先把属性设为0x0f黑底白字别用花里胡哨的颜色先确保能看见再说。5.3 用Bochs调试器单步看CPU状态比瞎猜快十倍这是我最想让你养成习惯的一步。Bochs自带的调试器可以让你逐条执行指令观察每条指令执行前后寄存器和内存的变化。我在调试MBR时最常用的三个命令# 反汇编当前指令 u /10 # 查看寄存器 info registers # 查看物理内存比如MBR开头 x /16bx 0x7c00当你怀疑“代码到底走没走到这一步”“内存数据对不对”时直接停下来看比盯着屏幕猜原因高效太多了。这个习惯在你后面写Loader、进保护模式、开分页时几乎可以救命因为越底层的错误越不会给你打印日志唯一的真相就在寄存器里。5.4 一个经典误区MBR和DOS版本、操作系统无关热词里有个“bios mbr 使用的dos版本”这个问题我见过不少人问。答案很简单MBR的引导机制是固件层面的约定和DOS、Windows、Linux都无关。一个磁盘能不能引导BIOS只认最后一个扇区签名0x55AA以及前446字节引导代码是否完成了它该做的事。至于这段引导代码之后又跳进DOS还是跳进Linux那是MBR自己的逻辑决定的。换句话说MBR不是DOS的专属产物DOS只是历史上第一个广泛使用这种引导方式的操作系统之一。你自己写的这段MBR照样可以加载Linux内核如果能写完后边章节那些代码的话。所以以后别再被“DOS版本”这个说法绕进去了写MBR的时候你根本不关心宿主操作系统是谁你只关心BIOS这一层协议。6. 从MBR到GPT分区表演进和Win11为什么必须GPT6.1 MBR和GPT分区究竟差在哪里这一节的源头其实还是MBR本身的一个结构性短板。当年IBM PC设计MBR时磁盘最大也就几十MB没人想到它要承载几TB的硬盘。但半个世纪过去了MBR分区的限制就非常明显了。我整理一张表直接对比两代分区方案的差异对比项MBRGPT发展年份1983年前后2000年代中期伴随UEFI推广分区表位置固定在磁盘第一个扇区0号扇区磁盘头部LBA1和尾部各存一份副本分区数量最多4个主分区或3主1扩展理论上128个Windows默认可扩展更多单盘最大容量约2TB因为分区表项里的起始LBA是32位约9.4ZB使用64位LBA引导方式BIOS固件 最后一个扇区签名0x55AAUEFI固件 ESP分区FAT格式里的.efi引导程序安全性无校验数据损坏无感知有CRC32校验损坏可发现、可恢复兼容性几乎所有x86设备都认老机器首选需要主板支持UEFI老BIOS机器不认为什么MBR有2TB限制因为MBR分区表每项用32位来表示起始扇区号2的32次方乘以512字节每扇区正好是2TB。超过2TB的磁盘MBR根本表达不了它的容量。而GPT的分区表项使用64位LBA这个上限在可预见的未来完全够用。6.2 为什么Windows 11安装时强制要求GPT如果你近期装过Windows 11肯定见过安装程序报“这台电脑无法运行Windows 11”或者“必须在GPT分区上安装”的提示。这不是微软拍脑袋的规定而是安全模型升级的结果。Windows 11要求UEFI安全启动Secure Boot和GPT分区搭配TPM 2.0。这里的安全逻辑是开机后UEFI固件只执行带有合法数字签名的引导程序引导程序再去验证内核签名形成一条从固件到操作系统的信任链。而MBR引导模式做不到这一点因为BIOS跳进MBR时没有任何签名验证一份恶意代码只要放在引导扇区里就能在操作系统启动之前获得执行权这在“引导攻击”面前等于城门大开。GPT本身是一种更现代的分区布局也天然地为UEFI时代的引导方式提供了土壤。GPT磁盘上会有一个专门的EFI System PartitionESP格式是FAT里面存放bootx64.efi这样的引导程序文件。UEFI固件直接从这个分区读取并执行引导程序不再依赖“固定512字节固定地址0x7C00”这种脆弱的老约定。6.3 学会MBR之后再看GPT底层逻辑并没有变很多人一听到GPT、UEFI就觉得是完全陌生的另一套体系但其实你学会MBR之后再去看GPT会发现底层思维完全可以平移。MBR的思维是固件从一个固定位置读取固定大小的代码放到固定地址然后跳过去。GPT/UEFI的思维是固件从一个文件系统里读取一个文件放到内存然后跳过去。本质都是“固件把控制权交给外部代码”只是载体从“裸扇区”变成了“文件”校验从“没有校验”变成了“数字签名校验”舞台从“16位实模式”进化到了“32位/64位保护模式”。你在MBR阶段练会的“如何从0x7C00开始分析代码”“如何看寄存器状态”“如何理解段地址和偏移地址”这些能力在调试UEFI引导程序时照样用得上只不过把0x7c00换成了某个由LoadImage分配的缓冲区地址而已。对你眼下学操作系统来说我还是建议老老实实把MBR这条老路走一遍。因为它足够简单没有任何文件系统、没有安全协议、没有驱动依赖代码写得对不对一眼就能看明白。先把这种“裸奔式”引导跑通再去看UEFI/GPT那一大堆规范你会发现它不过是在这套老逻辑上罩了一层新的壳。我在带别人学这部分内容时最常说的一句话是MBR是一个时代的遗迹但它同时是所有现代引导机制的“教科书实现”。把它吃透了你在看任何操作系统启动流程类的内容时都会有天然的方向感。最后分享一个实操小技巧在Bochs里跑MBR时别一上来就c让它飞跑。养成先用sb 0x7c00在0x7c00设断点再c的习惯这样Bochs会在MBR第一条指令处停下来之后你可以用n单步执行一条一条观察代码是怎么走完清屏、打印、自旋全过程的。我第一次完整单步走完这段代码时对“程序执行到底是怎么一回事”的理解深度比我之前写几千行C语言都管用。这一章的代码量不大但只要你有耐心调一遍后面所有章节的调试思路都会顺很多。
返回列表