ARTICLE DETAIL

资讯详情

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

从零编写MBR:汇编语言实现计算机启动引导程序

从零编写MBR:汇编语言实现计算机启动引导程序 1. 从零到一理解MBR与计算机启动的“第一声啼哭”如果你和我一样对计算机底层运作抱有强烈的好奇心那么亲手编写一个能引导计算机启动的程序无疑是极具诱惑力的第一步。《操作系统真象还原》这本书就为我们提供了这样一条清晰的路径。第二章的核心任务就是编写一个属于我们自己的MBR主引导记录。这听起来可能有点“硬核”但请相信我当你看到屏幕上跳出自己写的第一行引导信息时那种纯粹的快乐和成就感是任何高级语言“Hello World”都无法比拟的。这不仅仅是写几行汇编代码更是亲手揭开计算机启动时那层神秘面纱的过程。MBR是什么简单来说它是计算机开机后BIOS基本输入输出系统在完成自检后从硬盘的第一个扇区512字节读取并执行的第一段程序。你可以把它想象成整个操作系统的“引路人”或“启动开关”。在UEFI普及之前几乎所有基于传统BIOS的PC都依赖MBR来启动。它的结构是标准化的前446字节是引导代码紧接着是64字节的磁盘分区表DPT最后2字节是固定的魔数0x55AA作为有效MBR的签名。我们的目标就是填充那至关重要的前446字节引导代码。为什么不用C语言而是用汇编NASM因为在这个阶段CPU刚刚上电内存管理单元MMU还没初始化没有操作系统没有运行时库甚至栈指针SP都不一定可靠。我们处于一个被称为“实模式”的原始环境中能直接操作的只有CPU的寄存器和BIOS中断服务。汇编语言是唯一能在这个“蛮荒之地”精确控制每一个比特的工具。NASMNetwide Assembler是一个强大且语法清晰的开源汇编器非常适合我们的学习。2. 动手前的准备搭建你的汇编实验环境在开始敲代码之前我们需要一个干净、可控的实验环境。直接在物理机上操作MBR风险极高一个错误的字节就可能导致系统无法启动。因此使用虚拟机是绝对明智且安全的选择。2.1 工具链的选择与安装你需要准备三样核心工具汇编器NASM。负责将我们写的汇编源代码.asm编译成纯二进制机器码.bin。虚拟机Bochs 或 QEMU。用于加载和运行我们生成的二进制文件模拟整个启动过程。Bochs自带调试功能对学习尤其友好QEMU则更轻量快速。二进制查看/编辑工具可选dd,hexdump或xxd。用于验证我们生成的二进制文件是否正确写入了MBR应有的位置和内容。以在Linux如Ubuntu环境下为例安装非常简便sudo apt-get update sudo apt-get install nasm bochs bochs-sdl对于Windows用户可以去NASM和Bochs的官网下载对应的安装包。安装后确保nasm和bochs命令可以在终端或命令提示符中运行。2.2 创建你的第一个引导扇区文件首先我们创建一个简单的MBR它的唯一任务就是在屏幕上打印一个字符然后挂起。创建一个名为boot.asm的文件; boot.asm - 一个最简单的MBR雏形 org 0x7c00 ; 告诉编译器这段代码将被加载到内存地址0x7c00处。这是BIOS加载MBR的标准位置。 start: mov ax, 0xb800 ; 文本模式显示内存的段地址。在实模式下物理地址 段地址 4 偏移地址。 mov es, ax ; 将附加段寄存器ES设置为0xb800这样我们就能通过ES:DI来访问显存了。 mov di, 0 ; 将目标索引寄存器DI清零指向显存第一个字符的位置屏幕左上角。 mov byte [es:di], H ; 向ES:DI指向的内存位置即屏幕左上角写入字符‘H’的ASCII码。 mov byte [es:di1], 0x0c ; 紧接着写入属性字节0x0c表示黑底亮红色。高4位是背景低4位是前景。 halt: hlt ; 使CPU暂停执行进入低功耗状态。 jmp halt ; 无限循环跳转到halt标签确保程序停在这里。 times 510-($-$$) db 0 ; 填充指令$代表当前地址$$代表本节开始地址。 ; 510-($-$$)计算出从当前代码结束到第510字节还需要填充多少字节。 ; db 0就是用0填充这些字节。 dw 0xaa55 ; 在扇区的最后两个字节第511-512字节写入魔数0x55AA。 ; 注意x86是小端字节序所以写作dw 0xaa55。注意org 0x7c00这行指令至关重要。它并不生成任何机器码而是告诉NASM“我这段程序里的所有标号如start,halt的地址都是基于0x7c00这个基址来计算的。” 因为BIOS固定会把MBR加载到内存0x7c00处如果没有这行汇编器计算出的跳转地址将是错误的程序无法正确执行。2.3 编译与运行你的MBR保存boot.asm后打开终端进入文件所在目录执行编译nasm -f bin boot.asm -o boot.bin-f bin指定输出格式为纯二进制flat binary这正是MBR需要的格式。现在你得到了一个512字节的boot.bin文件。我们可以用dd命令把它写入一个虚拟软盘镜像或者直接让Bochs加载这个bin文件。创建一个1.44MB的软盘镜像并写入MBR# 创建一个全零的软盘镜像文件 dd if/dev/zero ofdisk.img bs512 count2880 # 将我们的boot.bin写入镜像的第一个扇区 dd ifboot.bin ofdisk.img bs512 count1 convnotruncconvnotrunc参数确保只覆盖第一个扇区而不截断整个镜像文件。最后使用Bochs来启动这个镜像bochs -f bochsrc.txt -q你需要一个简单的Bochs配置文件bochsrc.txt内容大致如下megs: 32 romimage: file/usr/share/bochs/BIOS-bochs-latest vgaromimage: file/usr/share/bochs/VGABIOS-lgpl-latest ata0-master: typedisk, pathdisk.img, modeflat boot: disk display_library: sdl运行后如果一切顺利Bochs窗口应该会打开并在屏幕左上角显示一个红色的“H”。恭喜你你的计算机“第一声啼哭”成功了3. 从打印字符到显示字符串完善MBR的引导功能仅仅打印一个字符显然不够。一个实用的MBR至少需要具备清屏、打印引导信息、以及尝试加载后续操作系统的能力。让我们来升级一下代码。3.1 利用BIOS中断显示字符串在实模式下我们无法直接调用操作系统的API因为还没有操作系统但我们可以调用BIOS预先固化在ROM中的中断服务程序Interrupt Service Routines。其中int 0x10是与视频显示相关的中断。下面是一个改进版的boot.asm它实现了清屏和打印字符串的功能org 0x7c00 start: ; 1. 清屏操作利用BIOS中断0x10功能号AH0x06 mov ax, 0x0600 ; AH0x06 表示向上滚动窗口 AL0x00 表示清空整个窗口 mov bh, 0x07 ; BH属性这里用0x07即黑底白字正常显示 mov cx, 0x0000 ; CH左上角行号(0)CL左上角列号(0) mov dx, 0x184f ; DH右下角行号(24)DL右下角列号(79)。80x25文本模式下这是全屏。 int 0x10 ; 调用BIOS视频中断 ; 2. 设置光标位置到左上角可选但更友好 mov ah, 0x02 ; AH0x02 表示设置光标位置 mov bh, 0x00 ; BH页码0 mov dh, 0x00 ; DH行号0 mov dl, 0x00 ; DL列号0 int 0x10 ; 3. 打印引导信息字符串 mov si, boot_msg ; 将字符串的地址偏移量加载到源索引寄存器SI call print_string ; 调用我们写的打印子程序 jmp $ ; 无限循环$代表当前行地址相当于原地跳转。 ; 打印以0结尾的字符串的子程序 print_string: pusha ; 保存所有通用寄存器ax, cx, dx, bx, sp, bp, si, di mov ah, 0x0e ; AH0x0e表示在TTY模式下显示字符自动推进光标 .print_loop: lodsb ; 从DS:SI加载一个字节到AL同时SI自增1。DS默认等于CS。 or al, al ; 检查AL是否为0字符串结束符 jz .print_done ; 如果是0则跳转到结束 int 0x10 ; 调用BIOS中断显示AL中的字符 jmp .print_loop ; 继续循环 .print_done: popa ; 恢复所有通用寄存器 ret ; 返回调用处 ; 数据区 boot_msg db My OS Bootloader is running..., 0x0d, 0x0a, 0 ; 字符串0x0d,0x0a是回车换行0是结束符。 times 510-($-$$) db 0 dw 0xaa55这个版本做了几件重要的事清屏通过int 0x10的0x06号功能将屏幕内容向上滚动AL0即为清空提供了一个干净的视觉起点。移动光标将光标重置到屏幕左上角使得后续输出从那里开始。模块化打印将打印字符串的功能封装成print_string子程序。这利用了int 0x10的0x0e号功能电传打字机模式它能自动处理光标前进比直接写显存更简单。lodsb指令是高效处理字符串的关键。使用数据段将字符串定义在代码之后。注意在实模式下代码段CS和数据段DS通常指向同一块内存区域因为org 0x7c00所以我们可以直接用mov si, boot_msg来获取字符串偏移地址。实操心得在编写汇编子程序时养成用pusha/popa保存和恢复现场的习惯非常重要。这能避免子程序无意中修改了调用者的寄存器值导致难以调试的错误。尤其是在引导程序这种“一锤子买卖”里任何寄存器污染都可能让程序跑飞。3.2 调试技巧当屏幕一片漆黑时怎么办第一次运行很可能不成功屏幕一片黑或者Bochs直接报错。别慌这是学习过程的一部分。首先检查编译和写入过程用hexdump -C boot.bin | less查看生成的二进制文件。重点看文件大小是不是正好512字节最后两个字节是不是55 aa小端显示为aa 55你的代码部分开头的一些字节是否和预期一致使用Bochs的内置调试器这是最强大的工具。在bochsrc.txt中增加一行magic_break: enabled1然后在代码中你想开始调试的地方比如start:标签后插入一条xchg bx, bx指令。这条指令在Bochs中被定义为“魔术断点”。运行Bochs时它会自动暂停在这里并进入调试命令行。你可以c继续执行。s单步执行一条指令。r查看寄存器状态。xp /32bx 0x7c00查看内存0x7c00处开始的32个字节。trace-reg on和trace-mem on可以开启指令和内存访问跟踪虽然输出信息量大但对理解程序流极有帮助。常见错误排查忘记org 0x7c00这会导致所有标号地址计算错误程序必然崩溃。字符串没有以0结尾print_string子程序会一直读取内存直到遇到0可能打印出一堆乱码后跑飞。段寄存器设置错误在实模式下访问内存必须通过“段:偏移”的方式。如果你直接mov [si], al而没有正确设置DS可能会写入错误的内存地址。没有正确挂起程序如果MBR执行完代码后“跑过了头”去执行后面随机的内容也会导致不可预知的行为。用jmp $或hlt循环确保它停住。4. 超越“Hello World”MBR的职责与加载操作系统的初步构想一个真正的MBR其核心职责是找到并加载活动分区中的操作系统引导程序如GRUB的stage1.5或Windows的bootmgr并将控制权交给它。我们的雏形目前只完成了“自显示”距离这个目标还有关键一步磁盘读取。4.1 利用BIOS中断读取磁盘扇区BIOS提供了int 0x13中断来访问磁盘。最常用的功能是AH0x02读扇区。在调用前我们需要设置好一系列参数; 假设我们要从硬盘读取1个扇区到内存0x8000处 mov ax, 0x8000 ; ES:BX 指向目标内存地址。ES0x800 BX0x0000 物理地址0x8000 mov es, ax mov bx, 0x0000 mov ah, 0x02 ; 功能号读扇区 mov al, 1 ; 要读取的扇区数 mov ch, 0 ; 柱面号低8位 mov cl, 2 ; 扇区号从1开始计数 mov dh, 0 ; 磁头号 mov dl, 0x80 ; 驱动器号0x00~0x7F是软盘0x80~0xFF是硬盘。0x80通常是第一块硬盘。 int 0x13 ; 调用磁盘中断 jc disk_error ; 如果进位标志CF被置1说明读取出错 cmp al, 1 ; 检查实际读取的扇区数是否与请求的AL1一致 jne disk_error ; 如果不一致也认为是错误 ; 读取成功可以跳转到0x8000处执行加载的代码 jmp 0x8000:0x0000这里有几个关键点驱动器号DL在虚拟机中我们写入的disk.img通常被识别为第一块硬盘0x80。如果你用软盘镜像可能需要设为0x00。CHS寻址这是传统的寻址方式柱面Cylinder、磁头Head、扇区Sector。注意扇区号从1开始而不是0。错误处理int 0x13执行后如果出错CF1错误代码在AH中。一个健壮的引导程序必须检查这个状态。4.2 设计一个简单的两阶段引导程序由于MBR只有512字节空间极其有限通常它只负责加载一个稍大一点的“第二阶段引导程序”比如2KB再由第二阶段程序去完成更复杂的任务如探测硬件、加载内核、进入保护模式等。我们可以这样规划MBR第一阶段存放在硬盘0面0道1扇区。它的任务是加载从第2扇区开始的N个扇区第二阶段代码到内存0x8000然后跳转过去。第二阶段引导程序存放在硬盘连续的几个扇区中。它可以使用更多空间实现更丰富的功能比如打印一个启动菜单。这就需要我们编写两个汇编文件分别编译然后合并到一个磁盘镜像中。boot.asmMBR负责加载stage2.bin而stage2.asm则可以自由地做更多事情。合并命令示例nasm -f bin boot.asm -o boot.bin nasm -f bin stage2.asm -o stage2.bin # 创建一个空白镜像 dd if/dev/zero ofos.img bs512 count2880 # 写入MBR到第一个扇区 dd ifboot.bin ofos.img bs512 count1 convnotrunc # 写入第二阶段程序从第二个扇区开始 dd ifstage2.bin ofos.img bs512 seek1 convnotruncseek1参数表示从输出文件os.img的第1个扇区之后开始写入即跳过第0扇区。4.3 从实模式到保护模式的思考前瞻虽然本章我们只接触实模式但了解MBR的最终使命很重要。现代操作系统都运行在CPU的保护模式或长模式下这些模式提供了内存保护、虚拟内存、多任务等强大功能。而MBR和早期的第二阶段引导程序都运行在实模式下。因此一个完整的引导加载程序Bootloader的关键任务之一就是从实模式切换到保护模式。这个过程涉及关闭中断。加载全局描述符表GDT。设置控制寄存器CR0的PE位保护模式使能位。进行一次长跳转来刷新CPU的指令流水线并进入保护模式代码段。这将是《操作系统真象还原》后续章节的重点内容。我们现在的MBR正是为这座大厦埋下的第一块基石。5. 常见问题与避坑指南实录在反复编写、调试MBR的过程中我踩过不少坑。这里记录一些典型问题和解决方法希望能帮你节省时间。5.1 编译与链接问题问题使用nasm -f elf编译后再用ld链接生成二进制文件结果文件巨大无法放入一个扇区。原因与解决MBR必须是纯二进制格式flat binary不能包含ELF等可执行文件格式的头部信息。务必使用nasm -f bin直接输出二进制文件。链接器ld是为构建复杂的、有多个段的程序准备的MBR这种单一、位置固定的代码不需要它。问题代码中引用了外部标签编译时报“未定义符号”错误。原因与解决确保所有用到的标号函数名、数据标签都在同一个源文件.asm中定义。MBR是自包含的不能依赖外部链接。如果代码太长可以用%include指令将其他文件包含进来但最终还是要汇编成一个文件。5.2 运行时问题问题程序运行后屏幕上显示乱码或者光标位置奇怪。排查检查显存地址文本模式下显存起始地址是0xb8000。在实模式下需要用“段:偏移”访问。mov ax, 0xb800; mov es, ax后ES:DI指向的物理地址是0xb800*16 DI。确保计算正确。检查属性字节每个显示字符在内存中占2字节低字节是ASCII码高字节是属性前景色、背景色、闪烁等。如果你只写了字符字节而属性字节是随机的就会显示为乱码。确保写入完整的字word或分别设置好两个字节。检查光标如果你用int 0x10, AH0x0e打印光标会自动前进。但如果之前用其他方式操作了显存光标可能停留在奇怪的位置。可以在打印前先用int 0x10, AH0x02重置光标。问题Bochs启动后直接重启或提示“No bootable device”。排查首要怀疑MBR签名用二进制查看工具确认你生成的boot.bin的第510、511字节是否是0x55、0xAA或0xAA55小端查看。这是BIOS判断一个扇区是否为有效引导扇区的唯一标准。检查代码入口确保你的代码逻辑正确没有在早期就执行了非法指令或访问了非法内存导致CPU触发异常重启。在Bochs调试器中单步执行观察在0x7c00处的第一条指令是什么。检查Bochs配置确认bochsrc.txt中boot: disk设置正确并且ata0-master的path指向了你正确的镜像文件。5.3 设计逻辑问题问题MBR读取第二个扇区失败。排查CHS参数确认你读取的柱面、磁头、扇区号是否正确。对于虚拟的硬盘镜像通常从CHS(0,0,2)开始是安全的。注意扇区号从1开始。内存地址冲突确保你加载第二阶段代码的目标内存区域如0x8000没有被BIOS数据区、栈或其他代码占用。实模式下0x7c00到0x7e00是MBR自身0x7e00往后通常是空闲的。0x8000即0x800:0x0000是一个常用的安全区域。磁盘中断错误码在jc disk_error后添加错误处理代码将AH中的错误码打印出来可以转换成十六进制数字显示。BIOS手册或网上可以查到int 0x13错误码的含义例如AH0x01表示非法功能号AH0x02表示找不到地址标记AH0x04表示扇区未找到等。问题第二阶段程序被执行但立刻崩溃。排查段寄存器在从MBR跳转到第二阶段程序时例如jmp 0x8000:0x0000CPU会将CS设为0x8000IP设为0x0000。你的第二阶段程序开头可能需要重新设置DS、ES等段寄存器因为它们的值可能还是MBR中设置的。一个常见的做法是在第二阶段程序开头执行mov ax, cs; mov ds, ax; mov es, ax让数据段和附加段与代码段一致。栈空间MBR中可能没有设置栈或者栈空间很小。第二阶段程序如果使用了call、push等指令需要一个合理的栈。可以在第二阶段程序初始化时设置栈指针SP例如mov sp, 0x7c00将栈设在MBR下方向低地址增长。编写MBR是一个精细活它强迫你关注每一个字节、每一个CPU状态。这个过程虽然充满挑战但每解决一个问题你对计算机启动过程的理解就加深一层。当你的代码成功在虚拟机上跑起来的那一刻你会真切地感受到自己不再是操作系统的被动使用者而是成为了那个在黑暗断电中点亮第一盏灯的人。这份雏形已显的快乐正是驱动我们继续向下探索——进入保护模式、实现内存管理、加载内核——的最原始动力。
返回列表