Loading and executing the Second Stage Boot Loader

In the last blog posting I have talked about how to read files from a FAT12 partition and print out its loaded content to the screen. In today’s blog posting I want to talk more about how to implement a Second Stage Boot Loader that can be loaded into memory for its execution. But let’s talk first, why we need a Second Stage Boot Loader.

Second Stage Boot Loader – Why?

In my first blog posting about OS development, we have talked about how the BIOS loads the boot sector at the memory location 0x7C00 and executes it. The restriction of the boot sector is that it has only a size of 512 bytes (one disk sector) and within that size you can’t implement that much functionality with that restriction.

Therefore, you need a Second Stage Boot Loader that gets loaded into memory and prepares the CPU and the actual OS kernel for its execution. A Second Stage Boot Loader must implement the following tasks:

  • Getting date and time information from the BIOS
  • Detecting the Memory Map
  • Retrieving the supported Video Modes
  • Enabling the A20 Gate
  • Switching the CPU into x32 Protected Mode or x64 Long Mode
  • Loading the real OS Kernel from Disk
  • Executing the OS Kernel

As you can see from this list, it would be impossible to do all these things within 512 bytes – or even 510 bytes when you subtract the magical byte pattern 0xAA55 at the end of the boot sector.

Implementing the Second Stage Boot Loader

Let’s talk now how to implement the Second Stage Boot Loader. In my case the Second Stage Boot Loader is x16 based so that I still have access to the BIOS interrupts – otherwise you can’t implement the above-mentioned tasks. One of the first things that the Second Stage Boot Loader implements is getting the date and time information from the BIOS.

This information will then be passed over to the Kernel who must keep them up to date – as precise as possible. The following listing shows the GetDate and GetTime functions that uses the BIOS interrupt 0x1A to get this information.

;=================================================
; This function retrieves the date from the BIOS.
;=================================================
GetDate:
    ; Get the current date from the BIOS
    MOV     AH, 0x4
    INT     0x1A

    ; Century
    PUSH    CX
    MOV     AL, CH
    CALL    Bcd2Decimal
    MOV     [Year1], AX
    POP     CX

    ; Year
    MOV     AL, CL
    CALL    Bcd2Decimal
    MOV     [Year2], AX

    ; Month
    MOV     AL, DH
    CALL    Bcd2Decimal 
    MOV     [Month], AX

    ; Day
    MOV     AL, DL
    CALL    Bcd2Decimal
    MOV     [Day], AX

    ; Calculate the whole year (e.g. "20" * 100 + "22" = 2022)
    MOV     AX, [Year1]
    MOV     BX, 100
    MUL     BX
    MOV     BX, [Year2]
    ADD     AX, BX
    MOV     [Year], AX
RET

;=================================================
; This function retrieves the time from the BIOS.
;=================================================
GetTime:
    ; Get the current time from the BIOS
    MOV     AH, 0x2
    INT     0x1A

    ; Hour
    PUSH    CX
    MOV     AL, CH
    CALL    Bcd2Decimal
    MOV     [Hour], AX
    POP     CX

    ; Minute
    MOV     AL, CL
    CALL    Bcd2Decimal
    MOV     [Minute], AX

    ; Second
    MOV     AL, DH
    CALL    Bcd2Decimal
    MOV     [Second], AX
RET

As you can see from the previous listing, the BIOS interrupt 0x1A returns the current date and time as BCD numbers, therefore the function Bcd2Decimal does the conversion to decimal values:

;==========================================================
; This function converts a BCD number to a decimal number.
;==========================================================
Bcd2Decimal:
    MOV     CL, AL
    SHR     AL, 4
    MOV     CH, 10
    MUL     CH
    AND     CL, 0Fh
    ADD     AL, CL
RET

Afterwards the retrieved information is printed to the screen through the functions PrintString and PrintDecimal.

;================================================
; This function prints a whole string, where the 
; input string is stored in the register "SI"
;================================================
PrintString:
    ; Set the TTY mode
    MOV     AH, 0xE
    INT     10

    ; Set the input string
    MOV     AL, [SI]
    CMP     AL, 0
    JE      PrintString_End
    
    INT     0x10
    INC     SI
    JMP     PrintString
    
    PrintString_End:
RET

;================================================
; This function prints out a decimal number
; that is stored in the register AX.
;================================================
PrintDecimal:
    MOV     CX, 0
    MOV     DX, 0

PrintDecimal_Start:
    CMP     AX ,0
    JE      PrintDecimal_Print
    MOV     BX, 10
    DIV     BX
    PUSH    DX
    INC     CX
    XOR     DX, DX
    JMP     PrintDecimal_Start
PrintDecimal_Print:
    CMP     CX, 0
    JE      PrintDecimal_Exit
    POP     DX
        
    ; Add 48 so that it represents the ASCII value of digits
    MOV     AL, DL
    ADD     AL, 48
    MOV     AH, 0xE
    INT     0x10

    DEC     CX
    JMP     PrintDecimal_Print
PrintDecimal_Exit:
RET

Over the next blog postings, the implementation of the Second Stage Boot Loader will be enhanced with the previous mentioned tasks.

Loading and executing the Second Stage Boot Loader

The Second Stage Boot Loader code will be compiled by NASM into a raw binary file and then it will be added as an additional file to our final floppy image. Our boot loader code can then read that file with the implementation from the last blog posting into memory for its code execution. The following code shows how the Second Stage Boot Loader is loaded into memory – the variable KAOSLDR_OFFSET currently points to the memory location 0x2000.

; Load the KAOSLDR.BIN file into memory
    MOV     CX, 11
    LEA     SI, [SecondStageFileName]
    LEA     DI, [FileName]
    REP     MOVSB
    MOV     WORD [Loader_Offset], KAOSLDR_OFFSET
    CALL    LoadRootDirectory

    ; Execute the KAOSLDR.BIN file...
    CALL KAOSLDR_OFFSET

Executing the Second Stage Boot Loader code is quite easy: we just do a simple CALL to the memory location KAOSLDR_OFFSET.

Summary

Reading an additional binary file into memory for its execution is not that complicated – if we are in x16 Real Mode, who offers us the necessary BIOS interrupts. Today we have used this functionality to load and execution the Second Stage Boot Loader of our OS. This additional boot loader code is necessary because we can’t do that many things within the limited boot sector size of 512 bytes. In the next blog posting we will continue with our Second Stage Boot Loader code and will finally switch the CPU into x64 Long Mode. Stay tuned…

Leave a Comment

Your email address will not be published. Required fields are marked *

Watch one of my Online Trainings:

SQL Server Performance Troubleshooting

EUR 229 incl. 20% VAT