Discovery Zone: Keeping Knowledge Free!
Search:
Keywords:
In Association with Amazon.com
Another great reference
cover
Assembly Language Programs and Organizations of the IBM PC

ASSEMBLY PROGRAMMING AND OS DESIGN


<HOME>

 

 

 

 

 

 

 

 

Another great reference
cover 80X86 IBM PC and Compatible Computers: Assembly Language, Design and Interfacing Vol. I and II

 

 

 

 

 

 

 

 

 

 

 

 

 

Need a good reference?
cover
Assembly Language Step-by-Step: Programming with DOS and Linux, 2nd Edition

 

 

 

 

 

 

 

 

 

 

 

 

 

Classic Geek Entertainment
cover Akira (DVD Limited Special Edition)
CH3.1 The Real Mode Boot Sector

<HOME><TOC><PREV><UP><NEXT>

The first step to writing an OS is to get the computer to load it.  The boot sector is the key to this mystery and can be quite intimidating to the novice system programmer.  The boot sector is stored on the first sector of the disk so BIOS can locate it.  The BIOS will only load one sector, so the boot loader must be less than 512 bytes in order to fit in the sector.  This limits what can be accomplished by the loader, but really all we need it to do is load the system.

org 0x7C00 ; BIOS loads the boot sector to this address

; ---------------------------------------------------------
; Main program
; ---------------------------------------------------------

BITS 16

START:
                                  ; Setup the stack.
                                  ; We can't allow interrupts while we
                                  ;   setup the stack
              cli                 ; Disable interrupts
              mov    AX,9000h     ; Put stack at 90000h right below screen
              mov    SS,AX        ; SS = 9000 and
              mov    SP,0         ; SP = 0000 => Stack = 90000
              sti                 ; Enable interrupts

                                  ; Remember the boot drive information 
              mov    [bootdrv],DL ;   provided in DL

              call   .LOAD        ; Load the system

              mov    AX,1000h     ; Jump to the loaded program
              mov    ES,AX
              mov    DS,AX
              push   AX
              mov    AX,0
              push   AX
              retf


; ---------------------------------------------------------
; Functions and variables used by our bootstrap
; ----------------------------------------------------------

                                  ; Load a program from the boot drive
.LOAD:
                                  ; Reset the diskdrive (Interrupt 13h, 0)
              push   DX           ; Store DX
              mov    AX,0         ; Select function (Reset Disk)
              mov    DL,[bootdrv] ; Drive to reset
              int    13h          ; Reset it!
              pop    DX           ; Restore DX
              jc     .LOAD        ; Failed -> Try again

              mov    AX,1000h     ; ES:BX = 10000
              mov    ES,AX

.LOAD1:       mov    BX, 0
                                  ; Read disk sectors (Interrupt 13h,2)
              mov    AH,2         ; Select function (Read disk sectors)
              mov    AL,16        ; read 16 sectors or 8K
              mov    CX,3         ; Cylinder=0, sector=3
              mov    DH,0         ; head=0
              mov    DL,[bootdrv] ; drive=boot drive
              int    13h          ; ES:BX = data from disk
              jc     .LOAD1       ; failed -> Try again

              retn

bootdrv       db     0            ; This variable will contain the 
                                  ;   boot drive id 

              times 512-($-$$)-2 db 0
              dw 0AA55h

This simple boot loader will load an 8KB kernel from sector 3 of the disk.  Sector 1 contains this loader and sector 2 holds the first File Allocation Table. 

BIOS loads the boot sector to address 0x7C00 by default, so we start our code with org 0x7c00.  The next step we take is setting up the stack.  This is the part where we cheat a little.  By definition a pure 16 Bit OS would only have access to 2^16 bytes(64KB) of memory.  The OS would hold the kernel, stack and program space all in one 64KB block(segment).  Since the point of this OS is to teach, we are going to place our stack in a different 64KB segment of memory.  This greatly simplifies coding since the stack does not have a fixed size.  If we kept it in the same segment as our code, we would have to take measures to ensure the stack didn't grow into our code.  Complex issues like that will be covered in another section.

0x9000 seemed like a good location, although we could have chosen another.  During this process we disable interrupts using cli.  As far as I can tell, this is unnecessary but it doesn't hurt.  As stated before, you cannot load an immediate value into a segment register, so we have to load AX first with mov AX,9000h.  Then we can move AX into the Stack Segment and clear the Stack Pointer with mov SS,AX and mov SP,0.  Now that we have the stack set up, we can turn the interrupts back on with sti.

