如何编写引导扇区以从其所在的 USB 记忆棒读取数据?
How can I write a boot sector that reads data from the USB stick that it's on?
我能够编写一个简单的 Hello World 启动 sector/boot 加载器,并 运行 它在带有 BIOS 的实际 x86 硬件上,方法是将它放入 USB 记忆棒的前 512 个字节并启动到它。现在我想知道如何将更多数据放在 USB 记忆棒的其余部分,并使用引导扇区代码将其加载到 RAM 中。我该怎么做?
首先我应该指出,我是编写低级代码的完全新手,所以我不能保证我的解决方案table如何。欢迎批评指正
如果您已经读到这里,我假设您了解 BIOS 中断调用的概念,例如用于在屏幕上显示字符的中断调用。您需要从 U 盘加载数据的是 interrupt call 0x13
.
了解 API
就像中断 0x10 可以用来在屏幕上打印字符一样,中断 0x13 执行各种磁盘操作,您必须通过向 AH
写入适当的数字来选择您想要的。我们将使用调用号 0x02
从驱动器读取数据。此调用需要以下寄存器中的以下参数:
AH
: 0x02
AL
: number of sectors to read
CH
: cylinder
CL
: sector
DH
: head
DL
: drive
BX
: memory address - where in RAM to write the data to
这些参数中的大多数都指定了从驱动器上读取的位置。 BIOS 想知道它应该从 DL
中读取的驱动器的 ID 号,它想知道 CHS coordinates 中该驱动器上数据的坐标。请注意,BIOS 一次只能从驱动器读取整个扇区,因此您指定的是扇区数,而不是字节数。我相信一个扇区通常是 512 字节长。
现在,查看此文档会立即提出两个问题:
- 如何找出加载我的代码的 U 盘的驱动器 ID?
- 当我将数据放入 USB 记忆棒时,我只是使用
dd
或其他工具将 boot.bin
或 boot.img
之类的二进制文件放入其中。我知道我要加载的数据在该文件中的字节偏移量是多少,但是如何将其转换为柱面-头-扇区坐标?
我将尝试回答这两个问题,然后我们将做一个 Hello World 示例。
对于第一个问题:当它跳转到您的引导扇区时,BIOS 将您的驱动器 ID 放入 DL
。这至少在我的硬件(在两台计算机上测试过)和 qemu 中是正确的。在我所有的测试设备上,这最初是 0x80
,根据 Wikipedia(参见 table“驱动器 Table”)对应于“第一个硬盘驱动器”。
对于第二个问题,答案似乎是,至少在我的机器上,使用可用的 CHS 到 LBA 转换 table on Wikipedia。我的理解是,您可以将刻录到 USB 记忆棒上的任何二进制文件视为分为 512 字节的“扇区”数组。第一个扇区的 CHS 坐标为 (0,0,1),然后是 (0,0,2),依此类推直到 (0,0,63),然后是 (0,1,0),依此类推。换句话说,您可以将 CHS 坐标 (c,h,s) 解释为一个三位 base-64 数字,它只为您提供文件 512 字节扇区的索引。这至少是我的硬件似乎做事的方式。
示例代码
所以,这是计划。从我们的 USB 记忆棒中的字节 513 开始(因此,在引导扇区之后),我们将放置一个字符串。这应该对应于驱动器上的 CHS 坐标 (0,0,2),默认情况下其 ID 将简单地位于 DL
中。我们将调用 BIOS 以读取这些坐标处的一个扇区。我打算将它们读入内存偏移量 0x7E00,因为根据 this memory map on osdev.org,这对应于一大块可用的空闲内存。然后我们将在屏幕上打印字符串。
代码如下:
ORG 0x7C00
;
; Main code
;
; Clear segment registers, always necessary
MOV AX, 0
MOV DS, AX
MOV ES, AX
; Read sector 2 of this drive into memory
MOV AH, 2 ; Code to read data
MOV BX, 0x0000_7E00 ; Destination
MOV AL, 1 ; Number of sectors to read
MOV CH, 0 ; Cylinder
MOV DH, 0 ; Head
MOV CL, 2 ; Sector
INT 0x13 ; Fire in the hole!
MOV BX, 0x0000_7E00
CALL print
;
; CPU trap
;
JMP $
;
; Functions
;
print:
; Prints to the screen the zero-terminated string starting at [BX].
PUSHA
MOV AH, 0x0E
loop:
MOV AL, [BX]
CMP AL, 0
JE break
INT 0x10
ADD BX, 1
JMP loop
break:
POPA
RET
;
; Padding and magic number
;
TIMES 510-($-$$) DB 0
DW 0xAA55
;
; This is after the boot sector and so not initially loaded by the BIOS
;
DB 'Hello, world - from disc sector 2!'
DB 0
为了完整起见,我将记录我用来编译它的命令(在 Ubuntu 上)。我 assemble 二进制使用 NASM:
nasm -f bin -o boot.bin main.s
其中main.s
就是上面的汇编文件,然后用
把它推到我的U盘上
dd if=boot.bin of=[YOUR USB STICK'S FILE HANDLE HERE]
例如,我的 USB 记忆棒的文件句柄是 /dev/sda
,但不要只是复制粘贴它,以防它在您的机器上有所不同并且您覆盖了其他设备上的引导扇区。
可移植性警告
以上代码在 qemu、32 位 ThinkPad 和旧的 64 位三星笔记本上按预期工作。但是,我已经看到示例代码执行了一些其他设置可能需要的其他事情,所以我会在这里提及它们。
首先是进行另一个中断 0x13
调用,以便在读取之前“重置”驱动器。我不确定这到底是做什么的,也不确定它是否有必要,但这是执行此操作的代码:
MOV AH, 0
INT 0x13
你会在阅读之前这样做。无论有没有这个步骤,我的代码都可以工作。同样,BIOS 假定 DL
包含驱动器 ID。
我看到的另一件事是用 table 启动引导扇区,如下所示:
OperatingSystemName db "PrettyOS" ; 8 byte
BytesPerSec dw 512
SecPerClus db 1
ReservedSec dw 1
NumFATs db 2
RootEntries dw 224
TotSec dw 2880
MediaType db 0xF0
FATSize dw 9
SecPerTrack dw 18
NumHeads dw 2
HiddenSec dd 0
TotSec32 dd 0
DriveNum db 0
Reserved db 0
BootSig db 0x29
VolumeSerialNum dd 0xD00FC0DE
VolumeLabel db "PRETTY OS " ; 11 byte
FileSys db "FAT12 " ; 8 byte
(Source)
这将位于汇编文件最顶部的任何代码之前,前面有一个跳转指令,可以让您越过它并进入主代码。我没有发现在我的设置中需要这样的东西,但这种东西叫做 BIOS Parameter Table,所以如果我的示例代码不适合你,也许这是值得研究的东西。我的假设是 BIOS 在跳转到您的代码之前读取此 table 并使用它来设置从驱动器读取时可能需要的一些内部状态,例如每个扇区的字节数。
我能够编写一个简单的 Hello World 启动 sector/boot 加载器,并 运行 它在带有 BIOS 的实际 x86 硬件上,方法是将它放入 USB 记忆棒的前 512 个字节并启动到它。现在我想知道如何将更多数据放在 USB 记忆棒的其余部分,并使用引导扇区代码将其加载到 RAM 中。我该怎么做?
首先我应该指出,我是编写低级代码的完全新手,所以我不能保证我的解决方案table如何。欢迎批评指正
如果您已经读到这里,我假设您了解 BIOS 中断调用的概念,例如用于在屏幕上显示字符的中断调用。您需要从 U 盘加载数据的是 interrupt call 0x13
.
了解 API
就像中断 0x10 可以用来在屏幕上打印字符一样,中断 0x13 执行各种磁盘操作,您必须通过向 AH
写入适当的数字来选择您想要的。我们将使用调用号 0x02
从驱动器读取数据。此调用需要以下寄存器中的以下参数:
AH
:0x02
AL
: number of sectors to read
CH
: cylinder
CL
: sector
DH
: head
DL
: drive
BX
: memory address - where in RAM to write the data to
这些参数中的大多数都指定了从驱动器上读取的位置。 BIOS 想知道它应该从 DL
中读取的驱动器的 ID 号,它想知道 CHS coordinates 中该驱动器上数据的坐标。请注意,BIOS 一次只能从驱动器读取整个扇区,因此您指定的是扇区数,而不是字节数。我相信一个扇区通常是 512 字节长。
现在,查看此文档会立即提出两个问题:
- 如何找出加载我的代码的 U 盘的驱动器 ID?
- 当我将数据放入 USB 记忆棒时,我只是使用
dd
或其他工具将boot.bin
或boot.img
之类的二进制文件放入其中。我知道我要加载的数据在该文件中的字节偏移量是多少,但是如何将其转换为柱面-头-扇区坐标?
我将尝试回答这两个问题,然后我们将做一个 Hello World 示例。
对于第一个问题:当它跳转到您的引导扇区时,BIOS 将您的驱动器 ID 放入 DL
。这至少在我的硬件(在两台计算机上测试过)和 qemu 中是正确的。在我所有的测试设备上,这最初是 0x80
,根据 Wikipedia(参见 table“驱动器 Table”)对应于“第一个硬盘驱动器”。
对于第二个问题,答案似乎是,至少在我的机器上,使用可用的 CHS 到 LBA 转换 table on Wikipedia。我的理解是,您可以将刻录到 USB 记忆棒上的任何二进制文件视为分为 512 字节的“扇区”数组。第一个扇区的 CHS 坐标为 (0,0,1),然后是 (0,0,2),依此类推直到 (0,0,63),然后是 (0,1,0),依此类推。换句话说,您可以将 CHS 坐标 (c,h,s) 解释为一个三位 base-64 数字,它只为您提供文件 512 字节扇区的索引。这至少是我的硬件似乎做事的方式。
示例代码
所以,这是计划。从我们的 USB 记忆棒中的字节 513 开始(因此,在引导扇区之后),我们将放置一个字符串。这应该对应于驱动器上的 CHS 坐标 (0,0,2),默认情况下其 ID 将简单地位于 DL
中。我们将调用 BIOS 以读取这些坐标处的一个扇区。我打算将它们读入内存偏移量 0x7E00,因为根据 this memory map on osdev.org,这对应于一大块可用的空闲内存。然后我们将在屏幕上打印字符串。
代码如下:
ORG 0x7C00
;
; Main code
;
; Clear segment registers, always necessary
MOV AX, 0
MOV DS, AX
MOV ES, AX
; Read sector 2 of this drive into memory
MOV AH, 2 ; Code to read data
MOV BX, 0x0000_7E00 ; Destination
MOV AL, 1 ; Number of sectors to read
MOV CH, 0 ; Cylinder
MOV DH, 0 ; Head
MOV CL, 2 ; Sector
INT 0x13 ; Fire in the hole!
MOV BX, 0x0000_7E00
CALL print
;
; CPU trap
;
JMP $
;
; Functions
;
print:
; Prints to the screen the zero-terminated string starting at [BX].
PUSHA
MOV AH, 0x0E
loop:
MOV AL, [BX]
CMP AL, 0
JE break
INT 0x10
ADD BX, 1
JMP loop
break:
POPA
RET
;
; Padding and magic number
;
TIMES 510-($-$$) DB 0
DW 0xAA55
;
; This is after the boot sector and so not initially loaded by the BIOS
;
DB 'Hello, world - from disc sector 2!'
DB 0
为了完整起见,我将记录我用来编译它的命令(在 Ubuntu 上)。我 assemble 二进制使用 NASM:
nasm -f bin -o boot.bin main.s
其中main.s
就是上面的汇编文件,然后用
dd if=boot.bin of=[YOUR USB STICK'S FILE HANDLE HERE]
例如,我的 USB 记忆棒的文件句柄是 /dev/sda
,但不要只是复制粘贴它,以防它在您的机器上有所不同并且您覆盖了其他设备上的引导扇区。
可移植性警告
以上代码在 qemu、32 位 ThinkPad 和旧的 64 位三星笔记本上按预期工作。但是,我已经看到示例代码执行了一些其他设置可能需要的其他事情,所以我会在这里提及它们。
首先是进行另一个中断 0x13
调用,以便在读取之前“重置”驱动器。我不确定这到底是做什么的,也不确定它是否有必要,但这是执行此操作的代码:
MOV AH, 0
INT 0x13
你会在阅读之前这样做。无论有没有这个步骤,我的代码都可以工作。同样,BIOS 假定 DL
包含驱动器 ID。
我看到的另一件事是用 table 启动引导扇区,如下所示:
OperatingSystemName db "PrettyOS" ; 8 byte
BytesPerSec dw 512
SecPerClus db 1
ReservedSec dw 1
NumFATs db 2
RootEntries dw 224
TotSec dw 2880
MediaType db 0xF0
FATSize dw 9
SecPerTrack dw 18
NumHeads dw 2
HiddenSec dd 0
TotSec32 dd 0
DriveNum db 0
Reserved db 0
BootSig db 0x29
VolumeSerialNum dd 0xD00FC0DE
VolumeLabel db "PRETTY OS " ; 11 byte
FileSys db "FAT12 " ; 8 byte
(Source)
这将位于汇编文件最顶部的任何代码之前,前面有一个跳转指令,可以让您越过它并进入主代码。我没有发现在我的设置中需要这样的东西,但这种东西叫做 BIOS Parameter Table,所以如果我的示例代码不适合你,也许这是值得研究的东西。我的假设是 BIOS 在跳转到您的代码之前读取此 table 并使用它来设置从驱动器读取时可能需要的一些内部状态,例如每个扇区的字节数。