Write the kernel in C++
By cedrou on Wednesday, December 31 2008, 16:35 - Boot - Permalink
This post is related to rev.6
In the previous post, we have written some code that made the CPU run in 32 bits protected mode.
We can now use the Visual C++ compiler to produce a kernel binary from C/C++ code. That was not possible before, because this compiler cannot generate 16 bits code.
In fact, we'll not write C++ code for the moment, but we'll use inline assembly. This is only to make a smooth introduction to the C++ compiler.
We've created a new C++ Console Project with only one source file and the following function:
void __declspec(naked) kmain() { __asm { mov eax, 0B8000h ; start of video memory mov [eax], 'B' ; Put the ASCII-code of 'B' inc eax mov [eax], 1Bh ; Assign a color code hang: jmp hang } }
This method is really simple - it prints a 'B' on the top-left of the screen and hangs into an infinite loop - but it needs some explanations.
The __asm { ... }
statement is used to embed assembly-language instruction directly in the C++ source. It invokes the inline assembler built into the C++ compiler and doesn't require any additional link step. The compiler does not try to optimize the __asm
block: what you write is what you get.
The __declspec(naked)
annotation in the header tells the compiler not to add byte code at start nor at end of the function. Actually, a compiler often creates a prologue and an epilogue for each function, excepted when occur some optimizations or when a 'naked' declaration is specified.
The aim of a prologue is to:
- set up EBP and ESP
- reserve space on stack for local variables
- save registers that should be modified in the body of the function
Here is a typical prologue:
push ebp ; Save ebp mov ebp, esp ; Set stack frame pointer sub esp, localbytes ; Allocate space for locals push <registers> ; Save registers
An epilogue has to:
- restore the saved register values
- clean up the reserved space for local variables
This a standard epilogue:
pop <registers> ; Restore registers mov esp, ebp ; Restore stack pointer pop ebp ; Restore ebp ret ; Return from function
In our case, we have no parameters and no local variable, and we don't need to save the registers because this method never returns, so these prologue and epilogue are unneeded.
Now, we must modify the project configuration to fit our needs. Here are the most important parameters to change:
- Ignore all default librairies: We don't want to link against Windows standard DLLs, not even against the C library that defines very standard functions like memcpy, memset, and so on.
- Base address = 0x00010000: The binary will be loaded in memory at this address, and we tell the compiler to generate offsets in accordance with this.
- Entry point = kmain: That tells the compiler to add in the file header the address of this function. This is the first function that will be called.
- Exception handling = false
- No basic runtime checks and No buffer security check: This would induce to link against several libraries.
- Native subsystem: I'm not sure it's mandatory. This subsystem is used for Windows drivers, thus would fit well with our project. Other subsystems are Console, "Windows", "EFI" and "POSIX".
We can now compile the project to get an executable file named "kernel.exe" that will be placed just after the bootloader on the second sector of the floppy image.
The last thing to do is to perform several modification to the bootlader in order to load this file in memory and to jump to kmain()
The destination address of the read data is now 0x00010000 and we need to read 2 sectors on the disk.
read_sector: mov ax, 01000h ; write into 1000h:0000h mov es, ax ; xor bx, bx ; mov ah, DISK_READ_SECTORS ; Read sector from drive mov al, 2 ; Load 1 sector mov ch, 0 ; Cylinder=0 mov cl, 2 ; Sector=2 mov dh, 0 ; Head=0 mov dl, [boot_drive] ; Bootable drive INT_DISK jc read_sector ; loop until success
Finally, the jump address is hard coded:
jmp 010200h