Page 1 of 1

Hardware task switching issue

Posted: Sun Aug 30, 2015 12:04 pm
by thxbb12
Hello all,

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

Re: Hardware task switching issue

Posted: Sun Aug 30, 2015 9:10 pm
by BASICFreak
I have not messed with segmentation, so I will not be the best to answer this... So I won't try, I'll just supply a few references and hopefully one of them helps.

First you may find this useful http://wiki.osdev.org/Getting_to_Ring_3#Entering_Ring_3 and http://wiki.osdev.org/Context_Switching ... _Switching

Also check out (LTR) http://x86.renejeschke.de/html/file_mod ... d_163.html and (LLDT) http://x86.renejeschke.de/html/file_mod ... d_157.html
At the very least they will tell you what the GPF error could be.


Also I would highly recommend break points and stepping the code as it will give you more detail on exactly where the error occurs.
I recommend Bochs and this (or the links from it) has all the information you will ever need on the debugger http://wiki.osdev.org/Bochs


Good luck with your project and hopefully one of our more skilled members can help you more than I can.

Re: Hardware task switching issue

Posted: Mon Aug 31, 2015 12:05 am
by thxbb12
Thanks for the reply.

I read all these resources already. Using hardware tasks, it seems simpler to enter ring 3 than software switching: a far jmp to the task's TSS should do the trick (vs an iret with software switching).
I don't use the lldt instruction since the hardware should load the LDT automatically when loading the context of the new task.

As far as debuggers go, I use qemu's builtin gdb server to connect to it remotely. It works pretty well because I'm able to step into the code (but not external asm files for some reason).
The GPF occurs exactly at the following asm line in switch_task1:

jmp 0x20:0

Re: Hardware task switching issue

Posted: Mon Aug 31, 2015 3:54 am
by thxbb12
I finally found the issue!

It's really something stupid, but as the saying goes "the devil is in the details". In the task's TSS, the selectors for code and data where missing 2 things:
[*] The user DPL (0x3)
[*] The bit specifying the selector is and LDT selector (0x4)

Here is the fix:

Code: Select all

tss->cs = (0 << 3) | DPL_USER | LDT_SELECTOR;  // code segment selector in the LDT
tss->ds = tss->es = tss->fs = tss->gs = tss->ss = (1 << 3) | DPL_USER | LDT_SELECTOR;  // data
There was another detail I forgot in my code: setting the 9th bit of the eflags register in the task's TSS in order to unmask interrupts (badly needed obviously).

Now, it works like a charm.
I switched to the user task through a far call and it works fine.

I now have an additional question: how to get back to the caller task?
Given I switched to task1 using a far call, a retf should do the trick. However, it will have to be called before the task1 function ends. The compiler (gcc) generates a leave instruction to restore the stack frame and a ret instruction to get back to the caller.
Therefore, if I issue a retf, I first need to issue a "leave" followed by an "add esp,4". The task1 function then becomes:

Code: Select all

void task1() {
    // Return to the calling task
    asm volatile("leave\n add $4,%esp\n retf");
}
Unfortunately, it doesn't work (GPF). What am I missing here?

Re: Hardware task switching issue

Posted: Mon Aug 31, 2015 4:01 am
by Combuster
The easiest way is to create a wrapper stub in assembly, pretty much like how int main() is not the first thing to be called in an application.

Code: Select all

actual_start:
    call my_real_function
    retf
You can also add other bits of administration in there.

Re: Hardware task switching issue

Posted: Tue Sep 01, 2015 1:12 am
by thxbb12
Thanks a lot, I'll try that :)