Higher Half With GDT

From OSDev Wiki
Jump to: navigation, search
Difficulty level
Difficulty 2.png
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!

Personal tools
Namespaces
Variants
Actions
Navigation
About
Toolbox