Jack M
Jack M

Reputation: 5995

How can I write a boot sector that reads data from the USB stick that it's on?

I was able to write a simple Hello World boot sector/boot loader and run it on actual x86 hardware with a BIOS by putting it into the first 512 bytes of a USB stick and booting into it. Now I want to know how to put more data on the rest of the USB stick, and load it into RAM with the boot sector code. How can I do this?

Upvotes: 2

Views: 1917

Answers (1)

Jack M
Jack M

Reputation: 5995

First of all I should note that I'm a complete newbie to writing low-level code, so I can't guarantee how portable my solution is. Criticism is welcome.

If you've gotten this far I assume you understand the concept of BIOS interrupt calls, such as the one used to display a character on the screen. What you need to load data from the USB stick is interrupt call 0x13.

Understanding the API

Just like with interrupt 0x10 which can be used to print character on-screen, interrupt 0x13 performs various disk operations and you must choose which one you want by writing an appropriate number to AH. We will be using call number 0x02 which reads data from a drive. This call expects the following parameters in the following registers:

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

Most of these parameters specify where on the drive to read from. The BIOS wants to know the ID number of the drive it should be reading from in DL, and it wants to know the coordinates of the data on that drive in CHS coordinates. Notice that the BIOS can only read from the drive an entire sector at a time, so you specify a number of sectors, not a number of bytes. I believe a sector is usually 512 bytes long.

Now, looking at this documentation immediately raises two questions:

  1. How do I find out the drive ID of the USB stick my code was loaded from?
  2. When I put my data on my USB stick, I just put a binary file like boot.bin or boot.img on it with dd or some other tool. I know what byte offset the data I want to load is at in that file, but how do I translate that into Cylinder-Head-Sector coordinates?

I'll try to answer both of these questions, and then we'll do a Hello World example.

For the first question: the BIOS puts your drive's ID in DL when it jumps to your bootsector. This is at least true on my hardware (tested on two computers) and in qemu. On all my test devices, this was initially 0x80, which according to Wikipedia (see the table "Drive Table") corresponds to the "first harddrive".

For the second question, the answer seems to be, at least on my machines, to use the CHS to LBA conversion table available on Wikipedia. My understanding is that you can think of whatever binary file you burn onto your USB stick as being divided into an array of "sectors" of 512 bytes. The first sector has CHS coordinates (0,0,1), then (0,0,2), and so on up to (0,0,63), then it's (0,1,0), and so on. In other words, you can interpret the CHS coordinates (c,h,s) as a three-digit base-64 number which simply gives you the index of a 512 byte sector of your file. This is at least how my hardware seems to do things.

Example code

So, here's the plan. Starting at byte 513 in our USB stick (so, after the boot sector), we'll place a string. This should correspond to CHS coordinates (0,0,2) on the drive whose ID will simply be in DL by default. We'll make the BIOS call to read one sector at those coordinates. I'm going to read them into memory offset 0x7E00, since according to this memory map on osdev.org, that corresponds to a large block of usable free memory. We'll then print the string onscreen.

Here's the code:

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

For completeness, I'll document the commands I use to compile this (on Ubuntu). I assemble the binary using NASM:

nasm -f bin -o boot.bin main.s

where main.s is the above assembly file, and then push it onto my USB stick with

dd if=boot.bin of=[YOUR USB STICK'S FILE HANDLE HERE]

Where, for example, my USB stick's file handle is /dev/sda, but don't just copy-paste that in case it's different on your machine and you overwrite the boot sector on some other device.

Portability caveats

The above code works as expected for me on qemu, on a 32-bit ThinkPad and on an old 64-bit Samsung notebook. However, I've seen example code which does some other things which may be necessary on other setups, so I'll mention them here.

The first is to make another interrupt 0x13 call to "reset" the drive before reading from it. I'm not sure what exactly this does or whether it's ever necessary, but here is the code to do it:

MOV AH, 0
INT 0x13

You would do this just before reading. My code works with or without this step. Again, the BIOS assumes DL contains the drive ID.

Another thing I've seen is to start off the boot sector with a table something like this:

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)

This would go before any code at the very top of the assembly file, preceded by a jump instruction to get you over it and into the main code. I did not find anything like this to be necessary on my setup, but this sort of thing is called a BIOS Parameter Table, so maybe this is something to look into if my example code doesn't work for you. My assumption is that the BIOS reads this table before jumping to your code and uses it to set some internal state which will may needed when reading from the drive, such as the number of bytes per sector.

Upvotes: 4

Related Questions