This post is related to rev.7
Our bootlader is now able to load a kernel written in C++ and compiled with Microsoft tools. It places the image at address 0x00010000 and jumps to the entry point. Now, the point is that this entry point is hard coded. It would be more robust to get dynamically this address. Luckily, this information is available in the header of the binary.
The Visual C++ compiler/linker generate an executable binary in a format called Portable Executable (PE). I'm not going to describe here in details this format: I'll have the opportunity to do this later. But, if you want more information, you could refer to the last version of the specifications.
What we need to know for the moment is that a PE header is made of several sections, or sub-header.
The first header is called DOS stub and contains some code used to maintain compatibility with the obsolete operating system. This stub begins with the two bytes
"MZ" (for Mark Zbikowski, a developer of MSDOS) and has at offset 60 (
0x3c) an offset to the next header.
This second header starts with the 4-byte signature
"PE\0\0" that identifies this file as a PE format image file. This section contains lots of information for the image loader and the fields we read are SizeOfCode (offset
0x1C from 'PE'), AddressOfEntryPoint (offset
0x28) and BaseOfCode (offset
The first value is the size of our kernel code. We'll see in a future post that a PE image file defines several standard sections: a
.text section contains code only, while
.data sections contains data, variables and constants. To simplify our present task, I have merged all these sections into a single
.text one. Hence, the SizeOfCode field give us the size of the code and the data.
The BaseOfCode field gives the address of the beginning of the code section once loaded in memory. This value has to be added to the base of the image, which is its loaded address (0x00010000).
Finally, the AddressOfEntryPoint value is the value to add to the base of the image to get the address of the first instruction to execute.
We have modified our bootloader source in order to load and use all this information. We first load the file header in memory, at address 1000h:0000h
read_kernel_header: mov ax, 01000h ; write into 1000h:0000h mov es, ax ; xor bx, bx ; mov ah, 02h ; Read sector from drive mov al, 1 ; Number of sectors mov ch, 0 ; Cylinder mov cl, 2 ; Sector mov dh, 0 ; Head mov dl, [boot_drive] ; Bootable drive int 13h jc read_kernel_header ; loop until success
Then, we parse the header to get the useful values:
; ds:bx points to the start of the image mov ax, 01000h mov ds, ax xor bx, bx ; Check MZ signature mov ax, [bx] cmp ax, 'MZ' jnz Error ; Read PE offset and move mov ax, [bx+3Ch] add bx, ax ; Check PE signature mov ax, [bx] cmp ax, 'PE' jnz Error ; Read code size mov eax, [bx+1Ch] shr eax, 9 mov [kernel_size], eax ; Read address of entry point mov eax, [bx+28h] add eax, 10000h mov [entry_point], eax ; Read base of code segment mov eax, [bx+2Ch] mov [code_base], eax
Finally, we read the rest of the file:
read_kernel: mov ax, 01000h ; write into 1000h:[code_base] mov es, ax ; mov bx, [code_base] ; mov ah, 02h ; Read sector from drive mov al, [kernel_size] ; Number of sectors mov ch, 0 ; Cylinder mov cl, 3 ; Sector mov dh, 0 ; Head mov dl, [boot_drive] ; Bootable drive int 13h jc read_kernel ; loop until success
And we jump into the entry point, after switching into protected mode:
mov ecx, [entry_point] [...] call ecx
Next to these modifications, I've added several files in the kernel project. I've specifically create a
Screen static class that manages all prints in the screen, and several helpers and typedefs. The kernel now starts by cleaning the screen, and then it prints the descriptive message "Starting GenOS".