mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1838 字
5 分钟
手写 MBR 引导程序:实模式磁盘读写与 Loader 加载
2024-03-11

手写 MBR 引导程序,实模式下的磁盘读写与 Loader 加载#

计算机上电后,CPU 初始模式是实模式,此时地址宽度为 20 位,即最大地址空间 1MB。这 1MB 空间的划分是固定的,每一块都有规定的用途,被映射到不同的设备上:

起始结束大小用途
FFFF0FFFFF16 BBIOS 入口地址,此地址也属于 BIOS 代码,这 16 字节的内容用于执行跳转指令
F0000FFFEF64KB-16B系统 BIOS 的地址范围实际上是 F000-FFFFF,上面是入口地址,所以单独列出
C8000EFFFF160KB映射硬件适配器的 ROM 或内存映射式 I/O
C0000C7FFF32KB显示适配器 BIOS
B8000BFFFF32KB文本模式显示适配器
B0000B7FFF32KB黑白显示适配器
A0000AFFFF64KB彩色显示适配器
9F0009FC001KBEBDA(Extended BIOS Data Area) 扩展 BIOS 数据区
7E009FBFF≈608KB可用区域
7C007DFF512BMBRBIOS 加载区域
5007BFF≈30KB可用区域
4004FF256BBIOS Data Area
0003FF1KBInterrupt Vector Table 中断向量表
Note

为什么 MBR 被加载到 0x7C00?这是 IBM PC 5150 BIOS 的历史遗留约定。当年 DOS 1.0 需要 32KB 内存运行,0x7C00 正好是 32KB 减去 512 字节(MBR 大小)再减去 16 字节(BIOS 参数块)的位置。所有 x86 BIOS 兼容实现都沿用了这个地址,否则现有的操作系统和引导程序将无法启动。

这张表直接影响后续的内存规划。0x0000x3FF 是中断向量表,任何中断调用都依赖它,绝对不能覆盖;0x4000x4FF 存储 BIOS 内部数据;0xA0000 以上被显存和各种 ROM 占据。真正可供 MBR 和 Loader 使用的安全区域只有两块:0x5000x7BFF(约 30KB)和 0x7E000x9FBFF(约 608KB)。MBR 自身被加载到 0x7C00,占用了其中 512 字节,所以 0x7C00 以下的区域可以安全地用作栈空间,而 0x7E00 以上则有充足的空间放置 Loader 和内核。

环境准备#

实验环境推荐使用 Linux + QEMU,Windows 用户可以使用 VirtualBox。此外需要 nasm(汇编器)、dd(写入镜像)和 hexdump(检查虚拟磁盘内容)三个工具。

开始编码 MBR#

现在编写的是电脑加电后运行的第一段代码。MBR 只有 512 字节,其中最后两个字节还必须留作引导签名 0xAA55,实际可用空间仅 510 字节。这点空间不可能容纳任何有意义的操作系统代码,所以 MBR 的唯一职责就是从磁盘读取更大的 Loader 程序到内存,然后把控制权交给它。整个实现分三步推进:先让 MBR 能在屏幕上打印字符,再加入磁盘读取功能,最后完成 Loader 加载与跳转。

内存规划方面,将 0x7C00 以下的区域作为栈空间(栈从高地址向低地址增长,MBR 在 0x7C00,栈顶设在 0x7C00 正好向下延伸不冲突),Loader 放在 0x8000,紧挨 MBR 之后,中间隔 512 字节的间隙,既不会覆盖 MBR 自身,又留出了栈的余量。

从 Hello MBR 开始#

首先创建一个 mbr.S 文件:

bits 16
org 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 0
dw 0xAA55

通过 nasm 将它编译成二进制文件:

nasm mbr.S

然后需要将编译好的 Hello MBR 二进制写入虚拟磁盘,以便虚拟机加载和执行。

# Windows
$ diskpart
$ create vdisk file = C:\hello.vhd maximum = 10 type=fixed
# Linux
$ qemu-img create -f vpc -o subformat=fixed hello.vhd 10M
Note

