Initial

Alright so I have been very busy this past month with midterms and work going like crazy, but we are so back. Now that I have interrupts working I can program the Programmable Interval Timer (PIT) to fire timer interrupts so that I can have a round robin scheduler! Lets gooo multitasking!!!!

First I set up a temporary page allocator with a large buffer in global memory because I am going to have to learn about that and page tables and such… so I am pushing it off until after this. It is nothing crazy, just got something working loosely based on the fact that I have good understanding of how the GLIBC heap and the Linux Kernel slabs work thanks to my exploitation background.

Alright cool lets get started

Scheduler

Before I get started, I want to say that everything named in my code is under the name of “thread”. I will be vulnerable and say I did not understand (or honestly cared about) the difference between “task” and “thread” until not too long ago that I looked it up.

Since I am only using one CPU core for the time being until I decide to implement Symmetric Multi-Processing (SMP), the implementation I made is technically using tasks and not threads… and for that I will tell you to respectfully fuck off!!!

Context save + restore

So obviously the most important part of a scheduler is the fact that it must be able to save the whole state of a task at the time of an interrupt before switching to a new task, and then restoring it once it is that task’s turn again. This is the struct I am using for ctx save and restore:

struct regs_context {
    uint64_t r15, r14, r13, r12, r11, r10, r9, r8;
    uint64_t rsi, rdi, rbp, rdx, rcx, rbx, rax;
    uint64_t rip, cs, rflags, rsp, ss;
};

So for that I want to tell you something very silly, at first I MANUALLY calculated all offsets in the stack and wrote it to the other MANUALLY calculated offset in the regs_context struct HAHAHA:

void mk_thread_ctx_save(struct regs_context* ctx) {

    asm volatile (
        ".intel_syntax noprefix\n\t"
        "push rax\n\t"  

        "mov rax, qword ptr [rsp + 0xa0]\n\t"  // r15
        "mov qword ptr [rdi + 0x00], rax\n\t"

        "mov rax, qword ptr [rsp + 0x98]\n\t"  // r14
        "mov qword ptr [rdi + 0x08], rax\n\t"

        "mov rax, qword ptr [rsp + 0x90]\n\t"  // r13
        "mov qword ptr [rdi + 0x10], rax\n\t"

	... MORE OF THIS ...

        "mov rax, qword ptr [rsp + 0xb8]\n\t"  // rflags
        "mov qword ptr [rdi + 0x88], rax\n\t"

        "pop rax\n\t"  // rax
        "mov qword ptr [rdi + 0x70], rax\n\t"

       ".att_syntax prefix\n\t"
        :
        : "D"(ctx)
        : "rax", "memory"
    );
}

Not only is this horribly disgusting and awful to debug, but also it made it so that if I EVER pushed anything to the stack inside of the timer handler function I would have to also change those offsets!!! It wasn’t until a bit later that I got the epiphany to move the value of RSP into RDI before calling the timer handler function in C and then using that as a stack pointer parameter for the state before the function call!!! Here is the same function but now nicer:

void mk_thread_ctx_save_from_stack(struct regs_context* ctx, uint64_t* stack) {
    ctx->rax = stack[0];
    ctx->rbx = stack[1];
    ctx->rcx = stack[2];
    ctx->rdx = stack[3];
    ctx->rbp = stack[4];
    ctx->rdi = stack[5];
    ctx->rsi = stack[6];
    ctx->r8  = stack[7];
    ctx->r9  = stack[8];
    ctx->r10 = stack[9];
    ctx->r11 = stack[10];
    ctx->r12 = stack[11];
    ctx->r13 = stack[12];
    ctx->r14 = stack[13];
    ctx->r15 = stack[14];
    
    // Interrupt frame
    ctx->rip    = stack[15];
    ctx->cs     = stack[16];
    ctx->rflags = stack[17];

    // CS & 3 gives us the RPL (requested privilege level)
    if ((ctx->cs & 0x3) != 0) {
        ctx->rsp = stack[18];
        ctx->ss  = stack[19];
    } else {
        ctx->rsp = (uint64_t)(&stack[15]);
        
        uint64_t current_ss;
        asm volatile("mov %%ss, %0" : "=r"(current_ss));
        ctx->ss = current_ss;
    }
}

So much nicer!!!

Scheduler

I don’t want this blog to be crazy long so I will only go over the important things, things like thread create, thread kill, etc are self explanatory so I will disregard. I will say in thread create I use the temporary page allocator I talked about earlier to allocate a new stack for each task.

In here, I will talk about two main important functions: the timer ISR and the context switch function. Here is the thread object structure:

struct mk_thread_obj {
    enum ThreadState state;
    struct regs_context regs;

    uint8_t started;
    uint32_t time_slice;

    void* entry;

    uint8_t* stack_base;
};

All the timer ISR function does is time slicing, initializing tasks that havent ran yet, and also switch context in between tasks if needed. This is the code:

void mk_timer_int_handler(uint64_t* stack) {
    
    mk_working_thread = mk_get_working_thread();
    
    if (mk_working_thread->time_slice > 0) {
        mk_working_thread->time_slice -= 1;

        mk_pic_send_eoi(0);

        return;
    }
        
    if (!mk_working_thread->started) {
        mk_pic_send_eoi(0);
        mk_working_thread->started = 1;

        mk_thread_ctx_restore_from_stack(&mk_working_thread->regs, stack);

        return;
    }
    
    mk_thread_ctx_save_from_stack(&mk_working_thread->regs, stack);

    mk_pic_send_eoi(0);

    if (!mk_thread_ctx_switch()) {
        mk_working_thread = mk_get_working_thread();

        mk_thread_ctx_restore_from_stack(&mk_working_thread->regs, stack);
    }
}

The context switch function will find the next ready thread in the ready queue, which is currently a set array in global memory, but once I have a slab allocator set up I will refactor all of this to use a linked list structure so there is unlimited tasks.

Outside of that it works!! A lot of debugging but that is part of the process.