I'm trying to implement a very simple monotask kernel that uses hardware task switching and only segmentation for protection (no paging for now). The goal is to have very simple code and concepts. However, I'm not able to successfully switch to a task (whether a ring0 or ring3 task) without getting a GPF. I set up an IDT and catch exceptions as well as hardware interrupts (0-15); this part works fine.
After boot, the kernel code is loaded at 0x100000. The kernel data (bss, etc.) sections are located after the code. The stack pointer (esp) is initialized 1MB after the kernel code. That should be plenty.
I set up a GDT with the following 6 entries (for a single task):
0: null segment
1: kernel code segment (4GB, starting at 0)
2: kernel data segment (4GB, starting at 0)
3: initial kernel TSS (ss = kernel segment; esp = 0x300000)
4: task TSS
5: task LDT
The last 2 entries are for the task. Of course the task TSS is filled appropriately as well as the GDT (see code below).
After setting up the GDT as above, I load it with the lgdt instruction and load the task register with the ltr instruction. The task LDT contains 2 segments of 1MB each: code and data, both overlapping. I also reserve 1MB of kernel stack for the task.
To switch to the task all I do is a far jmp to the task's TSS selector. However, when I do so, I get a GPF. I don't find the Intel manual very clear as to how to implement hardware task switching. The CPU should save the current context in the initial kernel TSS and load all the registers with the content of the task's TSS, and finally jump to the eip offset from the cs selector present in the TSS.
First question: did I miss anything in the steps explained above that would explain why a GPF is triggered?
I posted the relevant code below.
Second question: Is there something obviously wrong in my code? I went through it several times, but from my understanding of how hardware task switching works, I don't see it.
Thank you very much in advance!
Code: Select all
##################################################################################
GDT code
##################################################################################
typedef struct gdt_entry_st {
uint16_t lim15_0;
uint16_t base15_0;
uint8_t base23_16;
uint8_t type : 4;
uint8_t s : 1;
uint8_t dpl : 2;
uint8_t present : 1;
uint8_t lim19_16 : 4;
uint8_t avl : 1;
uint8_t l : 1;
uint8_t db : 1;
uint8_t granularity : 1;
uint8_t base31_24;
} __attribute__((packed)) gdt_entry_t;
// Structure describing a pointer to the GDT descriptor table.
// This format is required by the lgdt instruction.
typedef struct gdt_ptr_st {
uint16_t limit; // Limit of the table (ie. its size)
uint32_t base; // Address of the first entry
} __attribute__((packed)) gdt_ptr_t;
static gdt_entry_t gdt[6];
static gdt_ptr_t gdt_ptr;
tss_t initial_tss;
task_t task;
static void set_entry(gdt_entry_t *entry, uint32_t base, uint32_t limit, uint8_t type, uint8_t s, uint8_t db, uint8_t granularity, uint8_t dpl) {
// For a TSS and LDT, base is the addresse of the TSS/LDT structure
// and limit is the size of the structure.
entry->lim15_0 = limit & 0xffff;
entry->base15_0 = base & 0xffff;
entry->base23_16 = (base >> 16) & 0xff;
entry->type = type; // See TYPE_xxx flags
entry->s = s; // 1 for segments; 0 for system (TSS, LDT, gates)
entry->dpl = dpl; // privilege level
entry->present = 1; // present in memory
entry->lim19_16 = (limit >> 16) & 0xf;
entry->avl = 0; // available for use
entry->l = 0; // should be 0 (64-bit code segment)
entry->db = db; // 1 for 32-bit code and data segments; 0 for system (TSS, LDT, gate)
entry->granularity = granularity; // granularity of the limit value: 0 = 1 byte; 1 = 4096 bytes
entry->base31_24 = (base >> 24) & 0xff;
}
static void set_null_segment(gdt_entry_t *entry) {
memset(entry, 0, sizeof(gdt[0]));
}
static void set_code_segment(gdt_entry_t *entry, uint32_t base, uint32_t limit, uint8_t dpl) {
set_entry(entry, base, limit, TYPE_CODE_EXECREAD, S_CODE_OR_DATA, DB_SEG, 1, dpl);
}
static void set_data_segment(gdt_entry_t *entry, uint32_t base, uint32_t limit, uint8_t dpl) {
set_entry(entry, base, limit, TYPE_DATA_READWRITE, S_CODE_OR_DATA, DB_SEG, 1, dpl);
}
// Can only be set in the GDT!
static void set_tss_entry(gdt_entry_t *entry, tss_t *tss, uint8_t dpl) {
set_entry(entry, (uint32_t)tss, sizeof(tss_t)-1, TYPE_TSS, S_SYSTEM, DB_SYS, 0, dpl);
}
// Can only be set in the GDT!
static void set_ldt_entry(gdt_entry_t *entry, uint32_t base, uint32_t limit, uint8_t dpl) {
set_entry(entry, base, limit, TYPE_LDT, S_SYSTEM, DB_SYS, 0, dpl);
}
void gdt_init() {
gdt_ptr.limit = sizeof(gdt)-1;
gdt_ptr.base = (uint32_t)&gdt;
// Entries for null segment, kernel code and kernel data segments
set_null_segment(&gdt[0]); // null segment
set_code_segment(&gdt[1], 0x0, 0xFFFFF, DPL_KERNEL); // kernel code segment; 4GB limit
set_data_segment(&gdt[2], 0x0, 0xFFFFF, DPL_KERNEL); // kernel data segment (also used by the stack); 4GB limit
// Entry for initial kernel TSS
memset(&initial_tss, 0, sizeof(tss_t));
initial_tss.esp0 = 0x300000;
initial_tss.ss0 = GDT_KERNEL_DATA_SELECTOR;
set_tss_entry(&gdt[3], &initial_tss, DPL_KERNEL);
// Task 1
memset(task, 0, sizeof(task_t)); // Clears the whole task_t structure
task->id = 0;
// Add the task's TSS and LDT to the GDT
int gdt_tss_idx = 4;
int gdt_ldt_idx = 5;
set_tss_entry(&gdt[gdt_tss_idx], &task->tss, DPL_KERNEL);
set_ldt_entry(&gdt[gdt_ldt_idx], (uint32_t)task->ldt, sizeof(task->ldt)-1, DPL_KERNEL);
// Define code and data segments in the LDT
set_code_segment(&task->ldt[0], 0x400000, 0x100, DPL_USER); // code
set_data_segment(&task->ldt[1], 0x400000, 0x100, DPL_USER); // data + stack
// Store the TSS selector for easy referencing later when loading the task register
task->tss_selector = gdt_tss_idx << 3;
// Initialize the TSS fields
tss_t *tss = &task->tss;
task->tss.ldt_selector = gdt_ldt_idx << 3; // The LDT selector must point to the task's LDT
tss->eip = 0;
tss->esp = 0x40000; // 256KB stack
tss->ebp = tss->esp;
tss->cs = 0 << 3; // code segment selector in the LDT
tss->ds = 1 << 3; // data segment selector in the LDT
tss->es = tss->ds;
tss->fs = tss->ds;
tss->gs = tss->ds;
tss->ss = tss->ds;
tss->eflags = 0;
tss->ss0 = GDT_KERNEL_DATA_SELECTOR;
tss->esp0 = (uint32_t)(task->kernel_stack) + TASK_KERNEL_STACK_SIZE;
// Load the GDT
gdt_flush(&gdt_ptr);
// Load the task register to point to the initial TSS selector.
load_task_register(3 << 3);
}
##################################################################################
Task code
##################################################################################
// Task-State Segment (TSS) structure.
typedef struct tss_st {
uint16_t previous_task_link, reserved0;
uint32_t esp0;
uint16_t ss0, reserved1;
uint32_t esp1;
uint16_t ss1, reserved2;
uint32_t esp2;
uint16_t ss2, reserved3;
uint32_t cr3;
uint32_t eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
uint16_t es, reserved4;
uint16_t cs, reserved5;
uint16_t ss, reserved6;
uint16_t ds, reserved7;
uint16_t fs, reserved8;
uint16_t gs, reserved9;
uint16_t ldt_selector, reserved10;
uint16_t reserved11, iomap_base_addr;
} __attribute__ ((packed)) tss_t;
// A task
typedef struct task_st {
gdt_entry_t ldt[2]; // task code and segment descriptors
uint32_t id; // ID of the task (starting at 1)
tss_t tss; // Context of the task
uint16_t tss_selector; // selector required when switching to the task
uint8_t kernel_stack[TASK_KERNEL_STACK_SIZE];
} __attribute__((aligned(4))) task_t;
##################################################################################
Kernel code
##################################################################################
void task1() {
for (;;);
}
void kernel_main() {
...
gdt_init();
...
// For testing, simply copy 100 bytes worth of instructions from the task1 function
// to the task1's entry point (physical address 0x400000)
memcpy((void *)0x400000, task1, 100);
switch_task1();
for (;;);
}
##################################################################################
Assembly code
##################################################################################
load_task_register:
mov eax,[esp+4]
ltr ax
ret
switch_task1:
jmp 0x20:0
ret
gdt_flush:
mov eax,[esp+4] ; Get the pointer to the GDT, passed as a parameter.
lgdt [eax] ; Load the new GDT pointer
mov ax,KERN_DATA_DESC ; offset in the GDT of the kernel data segment
mov ds,ax ; Load all data segment selectors
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
jmp KERN_CODE_DESC:.flush ; far jump [descriptor:offset]
.flush:
ret