Reputation: 5995
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
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
.
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:
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.
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.
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