这里使用固定大小的虚拟磁盘是因为动态大小的虚拟磁盘文件在头部和尾部都会存在一些元信息,使用 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.vhd
00000000 e8 08 00 be 31 7c e8 1a 00 eb fe b8 00 06 b7 07 |....1|..........|
00000010 b9 00 00 ba 4f 18 cd 10 b4 02 b7 00 b6 00 b2 00 |....O...........|
00000020 cd 10 c3 ac 08 c0 74 08 b4 0e b7 00 cd 10 eb f3 |......t.........|
00000030 c3 68 65 6c 6c 6f 20 6d 62 72 00 00 00 00 00 00 |.hello 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 31 df 9c 37 71 65 6d 75 |........1..7qemu|
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 e7 91 cb 36 5a 89 14 05 4c 0b b2 28 90 f6 |.....6Z...L..(..|
00a07050 00 8c 12 5e 00 00 00 00 00 00 00 00 00 00 00 00 |...^............|
00a07060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00a07200

可以看到前面的内容即是通过 dd 写入的 mbr 二进制,55 aa 是 MBR 引导签名,后面星号后面的内容则是虚拟磁盘的尾部元信息(conectix 是 VHD 格式的固定标识)。

Windows 用户可能需要从 dd.exe 下载 dd 工具,从 hexdump 下载 hexdump 工具。

最后通过虚拟机程序加载虚拟硬盘,即可看到最终效果。

对于 Windows 用户,可以通过 VirtualBox 新建虚拟机,类型和版本都选 other,最后虚拟硬盘选择制作的 hello.vhd,即可进行开机查看效果: HelloMBR-for-virtualbox

对于 Linux 用户,可以通过下面的命令查看效果:

$ qemu-system-i386 -drive file=hello.vhd,format=vpc -nographic

正常情况下,控制台会输出字符串:

hello mbr
Note

qemu-system-i386 使用 -nographic 选项后,是通过串口重定向(ttyS0)到终端的,无图形界面。所以无法直接通过写显存(B800)的方式打印字符。若想避免这一限制,可以使用 -vnc :0 选项替代,但需要下载 realvnc 客户端充当显示器。

支持读写磁盘内容的引导制作#

上一章节成功地让虚拟机加载并执行了我们编写的第一段代码。MBR(主引导扇区记录)是计算机开机后系统自动访问的第一个扇区程序,由于这段程序大小有限,通常需要从其他扇区读取更复杂的引导程序加载到内存中运行。因此第二步需要通过 MBR 读取磁盘其他扇区的内容。

MBR 被加载到 0x7C00 的位置,这次的任务是从磁盘中加载另一段程序(引导或内核)到内存。首先需要规划实模式下的内存使用。根据上图的可用区域,可以选择将 loader 加载到 0x8000 的位置。

本章不负责实现具体的 Loader,所以当前实现的 loader.S 程序仅实现打印任务:

bits 16
org 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 16
org 0x7c00
LOADER_BASE_ADDR equ 0x8000 ; loader 所处的段地址
LOADER_SECTORS equ 8 ; 需要读取的扇区数
; 初始化寄存器
mov ax, cs
mov ds, ax
mov ss, ax
mov fs, ax
; es 段赋0
mov ax, 0
mov es, ax
; 将 0x7c00 以下的区域作为栈
mov sp, 0x7c00
; 显存段位置
mov ax, 0xb800
mov gs, ax
; 清空屏幕
call cls
; 输出
mov bx, hello_msg
call print ; This will be written after the BIOS messages
;读loader
mov dh, LOADER_SECTORS ; loader 扇区数,读8个扇区共4K空间
mov dl, 0x80 ; 表示第一个磁盘
mov bx, LOADER_BASE_ADDR ; loader 的地址
call disk_load
; 清空屏幕
call cls
jmp 0:LOADER_BASE_ADDR ;跳转至 loader,也就是 0x8000
hello_msg: db "HELLO, MBR!", 0
; 清屏,并将屏幕设置为黑底白字
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中断
ret
; load 'dh' sectors from drive 'dl' into es:bx
disk_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", 0
sectors_error_msg: db "incorrect number of sectors read", 0
; 打印函数
print:
pusha
print_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=0x1234
print_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 string
hex_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.vhd
00000000 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
Note

上面的 hexdump 取自后续章节(开启保护模式)的 loader 产物。第 0 扇区是本章的 mbr.S;第 1、2 扇区(0x2000x400)已经是下一章改造后的 loader.S,它带 GDT 与 Started in 16-bit real mode / Loaded 32-bit protected mode 两段提示字符串。本章最初的 loader.S 只把 LOADER 六个字母直接写进显存,不产生这些字符串。这里提前放出来是为了让读者直观看到 mbr 与 loader 在同一虚拟磁盘里的扇区布局。

最终效果如下图所示: hello-loader

至此,一个运行在实模式下并且支持从磁盘读取 4KB 大小 Loader 的 MBR 程序编码完成,接下来的任务是完善这个 Loader,为加载和运行内核做好准备。

参考#

nasm 手册

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

手写 MBR 引导程序:实模式磁盘读写与 Loader 加载
https://blog.souloss.cn/posts/os/handwriting-mbr-bootloader/
作者
Souloss
发布于
2024-03-11
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时