Higher Half With GDT
| Difficulty level |
|---|
Medium |
Contents |
Introduction
In this tutorial we'll write a very simple higher-half kernel using TimRobinson's GDT trick. This will allow us to load our kernel at some high address (e.g 0xC0000000) without enabling paging. Later, when we have done all our "critical" jobs, we'll enable paging and use a "normal" GDT.
The sources are very well commented and may be compiled with NASM/YASM and GCC.
The assembler part
This is our first file, start.asm. Here we define the Multiboot header and some useful functions, such as gdt_flush. Once GRUB has loaded the kernel in memory, we use the lgdt instruction to load the "fake" GDT, that will allow us to jump into our kernel. The CPU will add the base 0x40000000 to every function, so it will jump to address 0xC0100000 + 0x40000000 = 0x100000.
Note the .setup section: the CPU always needs the physical address of the GDT, but we are linking all the kernel in the higher half. How do we manage this? Simple, just link the GDT within a special section with identical virtual and physical addresses.
[BITS 32] ; 32 bit code [section .text] ; keep NASM happy [global start] ; make 'start' function global [extern kmain] ; our C kernel main ; Multiboot constants MULTIBOOT_PAGE_ALIGN equ 1<<0 MULTIBOOT_MEMORY_INFO equ 1<<1 MULTIBOOT_HEADER_MAGIC equ 0x1BADB002 MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO MULTIBOOT_CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS) ; Multiboot header (needed to boot from GRUB) ALIGN 4 multiboot_header: dd MULTIBOOT_HEADER_MAGIC dd MULTIBOOT_HEADER_FLAGS dd MULTIBOOT_CHECKSUM ; the kernel entry point start: ; here's the trick: we load a GDT with a base address ; of 0x40000000 for the code (0x08) and data (0x10) segments lgdt [trickgdt] mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ; jump to the higher half kernel jmp 0x08:higherhalf higherhalf: ; from now the CPU will translate automatically every address ; by adding the base 0x40000000 mov esp, sys_stack ; set up a new stack for our kernel call kmain ; jump to our C kernel ;) ; just a simple protection... jmp $ [global gdt_flush] ; make 'gdt_flush' accessible from C code [extern gp] ; tells the assembler to look at C code for 'gp' ; this function does the same thing of the 'start' one, this time with ; the real GDT gdt_flush: lgdt [gp] mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax jmp 0x08:flush2 flush2: ret [section .setup] ; tells the assembler to include this data in the '.setup' section trickgdt: dw gdt_end - gdt - 1 ; size of the GDT dd gdt ; linear address of GDT gdt: dd 0, 0 ; null gate db 0xFF, 0xFF, 0, 0, 0, 10011010b, 11001111b, 0x40 ; code selector 0x08: base 0x40000000, limit 0xFFFFFFFF, type 0x9A, granularity 0xCF db 0xFF, 0xFF, 0, 0, 0, 10010010b, 11001111b, 0x40 ; data selector 0x10: base 0x40000000, limit 0xFFFFFFFF, type 0x92, granularity 0xCF gdt_end: [section .bss] resb 0x1000 sys_stack: ; our kernel stack
The C part
At this point the CPU will be executing our kmain function. For this tutorial it will be very small and easy: just call some functions and print a message.
// We declare a pointer to the VGA array and its attributes unsigned short *video = (unsigned short *)0xB8000; // We could also use the virtual address 0xC00B8000 unsigned char attrib = 0xF; // White text on black background // Extern functions void gdt_install(); void init_paging(); // Clears the screen void cls() { int i = 0; for (i = 0; i < 80 * 25; i++) video[i] = (attrib << 8) | 0; } // Prints the welcome message ;) void helloworld() { char msg[] = "Hello, World!"; int i = 0; for (i = 0; msg[i] != '\0'; i++) video[i] = (attrib << 8) | msg[i]; } // Our kernel's first function: kmain void kmain() { // FIRST enable paging and THEN load the real GDT! init_paging(); gdt_install(); // We clear the screen and print our welcome message cls(); helloworld(); // Hang up the computer for (;;); }
Note that you must enable paging before loading the new GDT as the CPU, once loaded the GDT with 0x0 as base address, will be still using virtual addresses and if paging isn't enabled... triple fault!
// Declare the page directory and a page table, both 4kb-aligned unsigned long kernelpagedir[1024] __attribute__ ((aligned (4096))); unsigned long lowpagetable[1024] __attribute__ ((aligned (4096))); // This function fills the page directory and the page table, // then enables paging by putting the address of the page directory // into the CR3 register and setting the 31st bit into the CR0 one void init_paging() { // Pointers to the page directory and the page table void *kernelpagedirPtr = 0; void *lowpagetablePtr = 0; int k = 0; kernelpagedirPtr = (char *)kernelpagedir + 0x40000000; // Translate the page directory from // virtual address to physical address lowpagetablePtr = (char *)lowpagetable + 0x40000000; // Same for the page table // Counts from 0 to 1023 to... for (k = 0; k < 1024; k++) { lowpagetable[k] = (k * 4096) | 0x3; // ...map the first 4MB of memory into the page table... kernelpagedir[k] = 0; // ...and clear the page directory entries } // Fills the addresses 0...4MB and 3072MB...3076MB of the page directory // with the same page table kernelpagedir[0] = (unsigned long)lowpagetablePtr | 0x3; kernelpagedir[768] = (unsigned long)lowpagetablePtr | 0x3; // Copies the address of the page directory into the CR3 register and, finally, enables paging! asm volatile ( "mov %0, %%eax\n" "mov %%eax, %%cr3\n" "mov %%cr0, %%eax\n" "orl $0x80000000, %%eax\n" "mov %%eax, %%cr0\n" :: "m" (kernelpagedirPtr)); }
And here is the gdt_install function. I think it doesn't need any explanation as it has been taken from Bran's Kernel Development Tutorial, so read that if you need more information.
// Defines the structures of a GDT entry and of a GDT pointer struct gdt_entry { unsigned short limit_low; unsigned short base_low; unsigned char base_middle; unsigned char access; unsigned char granularity; unsigned char base_high; } __attribute__((packed)); struct gdt_ptr { unsigned short limit; unsigned int base; } __attribute__((packed)); // We'll need at least 3 entries in our GDT... struct gdt_entry gdt[3]; struct gdt_ptr gp; // Extern assembler function void gdt_flush(); // Very simple: fills a GDT entry using the parameters void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran) { gdt[num].base_low = (base & 0xFFFF); gdt[num].base_middle = (base >> 16) & 0xFF; gdt[num].base_high = (base >> 24) & 0xFF; gdt[num].limit_low = (limit & 0xFFFF); gdt[num].granularity = ((limit >> 16) & 0x0F); gdt[num].granularity |= (gran & 0xF0); gdt[num].access = access; } // Sets our 3 gates and installs the real GDT through the assembler function void gdt_install() { gp.limit = (sizeof(struct gdt_entry) * 3) - 1; gp.base = (unsigned int)&gdt; gdt_set_gate(0, 0, 0, 0, 0); gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); gdt_flush(); }
The linker script
Our last file is the linker script. Let's give a look at it:
OUTPUT_FORMAT("elf32-i386")
ENTRY(start)
SECTIONS
{
. = 0x100000;
.setup :
{
*(.setup)
}
. += 0xC0000000;
.text : AT(ADDR(.text) - 0xC0000000)
{
*(.text)
}
.data ALIGN (4096) : AT(ADDR(.data) - 0xC0000000)
{
*(.data)
*(.rodata*)
}
.bss ALIGN (4096) : AT(ADDR(.bss) - 0xC0000000)
{
*(COMMON*)
*(.bss*)
}
}
As you can see, we link the .setup section to the lower half (0x100000) and the whole kernel to the higher half (0xC0100000). A simple and elegant solution!
Conclusion
I hope this tutorial will help you writing your own higher half kernel. For any question please post on the forum!