Writing your own bootloader for a toy operating system (2)

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. We also instruct GNU as 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’ve also defined a constant LOAD_SEGMENT, which is the segment where we’ll be loading our second stage bootloader (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 < 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 > 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:                  ; just placeholder. We don't yet use volume labels.
 root_strt:   .byte 0,0          ; hold offset of root directory on disk
 root_scts:   .byte 0,0          ; holds # sectors in root directory
 file_strt:   .byte 0,0          ; holds offset of bootloader on disk
 file_scts:   .byte 0,0          ; holds # sectors in boot loader
              .byte 0,0
 rs_fail:     .byte 0            ; hold # tries done when attempting to read a sector
 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. Note that since we don’t volume labels here, we’re able to take the 11 bytes used for the volume label and store other things there – fields to be used later. Note that you are not required to do this: I thought it would be a great way to save space, but I later found that my bootloader did everything I wanted and I still have about 20 bytes to spare, so I could move these fields out of the EBPB after all. But for now, we’ll keep them here.

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 is set to 0x0, because that is where boot sector is loaded (0:07c00)
  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, subfunction 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 number) 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, subfunction to read a key (any key). We then add a far jump to 0xffff:0000 we causes the machine to reboot.

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 most of the framework we’ll need for the rest of the bootloader. In the next section, we’ll write code that actually accesses the floppy drive to load our kernel (or second stage bootloader, actually, but we’ll sort that out).

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" cssfile="">

Trackbacks and Pingbacks: