如何编写引导扇区以从其所在的 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 字节长。

现在,查看此文档会立即提出两个问题:

  1. 如何找出加载我的代码的 U 盘的驱动器 ID?
  2. 当我将数据放入 USB 记忆棒时,我只是使用 dd 或其他工具将 boot.binboot.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 并使用它来设置从驱动器读取时可能需要的一些内部状态,例如每个扇区的字节数。