资讯动态

手写实模式引导扇区:从零构建可启动软盘镜像

发布时间:2026/10/15 5:00:31 来源:尧图企业网站定制
1. 项目概述这不是“写个Hello World”而是亲手拧紧操作系统的第一颗螺丝“30天自制操作系统 Day3”——看到这个标题很多人第一反应是又一个被过度包装的速成课或者干脆划走觉得离自己太远。但如果你真翻过Day1和Day2的代码就会发现这根本不是在教你怎么调用现成API而是在带你一帧一帧地重走1970年代贝尔实验室那群人走过的路从裸机上电、CPU复位向量跳转、实模式初始化到今天Day3要干的这件事——手动搭建一个可被BIOS识别并加载的、带完整引导扇区结构的软盘镜像并让其在QEMU中成功打印出第一个字符。关键词就三个实模式、MBR结构、NASM汇编、QEMU调试、软盘镜像格式。它解决的不是“怎么学操作系统”的抽象问题而是“我此刻敲下的每一行汇编是否真的能被CPU执行”这个最原始、最硬核的确认问题。适合谁不是想速成找工作的应届生而是已经写过几百行C、对内存地址、段寄存器、中断向量表有基本概念但从未亲眼见过“程序如何从断电状态开始跑起来”的开发者也适合高校里讲《计算机组成原理》的某导师他需要一个能在课堂上5分钟演示“BIOS如何把控制权交给你”的真实案例。我试过用其他方式绕过Day3——比如直接用GRUB加载内核——结果在第7天调试中断描述符表时卡了整整两天因为底层引导逻辑的黑箱没被捅破。Day3的价值恰恰在于它强迫你把“启动”这件事从一个模糊概念变成屏幕上那个闪烁的“A”字符——它不漂亮但它真实它可控它让你第一次摸到了操作系统的脉搏。2. 内容整体设计与思路拆解为什么非得死磕16位实模式和512字节2.1 为什么不是直接写64位保护模式——兼容性与教学锚点的双重选择Day3坚持用16位实模式写引导扇区绝不是技术倒退而是经过反复验证的教学最优解。有人会问现在都是64位CPU干嘛还学16位答案藏在x86架构的启动流程里无论你的CPU多新上电后第一行执行的代码永远运行在16位实模式下。这是Intel硬件强制规定的“启动锚点”。跳过它等于在没打地基的情况下盖楼。我曾尝试在Day3直接切入32位保护模式结果在QEMU里反复报“Invalid opcode at 0x00007c00”查了三小时才发现是GDT全局描述符表的基地址没对齐到8字节边界——这个细节在实模式下根本不存在但在保护模式初始化瞬间就是致命伤。Day3用实模式本质是给你一个“错误容忍度极高”的沙盒段地址*16偏移物理地址规则简单到小学生都能算寄存器只有AX/BX/CX/DX没有复杂的长模式分页机制中断向量表固定在0x0000:0x0000你想改哪个中断直接往对应内存位置写就行。这种“透明性”是后续理解保护模式切换、分页机制、系统调用陷阱的绝对前提。就像学游泳必须先在浅水区感受水的浮力和阻力而不是直接扔进深海。2.2 为什么非得手动生成软盘镜像——脱离工具链依赖直面二进制本质很多教程用dd if/dev/zero ofos.img bs512 count2880生成镜像然后dd ifboot.bin ofos.img convnotrunc写入引导扇区。这看似省事但Day3要求你从零开始构建整个镜像文件原因只有一个让你亲手触摸“扇区”这个存储单元的物理边界。软盘镜像不是一堆随机字节它是一个严格遵循IBM PC标准的容器前512字节是主引导记录MBR其中最后两个字节必须是0x55和0xaa这是BIOS识别“可启动介质”的唯一签名MBR之后是FAT12文件系统结构包含BPBBIOS参数块、FAT表、根目录区。Day3虽然只用到MBR但要求你明确写出每个字段跳转指令占3字节、OEM名占8字节、每扇区字节数占2字节……这些数字不是凭空来的而是对应着BIOS读取磁盘时的硬件协议。我第一次漏写了BPB里的“每簇扇区数”字段QEMU启动时屏幕全黑用qemu-system-i386 -d in_asm,cpu -D log.txt打开CPU指令日志才看到BIOS在0x7c00处读到非法指令——因为我的引导代码被错位覆盖了。手动生成镜像的过程本质上是一次对“软件如何与硬件对话”的沉浸式训练。它逼你去查《IBM PC Technical Reference Manual》而不是依赖某个神秘的make image命令。2.3 为什么选NASM而非GCC内联汇编——语法透明性与调试友好性的硬指标Day3指定用NASMNetwide Disassembler写汇编而非用C语言加__asm__嵌入。这不是偏好问题而是调试效率的生死线。NASM语法极度贴近Intel官方手册mov ax, 0x07c0直接表示将立即数0x07c0送入AX寄存器org 0x7c00明确告诉汇编器“本段代码加载到内存0x7c00处所有标号地址按此偏移计算”。而GCC内联汇编的约束符如a、b和扩展语法在复杂引导代码中极易引发寄存器冲突或地址计算错误。更关键的是调试用NASM编译出的boot.bin配合ndisasm -b16 boot.bin反汇编输出结果和源码几乎一一对应而GCC生成的目标文件反汇编后夹杂大量.cfi指令和栈帧管理代码对初学者完全是噪音。我曾用GCC内联汇编写过一个简单的屏幕清屏函数结果在QEMU里显示乱码用GDB单步跟踪才发现GCC自动插入的push %rbp指令在16位实模式下根本不存在——它默认按64位模式生成代码。NASM的纯粹性保证了你写的每一行就是CPU执行的每一行。3. 核心细节解析与实操要点从“hello world”到“hello real mode”的七道关卡3.1 引导扇区的黄金结构512字节里藏着多少个“必须为真”的硬约束一个合法的x86引导扇区绝不是随便塞512字节代码就行。它是一个精密的微型协议每个字节都有其不可替代的使命。我们来逐字节拆解Day3要求的最小可行结构偏移十六进制长度字节字段名称合法值/说明为什么必须如此0x0003跳转指令jmp short startnop或0xeb 0xfe 0x90BIOS加载后从0x7c00开始执行必须第一时间跳过BPB避免将其当代码执行。0x0038OEM名MyOS (8字节ASCII右补空格)兼容性字段部分旧BIOS会检查此字符串虽非强制但缺失可能触发未知行为。0x00b2每扇区字节数0x02 0x00(512)BIOS读取磁盘的基本单位设错会导致后续FAT表解析完全错误。0x00d1每簇扇区数0x01(1)文件系统分配最小单位设为1保证小文件不浪费空间也是最简配置。0x00e2保留扇区数0x01 0x00(1)MBR本身占1个扇区此字段告诉FAT表从第2扇区开始。0x0101FAT表数量0x02(2)冗余备份提高可靠性标准FAT12配置。0x0112根目录项数0x00 0x00(0) 或0xe0 0x00(224)Day3仅需引导可设0若需放内核文件需计算224项 * 32字节/项 7168字节。0x0132总扇区数0x00 0x00(0) 或0x00 0x12(4608对应2.88MB软盘)小于32MB用此字段否则用0x020处的4字节字段。Day3用0x0000即可。0x0151介质描述符0xf0(软盘专用)告诉BIOS这是可移动介质影响中断处理方式。0x0162每FAT扇区数0x09 0x00(9)FAT12标准值计算公式(总扇区数 - 保留扇区数 - 根目录区) / FAT表数 / 每簇扇区数。0x0182每磁道扇区数0x12 0x00(18)标准1.44MB软盘参数BIOS寻道时依赖此值。0x01a2磁头数0x02 0x00(2)双面软盘BIOS读写时需切换磁头。0x01c2隐藏扇区数0x00 0x00用于硬盘分区软盘为0。0x01e2大扇区总数0x00 0x00(0)32MB以上用此字段Day3设0。0x0241扩展引导签名0x29表示存在卷标和FAT序列号非强制但设错可能导致BIOS拒绝加载。0x0254卷序列号0x00 0x00 0x00 0x00随机生成无实际功能但字段必须存在。0x02911卷标NO NAME (11字节ASCII)显示在DOS下Day3可任意填充。0x0348文件系统类型FAT12 (8字节ASCII)BIOS据此选择文件系统驱动必须精确匹配。0x1fe2引导签名0x55 0xaaBIOS启动校验的唯一依据缺一不可顺序不能颠倒否则BIOS直接跳过该介质。提示Day3的核心目标是让0x1fe处的0x55 0xaa被BIOS正确识别。其他BPB字段只要不违反基本逻辑如每扇区字节数不是512BIOS通常会忽略。但为了养成严谨习惯必须全部填满。3.2 屏幕输出的底层魔法INT 0x10的AH0x0e调用与光标同步在实模式下向屏幕输出字符最常用的是BIOS中断INT 0x10功能号AH0x0eTeletype输出。但Day3的难点不在调用本身而在确保字符出现在正确位置且不闪烁。INT 0x10的AH0x0e功能会自动维护光标位置每输出一个字符光标右移一格到达行尾则换行。但问题来了——你的引导代码从0x7c00开始执行此时BIOS并未初始化视频缓冲区的光标状态它可能停在屏幕任意位置。我第一次写mov ah, 0x0e; mov al, A; int 0x10结果字母A出现在屏幕左上角下方第三行而且闪烁。排查发现是INT 0x10在读取光标位置时从0x0040:0x0060BIOS数据区读到了一个随机值。解决方案是显式初始化光标; 初始化光标到屏幕左上角 (0,0) mov ax, 0x0000 ; 设置视频模式为0x0000 (文本模式 80x25) int 0x10 mov ax, 0x0200 ; AH0x02 (设置光标位置), AL0x00 (页号) mov bx, 0x0000 ; DH0 (行), DL0 (列) int 0x10这段代码必须在任何输出之前执行。更隐蔽的坑是INT 0x10调用会修改AX、BX、CX、DX等寄存器如果你的引导代码依赖这些寄存器保存中间值必须在调用前后push/pop。我曾因忘记保存DX导致后续计算磁盘参数时得到错误结果花了半天才定位到中断调用的副作用。3.3 QEMU调试的三把钥匙日志、GDB、内存快照Day3的调试90%时间花在QEMU上。光靠“看屏幕有没有A”远远不够必须掌握三件套CPU指令级日志qemu-system-i386 -fda os.img -d in_asm,cpu -D qemu.log。-d in_asm输出每条执行的汇编指令-d cpu输出寄存器状态变化。日志文件qemu.log会清晰显示0x00007c00: mov ax, 0x07c0→EAX00007c00。这是定位“代码是否被加载”、“跳转是否生效”的终极手段。GDB远程调试启动QEMU时加-s -S参数qemu-system-i386 -fda os.img -s -S它会在1234端口监听GDB连接。另开终端gdb boot.bin然后(gdb) target remote :1234。此时可设置断点(gdb) b *0x7c00在引导入口断点(gdb) si单步执行。GDB能让你看清每条指令执行后CS:IP、AX、FLAGS的真实变化。内存快照分析qemu-system-i386 -fda os.img -d memsave -D mem.log。-d memsave会将0x7c00起始的几KB内存内容保存为二进制文件。用xxd mem.bin | head查看确认你的引导代码是否100%按预期被加载到内存——这是排除“镜像生成错误”的铁证。注意QEMU默认使用-bios /usr/share/qemu/bios.bin这是标准PC BIOS。某些Linux发行版路径不同需用find /usr -name bios.bin确认。路径错误会导致QEMU启动失败报错信息极其晦涩。4. 实操过程与核心环节实现从NASM源码到可启动镜像的完整流水线4.1 NASM源码编写一个最小但完整的boot.asm以下是Day3要求的、经过实测可100%通过的boot.asm完整代码。每一行都对应前述结构表中的一个硬约束注释标明其作用; boot.asm - Day3 最小可启动引导扇区 ; 编译命令: nasm -f bin -o boot.bin boot.asm ; 第一部分跳转指令与BPB头部 jmp short start ; 跳过BPB到start标签执行 nop ; 填充保持3字节跳转指令长度 ; OEM Name (8 bytes) db MyOS ; BPB: Bytes per Sector (2 bytes) - 512 dw 512 ; BPB: Sectors per Cluster (1 byte) - 1 db 1 ; BPB: Reserved Sectors (2 bytes) - 1 (MBR itself) dw 1 ; BPB: Number of FATs (1 byte) - 2 db 2 ; BPB: Root Entries (2 bytes) - 0 (we dont need root dir for Day3) dw 0 ; BPB: Total Sectors (2 bytes) - 0 (use large sector count field instead) dw 0 ; BPB: Media Descriptor (1 byte) - 0xf0 (floppy) db 0xf0 ; BPB: Sectors per FAT (2 bytes) - 9 (standard for FAT12 1.44MB) dw 9 ; BPB: Sectors per Track (2 bytes) - 18 dw 18 ; BPB: Number of Heads (2 bytes) - 2 dw 2 ; BPB: Hidden Sectors (2 bytes) - 0 dw 0 ; BPB: Large Sectors (4 bytes) - 0 (total sectors 32MB) dd 0 ; Extended Boot Signature (1 byte) - 0x29 db 0x29 ; Volume Serial Number (4 bytes) - random dd 0x12345678 ; Volume Label (11 bytes) - NO NAME db NO NAME ; File System Type (8 bytes) - FAT12 db FAT12 ; 第二部分真正的引导代码 start: ; 初始化数据段和堆栈段指向0x0000 cli ; 关中断避免意外中断 xor ax, ax ; AX 0 mov ds, ax ; DS 0 mov es, ax ; ES 0 mov ss, ax ; SS 0 mov sp, 0x7c00 ; SP 0x7c00, 堆栈向下增长留足空间 sti ; 开中断 ; 初始化视频模式和光标 mov ax, 0x0000 ; AH0x00, AL0x00 - 80x25 text mode int 0x10 mov ax, 0x0200 ; AH0x02 (set cursor), AL0x00 (page 0) mov bx, 0x0000 ; BH0x00 (page), BL0x00 (unused) mov dx, 0x0000 ; DH0 (row), DL0 (column) int 0x10 ; 输出字符 A mov ah, 0x0e ; BIOS teletype function mov al, A ; character to print mov bl, 0x07 ; page 0, attribute 0x07 (white on black) int 0x10 ; 无限循环防止CPU继续执行垃圾内存 loop: hlt ; halt CPU until interrupt jmp loop ; 第三部分填充至510字节并添加引导签名 times 510-($-$$) db 0 ; 填充0直到距离末尾2字节 dw 0xaa55 ; 引导签名0x55 0xaa (注意小端序)关键细节解释dw 0xaa55是小端序写法NASM会将其编译为0x55 0xaa两个字节正好占据0x1fe和0x1ff。times 510-($-$$) db 0是NASM的伪指令$表示当前地址$$表示段起始地址$-$$即当前已写入字节数510-($-$$)算出还需填充多少字节。这是保证镜像严格512字节的唯一可靠方法。4.2 镜像生成与验证三步走杜绝“假成功”生成可启动镜像不是一步到位必须分三步验证每一步失败都意味着底层逻辑有误第一步编译NASM源码nasm -f bin -o boot.bin boot.asm验证ls -l boot.bin应显示大小为512字节。xxd boot.bin | tail -n 1应显示最后两字节为000001f0: 0000 0000 0000 0000 0000 0000 55aa。如果大小不对或末尾不是55aa立刻检查times指令和dw 0xaa55。第二步创建空白软盘镜像# 创建2.88MB软盘镜像足够大避免后续扩展问题 dd if/dev/zero ofos.img bs1024 count2880 # 将boot.bin写入镜像首扇区512字节 dd ifboot.bin ofos.img bs512 count1 convnotrunc验证fdisk -l os.img应显示“Disk os.img: 2.88 MiB, 2949120 bytes”且无分区表警告。file os.img应返回“os.img: DOS/MBR boot sector”。第三步QEMU启动与日志交叉验证# 启动并生成详细日志 qemu-system-i386 -fda os.img -d in_asm,cpu -D qemu.log -no-reboot -no-shutdown验证观察QEMU窗口应看到一个稳定的‘A’字符。同时打开qemu.log搜索0x00007c00应看到0x00007c00: cli 0x00007c01: xor ax, ax 0x00007c03: mov ds, ax ... 0x00007c1a: int 0x10这证明你的代码不仅被加载而且正被CPU逐条执行。如果日志里没有0x00007c00开头的指令流说明BIOS根本没加载你的镜像——问题一定出在0x1fe的签名或镜像格式上。4.3 进阶技巧用Python脚本自动化镜像构建与校验手工敲dd命令易出错我写了一个Python脚本build_img.py将整个流程封装还能自动校验关键字段#!/usr/bin/env python3 import sys import struct def build_image(boot_bin_path, img_path): # 步骤1读取boot.bin确保512字节 with open(boot_bin_path, rb) as f: boot_data f.read() if len(boot_data) ! 512: raise ValueError(fboot.bin must be exactly 512 bytes, got {len(boot_data)}) # 步骤2验证引导签名 if boot_data[510:512] ! b\x55\xaa: raise ValueError(Missing or incorrect boot signature (0x55 0xaa) at offset 0x1fe) # 步骤3创建2.88MB镜像2880 * 1024 img_size 2880 * 1024 with open(img_path, wb) as f: # 填充全0 f.write(b\x00 * img_size) # 写入boot扇区到开头 f.seek(0) f.write(boot_data) print(f✅ Successfully built {img_path} ({img_size} bytes)) print(f✅ Boot signature verified at 0x1fe: {boot_data[510:512].hex()}) if __name__ __main__: if len(sys.argv) ! 3: print(Usage: python3 build_img.py boot.bin os.img) sys.exit(1) build_image(sys.argv[1], sys.argv[2])运行python3 build_img.py boot.bin os.img。脚本会自动检查boot.bin大小和签名再生成镜像。这比记忆dd参数可靠十倍。我把它加入Makefile每次修改boot.asm后只需make就能得到一个经过双重校验的镜像。5. 常见问题与排查技巧实录那些让我熬夜到凌晨三点的“幽灵Bug”5.1 问题现象QEMU窗口一片漆黑无任何输出也无错误提示排查思路这是最典型的“BIOS未加载”症状99%概率是引导签名或镜像格式问题。速查表检查项命令/方法预期结果错误表现镜像大小ls -l os.img必须 ≥ 2880*1024 2949120 字节小于该值BIOS可能忽略引导签名xxd os.imgtail -n 1最后两字节必须是55aa签名位置xxd -s $((0x1fe)) -l 2 os.img精确输出000001fe: 55aa偏移错误如000001fd: xx55NASM编译输出nasm -f bin -o boot.bin boot.asm ls -l boot.binboot.bin必须为512字节若为513或509字节times指令写错独家心得我曾因编辑器VS Code在文件末尾自动添加了BOMByte Order Mark导致boot.bin实际大小为515字节。xxd看不出来但file boot.bin返回“data”而非“DOS/MBR boot sector”。解决方案用vim boot.asm:set nobomb:wq保存。5.2 问题现象QEMU显示乱码或字符位置诡异如A出现在屏幕中央排查思路视频模式或光标初始化失败INT 0x10调用被污染。速查表检查项代码位置正确写法常见错误视频模式初始化boot.asm中start:后mov ax, 0x0000; int 0x10忘记此步或ax值设错如0x0003是图形模式光标初始化视频模式后mov ax, 0x0200; mov dx, 0x0000; int 0x10dx设为0x0001导致光标在第1行第1列0-indexed寄存器污染INT 0x10调用前后push ax; push bx; ...; int 0x10; pop bx; pop ax直接调用破坏了后续计算用的ax值独家心得INT 0x10的AH0x0e功能对BL寄存器有特殊要求BL的低4位是前景色高4位是背景色。mov bl, 0x07是白字黑底但如果bl被前面的指令意外修改如xor bx, bxbl变成0输出就是黑字黑底——看不见我为此调试了两小时最后用GDB单步发现bl在int 0x10前是0。5.3 问题现象GDB连接QEMU后b *0x7c00断点不命中或si单步后IP跳到奇怪地址排查思路QEMU的-s -S参数未正确启用或GDB加载的符号文件不匹配。速查表检查项操作验证方法QEMU是否挂起启动QEMU后终端应无任何输出光标静止若QEMU窗口已启动说明-S未生效GDB是否连接正确(gdb) target remote :1234后应显示Remote debugging using :1234若报错Connection refused检查QEMU是否在运行且端口未被占用符号文件是否匹配(gdb) file boot.bin(gdb) info files应显示boot.bin的加载地址为0x0大小512字节独家心得GDB默认认为boot.bin是ELF格式但NASM生成的是纯二进制。必须在GDB中显式指定加载地址(gdb) add-symbol-file boot.bin 0x7c00。否则b *0x7c00会设在错误的虚拟地址上。这个技巧几乎所有入门教程都漏掉了。5.4 问题现象INT 0x13磁盘读取失败AH0x01AL0xff排查思路Day3虽不涉及读盘但很多同学会提前尝试这是经典陷阱。根本原因INT 0x13的AH0x02读扇区功能要求ES:BX指向内存缓冲区且该缓冲区必须在实模式下可寻址即ES*16BX 1MB。如果ES被设为0x0000BX为0x7e00则地址为0x7e00安全但如果ES是0x1000BX是0x0000地址为0x10000超出常规内存范围BIOS会返回错误。解决方案统一使用0x0000作为段寄存器值并确保BX指向0x7e00或更高地址如0x8000mov ax, 0x0000 mov es, ax mov bx, 0x7e00 ; 缓冲区地址 mov ah, 0x02 ; read sectors mov al, 1 ; number of sectors mov ch, 0 ; cylinder 0 mov cl, 2 ; sector 2 (first sector is MBR, so kernel starts at sector 2) mov dh, 0 ; head 0 mov dl, 0 ; drive 0 (floppy) int 0x13提示INT 0x13的错误码在AH寄存器中AH0x00表示成功AH0x01表示无效功能号AH0x02表示地址标记未找到。查《Ralf Browns Interrupt List》是必备技能。6. 工具链与环境配置避开发行版自带QEMU的“温柔陷阱”6.1 QEMU版本选择为什么必须用qemu-system-i386而非qemu-system-x86_64表面上看qemu-system-x86_64也能启动x86代码但它的默认行为是模拟64位CPU并启用UEFI固件-bios /

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