Creating a 64-bit kernel using a separate loader

From OSDev Wiki
Jump to navigation Jump to search
Difficulty level
Difficulty 3.png
Kernel Designs
Other Concepts

This page is under construction! This page or section is a work in progress and may thus be incomplete. Its content may be changed in the near future.

This page is an extension to Creating a 64-bit kernel, specifically the section Loading with a separate loader. If you have not read that, go read it now.
This may or may not work with your kernel and you may or may not need to tweak it to work with your kernel.

64-bit kernel

The kernel itself should not contain any multiboot headers, and does not need an assembly bootstrap (you are free to use one if you find it necessary).
The OUPUT_FORMAT, as specified in your linker script, should be elf64-x86-64 for it to be loaded correctly by the Loader.

After your loader has passed control to the kernel, you should set up a 64-bit IDT and paging.

Compile C files with

x86_64-elf-gcc -m64 -c <source file> -o <output> -ffreestanding -z max-page-size=0x1000 -mno-red-zone -mno-mmx -mno-sse -mno-sse2 -std=gnu99 -O2 -Wall -Wextra -I <path to your header files>

Link the kernel with

x86_64-elf-gcc -T <linker script> -o <output> <all object files> -ffreestanding -O2 -nostdlib -lgcc

Your ouput should be copied to loader_build/boot/kernel.bin (See 32-bit loader GRUB)

32-bit loader

Your loader will be similar to the kernel made in the Bare Bones tutorial.

Assembly bootstrap

It starts with a multiboot header and an assembly routine to set up a stack and call the C code.
Additionally it might check eax to see if it contains 0x2BADB002 (which means it was loaded by a multiboot bootloader)

	cli                              ; Clear interrupts, we don't want them as long as we are in the loader
	mov	esp, stack_top           ; Set up a valid stack
        mov     ebp, stack_top
	push	ebx                      ; Push pointer to multiboot info structure
	call	lmain


The loader must now set up its own 32-bit GDT and parse the multiboot structure given to it by the bootstrap.
A header containing useful multiboot structures can be downloaded here

#include "multiboot.h"