BIOS does us a favor by storing the Boot Drive ID in DL.  We will need that to load the kernel, so we store it in our bootdrv variable with mov [bootdrv],DL.

All that is left is to load the kernel and jump to it.  If you are one of those cycle counting speed freaks, then you would place the loading code in the main body of the boot program.  We are here to teach, so we place the code in a set of functions to keep it organized and easy to read.

The main part of the program finished by calling the load function with call .LOAD.  The period in front of LOAD is a NASM convention to define local data.  This makes the function/variable only available to the code running from START: to the end.  If we had code above START:, it would not be able to call .LOAD.

The kernel is now loaded, but in a different 64KB segment from the boot code.  The easiest way to jump to a new segment is to pretend we came from that segment.  We can do that by pushing the segment:offset on to the stack and then using the far return command.  There are other ways to do this, but this is the easiest.  The kernel is located at the 64KB segment starting at 0x10000.  We directly change the Data and Extra Segment with mov AX,1000h, mov DS,AX and mov ES,AX.  To change the Code Segment, we push AX on the stack.  The final step is to prep the Instruction Pointer by moving 0 onto the stack with mov AX,0 and push AX.  The retf instruction then pops the 0 directly into IP and 0x1000 into CS which is exactly where our kernel was loaded.

Now to our functions...

.LOAD doubles as a function address and a loop address.  The first step to loading something from a drive is to reset the drive.  int 13h function 0 of the BIOS accomplishes this task.  For those readers that are experienced with assembly, you've probably already noticed that this code was written to be a generic load routine.  The giveaway is where we push DX to preserve its value.  In reality, we could save a few bytes and a few processor cycles by not preserving DX.  But what's the point of making something a function if you don't plan on using it again later?  AX is left unpreserved by convention in my code.  You are always welcome to preserve it, but I use AX as a temp variable always assuming it will get destroyed. You will see this code again when we discuss the File System.

int 13h function 0 takes two arguements.  The function number is stored in AH and the drive in DL.  Since AL doesn't matter for this we will set AX to 0 with mov AX,0 and load DL with mov DL,[bootdrv]int 13h returns two values.  AH will contain an error code if there was a problem and the Carry Flag(CF) will be set on an error, or clear on success.  We pop DX to restore it and then test the Carry Flag to see if we successfully reset the drive with jc .LOAD.  if CF is set, then the code will jump back to .LOAD and do it again until we finally succeed.

Next we make sure ES points to the correct segment by using mov AX,1000h and mov ES,AX.

.LOAD1 is the second loop in the sequence.  Its job is to actually load the sectors into memory.  ES:BX points to where we want the data stored.  We hard code this to 0 with mov BX,0.  Loading data from a disk is accomplished by int 13h function 2.  The function number is loaded into AH with mov AH,2.  We want to load 8KB or 16 sectors so we load AL with mov AL,16CH holds the cylindar number and CL holds the sector number.  Our kernel is stored at sector 3 on cylindar 0, so we load CX with mov CX,3DH holds the head number and DL the drive number.  We want head 0 of the drive we're booting from, so we use mov DH,0 and mov DL,[bootdrv].  Now everything is set up so we can call int 13h.

int 13h actually returns a couple of important values.  The only one important here is whether or not it was successful.  As with function 0, the CF will be clear on success, so we use jc .LOAD1 to retry the load on an error.  If CF is clear, then we do a near return back to the main part of the loader with retn.

At the end of the code is where we have declared our bootdrv variable as a byte with the db operator.  Next comes a bit of NASM magic where we pad the code to make our boot loader exactly 512 bytes.  NASM treats $ as the current location and $$ as the location of the current block of code.  We only have one block of code defined as START: so $$ should be 0.  $-$$ gives us the length in bytes from START: to this point.  We subtract that from 512 as well as 2 more bytes to fit the boot signature using times 512-($-$$)-2 db 0 to fill the space with 0's.  Finally, every boot sector must have a signature to identify it as a valid boot sector.  The signature is the word 0AA55h.  The signature has to appear as the last two bytes in the sector.  We define it with dw 0AA55h and our boot sector is now complete.

<HOME><TOC><PREV><UP><NEXT>

www.cynergysoft.com

Email any questions to the author, Nathan Daniels.

Copyright © 2001