手写 MBR 引导程序,实模式下的磁盘读写与 Loader 加载
计算机上电后,CPU 初始模式是实模式,此时地址宽度为 20 位,即最大地址空间 1MB。这 1MB 空间的划分是固定的,每一块都有规定的用途,被映射到不同的设备上:
| 起始 | 结束 | 大小 | 用途 |
|---|---|---|---|
FFFF0 | FFFFF | 16 B | BIOS 入口地址,此地址也属于 BIOS 代码,这 16 字节的内容用于执行跳转指令 |
F0000 | FFFEF | 64KB-16B | 系统 BIOS 的地址范围实际上是 F000-FFFFF,上面是入口地址,所以单独列出 |
C8000 | EFFFF | 160KB | 映射硬件适配器的 ROM 或内存映射式 I/O |
C0000 | C7FFF | 32KB | 显示适配器 BIOS |
B8000 | BFFFF | 32KB | 文本模式显示适配器 |
B0000 | B7FFF | 32KB | 黑白显示适配器 |
A0000 | AFFFF | 64KB | 彩色显示适配器 |
9FC00 | 9F000 | 1KB | EBDA(Extended BIOS Data Area) 扩展 BIOS 数据区 |
7E00 | 9FBFF | ≈608KB | 可用区域 |
7C00 | 7DFF | 512B | MBR 被 BIOS 加载区域 |
500 | 7BFF | ≈30KB | 可用区域 |
400 | 4FF | 256B | BIOS Data Area |
000 | 3FF | 1KB | Interrupt Vector Table 中断向量表 |
从上一章节了解了 BIOS 的基本流程后,现在可以开始实际编码工作。
环境准备
在 Windows 环境下,可以使用 virtualbox 作为实验环境。 在 Linux 环境下,可以使用 QEMU 系工具作为实验环境。
需要的其他工具:
- nasm(汇编器)
- dd(写入镜像)
- hexdump(检查校验虚拟磁盘内容)
开始编码 MBR
现在编写的是电脑加电后运行的第一段代码,可以分三个阶段完成这个目标:
- 打印 Hello MBR 的引导制作
- 支持读写磁盘内容的引导制作
- 完整的可从磁盘中加载内核的 MBR
规定实模式下的可用区域内存使用:
- 将 0x7C00 以下的所有可用区域作为程序堆栈区
- 0x8000 往后的 4KB 空间作为 loader 区(后续章节使用)
从 Hello MBR 开始
首先创建一个 mbr.S 文件:
bits 16org 0x7C00
start: call clear_screen ; 调用清屏函数 mov si, msg call print_string jmp $ ; 无限循环
; ------------------------------; 清屏 + 重置光标函数; ------------------------------clear_screen: ; 清屏 mov ax, 0x0600 ; AH=06h 滚屏,AL=0 清屏 mov bh, 0x07 ; 属性(白字黑底) mov cx, 0x0000 ; 左上角 (0,0) mov dx, 0x184F ; 右下角 (24,79) int 0x10
; 重置光标到左上角 mov ah, 0x02 ; 设置光标位置 mov bh, 0x00 ; 显示页0 mov dh, 0x00 ; 行=0 mov dl, 0x00 ; 列=0 int 0x10 ret
; ------------------------------; 打印字符串函数; DS:SI 指向字符串,以 0 结尾; ------------------------------print_string: lodsb ; 取下一个字符到 AL or al, al ; 判断是否为 0 jz .done mov ah, 0x0E ; BIOS 打印字符 mov bh, 0x00 ; 显示页0 int 0x10 jmp print_string.done: ret
; ------------------------------; 数据区; ------------------------------msg db "hello mbr", 0
; ------------------------------; 填充 & MBR 签名; ------------------------------times 510 - ($ - $$) db 0dw 0xAA55通过 nasm 将它编译成二进制文件:
nasm mbr.S然后需要将编译好的 Hello MBR 二进制写入虚拟磁盘,以便虚拟机加载和执行。
对于 Windows 用户,可以通过下面的命令创建一个固定大小的虚拟磁盘:
$ diskpart$ create vdisk file = C:\hello.vhd maximum = 10 type=fixed对于 Linux 用户,可以通过 qemu-img 命令创建固定大小的虚拟磁盘:
$ qemu-img create -f vpc -o subformat=fixed hello.vhd 10M这里使用固定大小的虚拟磁盘是因为动态大小的虚拟磁盘文件在头部和尾部都会存在一些元信息,使用 dd 命令写入二进制会破坏虚拟磁盘格式,而固定大小的虚拟磁盘只有尾部有元信息。可以通过形如
hexdump -C hello.vhd的命令查看虚拟磁盘的十六进制内容,观察是否包含头尾元信息。
然后通过 dd 命令将二进制写入虚拟磁盘:
# if 表示输入# of 表示输出# bs 表示读写块大小# count 表示复制的块数量# conv=notrunc 表示不截断清除后面的内容$ dd if=mbr of=hello.vhd bs=512 count=1 conv=notrunc最后通过 hexdump 命令查看写入后的虚拟磁盘内容:
$ hexdump -C hello.vhd00000000 b8 00 06 b7 07 b9 00 00 ba 4f 18 cd 10 b4 02 b7 |.........O......|00000010 00 b6 00 b2 00 cd 10 be 2d 7c e8 02 00 eb fe ac |........-|......|00000020 08 c0 74 08 b4 0e b7 00 cd 10 eb f3 c3 48 45 4c |..t..........HEL|00000030 4c 4f 2c 20 4d 42 52 21 00 00 00 00 00 00 00 00 |LO, MBR!........|00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|00000200 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*00a07000 63 6f 6e 65 63 74 69 78 00 00 00 02 00 01 00 00 |conectix........|00a07010 ff ff ff ff ff ff ff ff 30 07 aa 84 71 65 6d 75 |........0...qemu|00a07020 00 05 00 03 57 69 32 6b 00 00 00 00 00 a0 70 00 |....Wi2k......p.|00a07030 00 00 00 00 00 a0 70 00 01 2e 04 11 00 00 00 02 |......p.........|00a07040 ff ff e5 ff bc 12 f4 ed 1f b5 42 cb ac 47 5b 16 |..........B..G[.|00a07050 90 36 6c 9a 00 00 00 00 00 00 00 00 00 00 00 00 |.6l.............|00a07060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*00a07200可以看到前面的内容即是通过 dd 写入的 loader 二进制,aa55 后面星号后面的内容则是虚拟磁盘的尾部元信息。
最后通过虚拟机程序加载虚拟硬盘,即可看到最终效果。
对于 Windows 用户,可以通过 VirtualBox 新建虚拟机,类型和版本都选 other,最后虚拟硬盘选择制作的 hello.vhd,即可进行开机查看效果:

对于 Linux 用户,可以通过下面的命令查看效果:
$ qemu-system-i386 -drive file=hello.vhd,format=vpc -nographic正常情况下,控制台会输出字符串:
HELLO, MBR!注:qemu-system-i386 使用 -nographic 选项后,是通过串口重定向(ttyS0)到终端的,无图形界面。所以无法直接通过写显存(B800)的方式打印字符。若想避免这一限制,可以使用
-vnc :0选项替代,但需要下载 realvnc 客户端充当显示器。
支持读写磁盘内容的引导制作
上一章节成功地让虚拟机加载并执行了我们编写的第一段代码。MBR(主引导扇区记录)是计算机开机后系统自动访问的第一个扇区程序,由于这段程序大小有限,通常需要从其他扇区读取更复杂的引导程序加载到内存中运行。因此第二步需要通过 MBR 读取磁盘其他扇区的内容。
MBR 被加载到 0x7C00 的位置,这次的任务是从磁盘中加载另一段程序(引导或内核)到内存。首先需要规划实模式下的内存使用。根据上图的可用区域,可以选择将 loader 加载到 0x8000 的位置。
本章不负责实现具体的 Loader,所以当前实现的 loader.S 程序仅实现打印任务:
bits 16org 0x8000
mov ax, 0xb800 ;显存位置 mov es, ax
mov byte[es: 0x00], 'L' mov byte[es: 0x01], 0x07 mov byte[es: 0x02], 'O' mov byte[es: 0x03], 0x06 mov byte[es: 0x04], 'A' mov byte[es: 0x05], 0x07 mov byte[es: 0x06], 'D' mov byte[es: 0x07], 0x06 mov byte[es: 0x08], 'E' mov byte[es: 0x09], 0x07 mov byte[es: 0x0a], 'R' mov byte[es: 0x0b], 0x06 jmp $将这个程序通过 nasm 编译后写入磁盘的第二个扇区。现在开始改造 mbr.S,使其支持加载磁盘的第二个扇区并将 loader 程序载入内存运行:
bits 16org 0x7c00
LOADER_BASE_ADDR equ 0x8000 ; loader 所处的段地址LOADER_SECTORS equ 8 ; 需要读取的扇区数
; 初始化寄存器 mov ax, cs mov ds, ax mov ss, ax mov fs, ax
; 将 0x7c00 以下的区域作为栈 mov sp, 0x7c00 ; 显存段位置 mov ax, 0xb800 mov gs, ax
; 清空屏幕 call cls
; 输出 mov byte [gs: 0x00], 'M' mov byte [gs: 0x01], 0x0f
mov byte [gs: 0x02], 'B' mov byte [gs: 0x03], 0x0f
mov byte [gs: 0x04], 'R' mov byte [gs: 0x05], 0x0f
;读loader mov dh, LOADER_SECTORS ; loader 扇区数,读8个扇区共4K空间 mov dl, 0x80 ; 表示第一个磁盘 mov bx, LOADER_BASE_ADDR ; loader 的地址 ; es 段赋0 mov ax, 0 mov es, ax call disk_load jmp 0:LOADER_BASE_ADDR ;跳转至 loader,也就是 0x8000
; 清屏,并将屏幕设置为黑底白字cls: mov ax, 0x0600 ; ah=06h(滚动窗口功能), al=00h(清空整个窗口) mov bx, 0x0700 ; bh=07h(空白行的显示属性: 黑底白字) mov cx, 0 ; ch=00h(左上角行号=0), cl=00h(左上角列号=0) mov dx, 184fh ; dh=18h(右下角行号=24), dl=4fh(右下角列号=79) int 10h ; 调用bios中断 ; 重置光标到 (0,0) mov ah, 0x02 xor dx, dx ; DH=0, DL=0 mov bh, 0 int 0x10 ret
; load 'dh' sectors from drive 'dl' into es:bxdisk_load: pusha ; reading from disk requires setting specific values in all registers ; so we will overwrite our input parameters from 'dx'. let's save it ; to the stack for later use. push dx
mov ah, 0x02 ; ah <- int 0x13 function. 0x02 = 'read' mov al, dh ; al <- number of sectors to read (0x01 .. 0x80) mov cl, 0x02 ; cl <- sector (0x01 .. 0x11) ; 0x01 is our boot sector, 0x02 is the first 'available' sector mov ch, 0x00 ; ch <- cylinder (0x0 .. 0x3ff, upper 2 bits in 'cl') ; dl <- drive number. our caller sets it as a parameter and gets it from bios ; (0 = floppy, 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) mov dh, 0x00 ; dh <- head number (0x0 .. 0xf)
; [es:bx] <- pointer to buffer where the data will be stored ; caller sets it up for us, and it is actually the standard location for int 13h int 0x13 ; bios interrupt jc disk_error ; if error (stored in the carry bit)
pop dx cmp al, dh ; bios also sets 'al' to the # of sectors read. compare it. jne sectors_error popa ret
disk_error: mov bx, disk_error_msg call print call print_nl mov dh, ah ; ah = error code, dl = disk drive that dropped the error call print_hex ; check out the code at http://stanislavs.org/helppc/int_13-1.html jmp disk_loop
sectors_error: mov bx, sectors_error_msg call print
disk_loop: jmp $
disk_error_msg: db "disk read error", 0sectors_error_msg: db "incorrect number of sectors read", 0
; 打印函数print: pushaprint_start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done
; the part where we print with the bios help mov ah, 0x0e int 0x10 ; 'al' already contains the char
; increment pointer and do next loop add bx, 1 jmp print_start
done: popa ret
; 打印换行函数print_nl: pusha
mov ah, 0x0e mov al, 0x0a ; newline int 0x10 mov al, 0x0d ; carriage return int 0x10
popa ret
; receiving the data in 'dx'; For the examples we'll assume that we're called with dx=0x1234print_hex: pusha
mov cx, 0 ; our index variable
; Strategy: get the last char of 'dx', then convert to ASCII; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N.; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40; Then, move the ASCII byte to the correct position on the resulting stringhex_loop: cmp cx, 4 ; loop 4 times je end
; 1. convert last char of 'dx' to ascii mov ax, dx ; we will use 'ax' as our working register and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros add al, 0x30 ; add 0x30 to N to convert it to ASCII "N" cmp al, 0x39 ; if > 9, add extra 8 to represent 'A' to 'F' jle step2 add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7
step2: ; 2. get the correct position of the string to place our ASCII char ; bx <- base address + string length - index of char mov bx, hex_out + 5 ; base + length sub bx, cx ; our index variable mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx' ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234
; increment index and loop add cx, 1 jmp hex_loop
end: ; prepare the parameter and call the function ; remember that print receives parameters in 'bx' mov bx, hex_out call print
popa ret
hex_out: db '0x0000',0 ; reserve memory for our new string
; 签名 times 510 - ($ - $$) db 0 db 0x55, 0xaa以上代码实现了 MBR 从磁盘 2 号扇区开始加载 8 个扇区的内容到内存 0x8000,并跳转到 0x8000 开始运行。通过下面的命令制作虚拟磁盘并将 mbr 和 loader 程序放到 0 扇区和 1 扇区:
$ nasm mbr.S$ nasm loader.S$ qemu-img create -f vpc -o subformat=fixed loader.vhd 10M$ dd if=mbr of=loader.vhd bs=512 count=1 conv=notrunc$ dd if=loader of=loader.vhd bs=512 seek=1 conv=notrunc,sync$ hexdump -C loader.vhd00000000 8c c8 8e d8 8e d0 8e e0 b8 00 00 8e c0 bc 00 7c |...............||00000010 b8 00 b8 8e e8 e8 33 00 65 c6 06 00 00 4d 65 c6 |......3.e....Me.|00000020 06 01 00 0f 65 c6 06 02 00 42 65 c6 06 03 00 0f |....e....Be.....|00000030 06 04 00 52 65 c6 06 05 00 0f b6 08 b2 80 |e....Re.........|00000040 bb 00 80 e8 14 00 ea 00 80 00 00 b8 00 06 bb 00 |................|00000050 07 b9 00 00 ba 4f 18 cd 10 c3 60 52 b4 02 88 f0 |.....O....`R....|00000060 b1 02 b5 00 b6 00 cd 13 72 07 5a 38 f0 75 12 61 |........r.Z8.u.a|00000070 c3 bb 89 7c e8 43 00 e8 52 00 88 e6 e8 5a 00 eb |...|.C..R....Z..|00000080 06 bb 99 7c e8 33 00 eb fe 64 69 73 6b 20 72 65 |...|.3...disk re|00000090 61 64 20 65 72 72 6f 72 00 69 6e 63 6f 72 72 65 |ad error.incorre|000000a0 63 74 20 6e 75 6d 62 65 72 20 6f 66 20 73 65 63 |ct number of sec|000000b0 74 6f 72 73 20 72 65 61 64 00 60 8a 07 3c 00 74 |tors read.`..<.t|000000c0 09 b4 0e cd 10 83 c3 01 eb f1 61 c3 60 b4 0e b0 |..........a.`...|000000d0 0a cd 10 b0 0d cd 10 61 c3 60 b9 00 00 83 f9 04 |.......a.`......|000000e0 74 1c 89 d0 83 e0 0f 04 30 3c 39 7e 02 04 07 bb |t.......0<9~....|000000f0 0b 7d 29 cb 88 07 c1 ca 04 83 c1 01 eb df bb 06 |.}).............|00000100 7d e8 b6 ff 61 c3 30 78 30 30 30 30 00 00 00 00 |}...a.0x0000....|00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|00000200 bd 00 7c 89 ec b8 00 b8 8e c0 26 c6 06 00 00 4c |..|.......&....L|00000210 26 c6 06 01 00 07 26 c6 06 02 00 4f 26 c6 06 03 |&.....&....O&...|00000220 00 06 26 c6 06 04 00 41 26 c6 06 05 00 07 26 c6 |..&....A&.....&.|00000230 06 06 00 44 26 c6 06 07 00 06 26 c6 06 08 00 45 |...D&.....&....E|00000240 26 c6 06 09 00 07 26 c6 06 0a 00 52 26 c6 06 0b |&.....&....R&...|00000250 00 06 bb 97 80 e8 02 00 eb fe 60 8a 07 3c 00 74 |..........`..<.t|00000260 09 b4 0e cd 10 83 c3 01 eb f1 61 c3 60 b4 0e b0 |..........a.`...|00000270 0a cd 10 b0 0d cd 10 61 c3 00 00 00 00 00 00 00 |.......a........|00000280 00 ff ff 00 00 00 9a cf 00 ff ff 00 00 00 92 cf |................|00000290 00 17 00 79 80 00 00 53 74 61 72 74 65 64 20 69 |...y...Started i|000002a0 6e 20 31 36 2d 62 69 74 20 72 65 61 6c 20 6d 6f |n 16-bit real mo|000002b0 64 65 00 4c 6f 61 64 65 64 20 33 32 2d 62 69 74 |de.Loaded 32-bit|000002c0 20 70 72 6f 74 65 63 74 65 64 20 6d 6f 64 65 00 | protected mode.|000002d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*00000400 bd 00 7c 89 ec b8 00 b8 8e c0 26 c6 06 00 00 4c |..|.......&....L|00000410 26 c6 06 01 00 07 26 c6 06 02 00 4f 26 c6 06 03 |&.....&....O&...|00000420 00 06 26 c6 06 04 00 41 26 c6 06 05 00 07 26 c6 |..&....A&.....&.|00000430 06 06 00 44 26 c6 06 07 00 06 26 c6 06 08 00 45 |...D&.....&....E|00000440 26 c6 06 09 00 07 26 c6 06 0a 00 52 26 c6 06 0b |&.....&....R&...|00000450 00 06 bb 97 80 e8 02 00 eb fe 60 8a 07 3c 00 74 |..........`..<.t|00000460 09 b4 0e cd 10 83 c3 01 eb f1 61 c3 60 b4 0e b0 |..........a.`...|00000470 0a cd 10 b0 0d cd 10 61 c3 00 00 00 00 00 00 00 |.......a........|00000480 00 ff ff 00 00 00 9a cf 00 ff ff 00 00 00 92 cf |................|00000490 00 17 00 79 80 00 00 53 74 61 72 74 65 64 20 69 |...y...Started i|000004a0 6e 20 31 36 2d 62 69 74 20 72 65 61 6c 20 6d 6f |n 16-bit real mo|000004b0 64 65 00 4c 6f 61 64 65 64 20 33 32 2d 62 69 74 |de.Loaded 32-bit|000004c0 20 70 72 6f 74 65 63 74 65 64 20 6d 6f 64 65 00 | protected mode.|000004d0 c3 01 83 c2 02 eb ed 61 c3 ed 61 c3 00 00 00 00 |.......a..a.....|000004e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*00a07000 63 6f 6e 65 63 74 69 78 00 00 00 02 00 01 00 00 |conectix........|00a07010 ff ff ff ff ff ff ff ff 30 17 8c 04 71 65 6d 75 |........0...qemu|00a07020 00 05 00 03 57 69 32 6b 00 00 00 00 00 a0 70 00 |....Wi2k......p.|00a07030 00 00 00 00 00 a0 70 00 01 2e 04 11 00 00 00 02 |......p.........|00a07040 ff ff e5 37 98 ac aa 27 8b fe 42 d2 96 88 78 f4 |...7...'..B...x.|00a07050 62 ed 38 53 00 00 00 00 00 00 00 00 00 00 00 00 |b.8S............|00a07060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|*00a07200
$ qemu-system-i386 -drive file=loader.vhd,format=vpc -vnc :1最终效果如下图所示:

至此,一个运行在实模式下并且支持从磁盘读取 4KB 大小 Loader 的 MBR 程序编码完成,接下来的任务是完善这个 Loader,为加载和运行内核做好准备。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






