This article is part of a short series. Find part 1 here, or go straight through to part 3.
Now that we know the structure of the boot parameter block (BPB) and extended boot parameter block (EBPB), we can start writing our first code. (If you need a refresher, have a look at part 1 of this article).
First code in GNU assembler
We’ll be using the GNU assembler, since it’s free, comes with a boatload of options, supports AT&T and Intel assembly syntax and plays nice with gcc and ld later on. Some of the preprocessor directives used may need some explanation, but all code will be in straightforward Intel syntax.
Here’s some boilerplate code to get started:
.code16 .intel_syntax noprefix .text .org 0x0 LOAD_SEGMENT = 0x1000 .global main main: jmp short start nop # BPB and EBPB here start: # rest of code
The pile of preprocessor instructions at the top tell the assembler to assemble code for real mode. Since all (intel-based) computers start up in real mode with 16-bit instructions, we won’t be able to write 32-bit code here yet. We also instruct GNU assembler that we’ll be using Intel syntax (e.g. mov ax, 1 instead of movw $1, %ax – some prefer the latter, but most readers of this text will be familiar with Intel). The origin of our code will be 0×0, i.e. all absolute addresses start at 0×0, which will be convenient.
Then there’s the main entry point of our code, which corresponds to the first byte of actual output when assembled. The code under “main” simply jumps over the BPB and EBPB located at offset 0×3, resuming execution at the label start. We’ll flesh out the BPB/EBPB in a bit, since it’ll have to have a very exact size.
We’ve also defined a constant LOAD_SEGMENT, which is the segment where we’ll be loading our second stage boot loader (more about that later).
The Boot Parameter Block
The structure of the boot parameter block can be coded like this:
bootsector: iOEM: .ascii "DevOS " # OEM String iSectSize: .word 0x200 # bytes per sector iClustSize: .byte 1 # sectors per cluster iResSect: .word 1 # #of reserved sectors iFatCnt: .byte 2 # #of FAT copies iRootSize: .word 224 # size of root directory iTotalSect: .word 2880 # total # of sectors if over 32 MB iMedia: .byte 0xF0 # media Descriptor iFatSize: .word 9 # size of each FAT iTrackSect: .word 9 # sectors per track iHeadCnt: .word 2 # number of read-write heads iHiddenSect: .int 0 # number of hidden sectors iSect32: .int 0 # # sectors for over 32 MB iBootDrive: .byte 0 # holds drive that the boot sector came from iReserved: .byte 0 # reserved, empty iBootSign: .byte 0x29 # extended boot sector signature iVolID: .ascii "seri" # disk serial acVolumeLabel: .ascii "MYVOLUME " # volume label acFSType: .ascii "FAT16 " # file system type
The fields in this structure correspond to the specification in part 1 of this text, and since they’re nicely labelled, we’ll be able to refer to them later on.
Real-mode Segments
After the start label, we can write some actual code. Let’s start by defining our real mode data segments:
cli mov iBootDrive, dl # save what drive we booted from (should be 0x0) mov ax, cs # CS = 0x0, since that's where boot sector is (0x07c00) mov ds, ax # DS = CS = 0x0 mov es, ax # ES = CS = 0x0 mov ss, ax # SS = CS = 0x0 mov sp, 0x7C00 # Stack grows down from offset 0x7C00 toward 0x0000. sti
Here, we mask interrupts so that interrupt calls don’t mess up our sector declarations. We set ES = DS = SS = CS = 0×0, and make the stack grow down from 0x7C00 (our boot loader was loaded at 0x7C00). When done, we turn the interrupts back on. It’s important to note that the BIOS places the number of the boot drive in the DL register. We store it in our BPB for later use.
Resetting the disk system
Next, we need to prepare the floppy drive for use. This is done through BIOS interrupt 0×13, subfunction 0. We call it with the boot drive in DL:
mov dl, iBootDrive # drive to reset xor ax, ax # subfunction 0 int 0x13 # call interrupt 13h jc bootFailure # display error message if carry set (error)
If the reset fails, the carry flag will be set and we jump to a label where we handle a boot failure by showing a message, waiting for a keypress and rebooting. Come to think of it, we’ll need a way to print a string to the screen.
Printing a string
We’ll add a short function that uses BIOS interrupt 0×10, sub-function 9 to print characters to the screen. The calling code must point DS:SI to the null-terminated string to be printed.
.func WriteString WriteString: lodsb # load byte at ds:si into al (advancing si) or al, al # test if character is 0 (end) jz WriteString_done # jump to end if 0. mov ah, 0xe # Subfunction 0xe of int 10h (video teletype output). mov bx, 9 # Set bh (page nr) to 0, and bl (attribute) to white (9). int 0x10 # call BIOS interrupt. jmp WriteString # Repeat for next character. WriteString_done: retw .endfunc
We can now define the “bootFailure” label:
diskerror: .asciz "Disk error. " bootFailure: lea si, diskerror call WriteString call Reboot
Great. We’ve got code to reset the floppy drive, and if it fails, there’s code that prints failure strings and reboots. Although, we still have to write a Reboot function.
Rebooting
Here is some code that prints a “Press any key to reboot” message, waits for a keystroke, and reboots the machine.
rebootmsg: .asciz "Press any key to reboot\r\n" .func Reboot Reboot: lea si, rebootmsg # Load address of reboot message into si call WriteString # print the string xor ax, ax # subfuction 0 int 0x16 # call bios to wait for key .byte 0xEA # machine language to jump to FFFF:0000 (reboot) .word 0x0000 .word 0xFFFF .endfunc
Here, we use BIOS interrupt 0×16, sub-function 0 to read a key (any key). We then add a far jump to 0xffff:0000 we causes the machine to reboot.
Putting it all together
When we combine the functions above into a single source file, we end up with this:
.code16 .intel_syntax noprefix .text .org 0x0 LOAD_SEGMENT = 0x1000 ; load the boot loader to segment 1000h .global main main: jmp short start # jump to beginning of code nop bootsector: iOEM: .ascii "DevOS " # OEM String iSectSize: .word 0x200 # bytes per sector iClustSize: .byte 1 # sectors per cluster iResSect: .word 1 # #of reserved sectors iFatCnt: .byte 2 # #of FAT copies iRootSize: .word 224 # size of root directory iTotalSect: .word 2880 # total # of sectors if over 32 MB iMedia: .byte 0xF0 # media Descriptor iFatSize: .word 9 # size of each FAT iTrackSect: .word 9 # sectors per track iHeadCnt: .word 2 # number of read-write heads iHiddenSect: .int 0 # number of hidden sectors iSect32: .int 0 # # sectors for over 32 MB iBootDrive: .byte 0 # holds drive that the boot sector came from iReserved: .byte 0 # reserved, empty iBootSign: .byte 0x29 # extended boot sector signature iVolID: .ascii "seri" # disk serial acVolumeLabel: .ascii "MYVOLUME " # volume label acFSType: .ascii "FAT16 " # file system type .func WriteString WriteString: lodsb # load byte at ds:si into al (advancing si) or al, al # test if character is 0 (end) jz WriteString_done # jump to end if 0. mov ah, 0xe # Subfunction 0xe of int 10h (video teletype output) mov bx, 9 # Set bh (page nr) to 0, and bl (attribute) to white (9) int 0x10 # call BIOS interrupt. jmp WriteString # Repeat for next character. WriteString_done: retw .endfunc .func Reboot Reboot: lea si, rebootmsg # Load address of reboot message into si call WriteString # print the string xor ax, ax # subfuction 0 int 0x16 # call bios to wait for key .byte 0xEA # machine language to jump to FFFF:0000 (reboot) .word 0x0000 .word 0xFFFF .endfunc start: # Setup segments: cli mov iBootDrive, dl # save what drive we booted from (should be 0x0) mov ax, cs # CS = 0x0, since that's where boot sector is (0x07c00) mov ds, ax # DS = CS = 0x0 mov es, ax # ES = CS = 0x0 mov ss, ax # SS = CS = 0x0 mov sp, 0x7C00 # Stack grows down from offset 0x7C00 toward 0x0000. sti # Display "loading" message: lea si, loadmsg call WriteString # Reset disk system. # Jump to bootFailure on error. mov dl, iBootDrive # drive to reset xor ax, ax # subfunction 0 int 0x13 # call interrupt 13h jc bootFailure # display error message if carry set (error) # End of loader, for now. Reboot. call Reboot bootFailure: lea si, diskerror call WriteString call Reboot # PROGRAM DATA loadmsg: .asciz "Loading OS...\r\n" diskerror: .asciz "Disk error. " rebootmsg: .asciz "Press any key to reboot.\r\n" .fill (510-(.-main)), 1, 0 # Pad with nulls up to 510 bytes (excl. boot magic) BootMagic: .int 0xAA55 # magic word for BIOS
Points of note
- The WriteString and Reboot sections are functions, that we’ll want to call various times in the other boot code that we’ll write soon. They are not part of the main body of code. That is why they are placed before the start label, so that execution will jump over them.
- The loader will not print a “Loading OS…” message right after it sets the segments.
- After having reset the disk system successfully, the loader will reboot. We’re doing that only because we haven’t written any more code yet that does interesting things.
- The source code ends with a .fill preprocessor directive. This causes the assembler to fill up the output file with null bytes all the way to offset 510. The final two bytes contain the magic word required by some BIOSes. Compilation will now yield a file of exactly 512 bytes, which is what we need for our boot sector.
Summary
We’ve written assembler code that prepares data and stack segments and resets the floppy drive. We’ve also added functions for writing text to the screen, waiting for a keypress, and rebooting, which wraps up
I don’t know about you, but I’m just about ready to see this code in action! In the next section, we’ll see how we can actually compile and test this code.
