Boot loader - proctopj/CSE381 GitHub Wiki

Boot loader

Naoki Mizuno

Overview

This project starts off from 16 bit real mode, goes into 32 bit protected mode, and loads the kernel, which is written in C.

  1. Print out strings using BIOS routines
  2. Read disks
  3. Enter 32 bit protected mode
  4. Print out strings using video memory
  5. Load kernel (i.e. jump to the location of main function in kernel)
  6. Print out strings using video memory in the kernel.

The magic number

0xaa55 is one of the most important numbers to this project. These 2 bytes must come at the 511th (0x55) and 512th (0xaa) byte of the boot sector.

times 510-($-$$) db 0
dw 0xaa55

This is a snippet that you will find in any boot sector program. ($-$$) calculates the address offset from the beginning of the sector to the current address. In other words, ($-$$) shows how many bytes you have already used. 510-(how many bytes you used) gives you how many bytes you have left until the end of the 512-byte sector (don't forget the last two bytes 0x55 and 0xaa. The reason why it's dw 0xaa55 rather than dw 0x55aa is "because the x86 architecturehandles multi-byte values inlittle-endianformat, whereby less signi cant bytes proceedmore signi cant bytes, which is contrary to our familiar numbering system."

First step

The first step of this project was to display the letter 'X' on the screen. This is done by using the 0x10 interrupt of the BIOS. Issuing a 0x10 interrupt will make the BIOS print out the character in the al register. You also have to set ah register to 0x0e

mov ah, 0x0e
mov al, 'X'
int 0x10

This will print out the letter 'X' on the screen. In order to print a string, you need to create a loop to iterate through a sequence of characters until it reaches 0.

print_string:
pusha           ; Save all

print_loop:
mov cx, [bx]    ; Copy in order to
cmp cl, 0       ; see if null terminator
je print_end    ; Jump if cl == 0

mov ax, [bx]    ; Store content of address BX
mov ah, 0x0e
int 0x10
add bx, 1       ; Increment address to look at
jmp print_loop

print_end:
popa
ret

This code can be found in bootloader/print/print_string.asm.

Reading from the disk

The following table illustrates what register is used for what when reading from the disk:

Register What
ah = 0x02 BIOS read sector function
al Number of sectors to read
ch Cylinder number
dh Head number
cl What sector to begin (1 is first)

The interrupt for a disk read is 0x13.

You can detect an error by using jc, which stands for "jump if carry set" (1). Also, the number of the sectors that were actually read is set to al, so we can detect errors by comparing that, too (2).

jc disk_error    # 1
...
cmp al, dh
jne disk_error   # 2

32 bit protected mode

Entering a 32-bit environment means that we now have access to a larger region on the main memory (4 GB). One of the largest characteristics of 32 bit protected mode is the existence of Global Descriptor Table. My understanding of this is that this is a way to treat main memory like disks by dividing up into smaller chunks. There are certain bits you have to set in order for this to work, but I honestly have no idea whatsoever what most of these bits mean. This was probably the most boring part (although the author was emphasizing that this part is critical) in the project.

After loading the appropriate bits to the appropriate registers, we make the switch by executing the following instructions:

mov eax, cr0
or eax, 0x1
mov cr0, eax

jmp CODE_SEG:init_pm

The last instruction is called a "far jump" that jumps among sectors, as oppose to a "close jump" where you jump within a sector. After this jump, the program will land on init_pm label, which is already in 32 bit protected mode.

Printing strings to screen (32 bit protected mode)

Once we entered 32 bit protected mode, we want to write strings to the screen. We do this by directly manipulate the video memory, which usually starts at 0xb8000. The idea is the same as what we did in 16 bit real mode, but the video memory address needs to be incremented by 2 instead of 1 because the first byte is used for the ASCII character and the second for the character attributes. Here is the actual code:

[bits 32]

VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f

; Prints a null-terminated string pointed to by edx
print_string_pm:
pusha
mov edx, VIDEO_MEMORY       ; Set edx to the start of vid mem

print_string_pm_loop:
mov al, [ebx]
mov ah, WHITE_ON_BLACK
cmp al, 0
je print_string_pm_done

mov [edx], ax
add ebx, 1
add edx, 2

jmp print_string_pm_loop

print_string_pm_done:
popa
ret

You can see that edx, which contains the video memory address, is incremented by 2. ebx is the pointer to a character to be shown on the screen.

Loading the kernel

Loading the kernel was easier than I imagined. Here are the premises:

  • The boot loader and kernel binary files are concatenated into one file
  • ld -m elf_i386 -s -Ttext 0x1000 --oformat binary -o kernel.bin SOURCE_FILES specifies where the kernel starts
  • The above -Ttext option must be the same as KERNEL_OFFSET equ 0x1000 specified in boot_sect.asm

by calling KERNEL_OFFSET after entering 32 bit protected mode, we will be able to jump to the first instruction of the kernel. kernel_entry.asm tells which function the program should jump into. You can tell by looking at the Makefile that kernel_entry is one of the "ingredients" in making the kernel image.

Print out strings using kernel

Even though the idea of using the video memory is the same as step 4, this time we use a higher level language to do that. C language lets you execute raw assembly code with __asm__ so we can read directly from registers:

unsigned char port_byte_in(unsigned short port) {
    unsigned char result;
    // C wrapper function that reads a byte from a port
    // "=a" (result) sets AL register in "result"
    // "d" (port) load EDX with port
    __asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
    return result;
}

void port_byte_out(unsigned short port, unsigned char data) {
    // "a" (data) load EAX with data
    // "d" (port) load EDX with port
    __asm__("out %%al, %%dx" : : "a" (data), "d" (port));
}

print_at function iterates through the character array until it hits a null terminator. It first uses set_cursor to set the location to print to and prints out one character by writing to the vidmem[offset] = character. We must not forget that one character uses two bytes of video memory: one for the ASCII character and one for the character attributes, which in our case is WHITE_ON_BLACK. It then moves the cursor and repeats this.

__asm__ is called the inline assembler, and has the syntax:

__asm__(ASSEMBLER_TEMPLATE
    : INPUT_OPERAND
    : OUTPUT_OPERAND
    : WORK_REGISTER);

The out instruction "outputs data from a given register (src) to a given output port (dest)." The "a" and "d" indicates which register to use.

One problem that I had was that the last line in the set_cursor function was missing in the tutorial and printed only the last character. This was caused because the lower byte of the cursor location was not updated, thus, the program kept overwriting the same video memory address.

void set_cursor(int offset) {
    offset /= 2;

    // 14 = high byte of cursor location
    port_byte_out(REG_SCREEN_CTRL, 14);
    port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset >> 8));
    // 15 = low byte of cursor location
    port_byte_out(REG_SCREEN_CTRL, 15);
    port_byte_out(REG_SCREEN_DATA, (unsigned char)(offset)); // Was missing
}

Conclusions

One of the nicest things about x86 assembly language, I thought, is the popa and pusha instructions, which let you push/pop all the registers to the stack. Simply calling pusha at the beginning of a function and popa-ing at the end ensures you that all the registers are unharmed. There were some strange rules such as you need to cmp first and branch after that (so it takes two instructions to do a comparison) but I liked x86 better than MIPS because it comes with more functionality.

References