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.
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…