/* This function gets called by the bootloader */
void lmain(const void* multiboot_struct) {
        // Set up GDT
        const multiboot_info_t* mb_info = multiboot_struct;            /* Make pointer to multiboot_info_t struct */
	multiboot_uint32_t mb_flags = mb_info->flags;                  /* Get flags from mb_info */

        void* kentry = NULL;                                           /* Pointer to the kernel entry point */

        if (mb_flags & MULTIBOOT_INFO_MODS) {                          /* Check if modules are available */
                multiboot_uint32_t mods_count = mb_info->mods_count;   /* Get the amount of modules available */
		multiboot_uint32_t mods_addr = mb_info->mods_addr;     /* And the starting address of the modules */

                for (uint32_t mod = 0; mod < mods_count; mod++) {
                        multiboot_module_t* module = (multiboot_module_t*)(mods_addr + (mod * sizeof(multiboot_module_t)));     /* Loop through all modules */

Every multiboot module has a command line associated with it, available from the multiboot_module_t is a pointer to it.
One might load all the modules available into memory at this point, or check them for a specific command line to load only a specific subset.

        const char* module_string = (const char*)module->cmdline;
        /* Here I check if module_string is equals to the one i assigned my kernel
           you could skip this check if you had a way of determining the kernel module */
        if(strcmp(module_string, kernel_bin_string)){
                kentry = load_elf_module(module->mod_start, module->mod_end);

Now, we must parse the kernel ELF file that the bootloader loaded into memory for us.
This can be done pretty easily using a helper function and a header I wrote following the ELF64 specification (Available [|here]) and function declarations (Available [|here])

        #include "elf64.h" // Also requires elf64.c

        char* kernel_elf_space[sizeof(elf_file_data_t)];
        elf_file_data_t* kernel_elf = (elf_file_data_t*) kernel_elf_space;                                          /* Pointer to elf file structure (remember there is no memory management yet) */

        /* This function parses the ELF file and returns the entry point */
        void* load_elf_module(multiboot_uint32_t mod_start, multiboot_uint32_t mod_end) {
                unsigned long err = parse_elf_executable((void*)mod_start, sizeof(elf_file_data_t), kernel_elf);    /* Parses ELF file and returns an error code */
                if(err == 0){                                                                                       /* No errors occurred while parsing the file */
                        for(int i = 0; i < kernel_elf->numSegments; i++){
			        elf_file_segment_t seg = kernel_elf->segments[i];                                   /* Load all the program segments into memory */
			                                                                                            /*  if you want to do relocation you should do so here, */
			        const void* src = (const void*) (mod_start + seg.foffset);                          /*  though that would require some changes to parse_elf_executable */
			        memcpy((void*) seg.address, src, seg.flength);
                        return (void*) kernel_elf->entryAddr;                                                       /* Finally we can return the entry address */
                return NULL;

Finally, we can set up long mode and jump to the 64-bit kernel.
We do this by putting 2 assembly functions at the end of the lmain function

       extern void setup_longmode();
       extern void enter_kernel(void* entry, uint32_t multiboot_info);

       void lmain(const void* multiboot_struct) {
               // Set up GDT
               // Check modules

               setup_longmode();                                                                                    /* Set up long mode and jump to the kernel code */
               enter_kernel(kentry, (uint32_t) mb_info);

Setting up long mode

Before we even look at switching to long mode, we must make sure that it is available on the current CPU.

  • First, we check if the CPUID instruction is available by flipping bit 21 (CPUID) in the eflags register. If the bit was flipped CPUID is available.
  • Second, we must check if the extended CPUID functions are avaiable. This is done by checking CPUID function 0x80000000, to see if it is higher than or equals to 0x80000001.
  • And last a check if long mode is available, by executing CPUID function 0x80000001 and testing if bit 29 (Long mode) is set.

To set up long mode, many things must be done.

  • Disable any paging your bootloader might have set up (clearing bit 31 in CR0)
  • Then, set up 64-bit paging (but do not enable it yet). This includes PML4T, PDPT, PDT and PT to identity map any memory you will need before your 64-bit kernel has set up it's own paging.
  • Enable PAE (Physical Address Extension) by setting bit 5 of CR4
  • Switch to IA32e (compatibility mode), by setting bit 8 (Long Mode Enable) in MSR 0xC0000080
  • Enable paging again, by setting bit 31 in CR0

Now we are in Compatibility mode, to enter Long mode we have to set up a 64-bit GDT (This will be the GDT your kernel uses, so you have to set up everything here)
The code below is not optimal in the slightest, and you should try to put together your own alternative.

global enter_kernel
	push	ebp
	mov	ebp, esp                ; Set up the stack so the variables passed from the C code can be read

	mov	esi, [ebp+8]            ; This is the kernel entry point
	mov	[k_ptr], esi

	lgdt	[GDT.pointer]           ; Load GDT
	mov	ax,            ; Reload data segment selectors
	mov	ss, ax
	mov	ds, ax
	mov	es, ax
	jmp	GDT.code:.jmp_k         ; Reload code selector by jumping to 64-bit code
	mov	edi, [ebp + 12]		; 1st argument of kernel_main (pointer to multiboot structure)
	mov	eax, [k_ptr]                                                                                                      ; This is transformed to mov rax, [k_ptr] and uses the double word reserved below 
	dd	0			; Trick the processor, contains high address of k_ptr                                     ; as higher half of the address to k_ptr
	jmp	eax                     ; This part is plain bad, tricking the processor is not the best thing to do here

section .data
	dd	0

Compiling and linking the loader

The loader must be compiled and linked as a 32-bit ELF file with the Multiboot header within the first 8 kB (See Bare Bones Tutorial to see how to do this)


If you used GRUB this is an example of how to set up the directory structure:

  • loader_build/
    • boot/
      • grub/
        • grub.cfg
      • kernel.bin (This is the 64-bit kernel ELF file)
      • loader.bin (This is the 32-bit loader ELF file)

Your grub.cfg file will contain at least the following

menuentry "Kernel" {
     multiboot /boot/loader.bin                  // Path to the loader executable
     module /boot/kernel.bin "KERNEL_BIN"        // Path to the kernel executable, the string in "" is your command line

     // More modules may be added here in the form 'module <path> "<cmdline>"'

Note: if you are using Multiboot 2, replace the above multiboot/module lines with multiboot2/module2

To build it:

grub-mkrescue -o os.iso loader_build/

See Also