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