mov eax , cr0
or eax , 0x01
mov cr0 , eax



back to months list

Project : The "Microkernel" Operating System

Journal Entry Date : 2025.01.27

Today, I am going to open the new chapter of my kernel development. I am going to develop a full task management system, capable of providing the application's own virtual memory space, I/O scheduling, and whole lot more.

(At first, I wanted to set my goal to making a proper keyboard driver. However, keyboard is a "shared" I/O system, which means the tasks need to share the I/O data given from the device. This also means that you have to first implement task management system in order to develop the "proper" keyboard device. Long story short, I'm doing everything the hard way... haha)

This is my great roadmap :

  1. First, to develop a scheduling system, we first need a method of context switching. An architecture-dependent function that has two parameter of Register structure would be favorable.
  2. Second, we need a virtual memory management system that could allocate pages and also create the entire page entry for programs to have their own virtual memory space.
  3. Third, we need task management system(obviously) with proper scheduling algorithm. MLFQ would be favorable, as it has multiple layers of queues that can handle various types of programs.
  4. Finally, we need some kind of event system that can receive I/O inputs(like keyboard) from the device. We would also need a special type of device that sends the device data to kernel in event-like way(not using read(), write() functions.)

I don't have much of "concrete plan" on all of the steps, but I'm sure that we'll all figure it out as we embark on the journey of development!

Let's implement the simple things -- Context Switching.

Implementing context switching is simple. We just have to do very similar thing that we did on the interrupt wrapper. Save all the registers to the current context, and load all the registers from the next context.

The abstraction of the context switching function would be like this :

extern "C" void switch_context(struct Registers *current_context , struct Registers *next_context);

In case of using assembly to implement the function, I added the "extern C" keyword at the front. But because I'm not that smart, I will be implementing all of it with inline assembly.

The implementation is nearly identical to the interrupt routine. One difference is that now you don't have to store the registers onto the stack; you have to store it to the Register structure of the previous context pointed by the parameter. (In this case, it would be RDI register.)

__attribute__ ((naked)) void switch_context(struct Registers *current_context , struct Registers *next_context) {
    // prev_context : rdi
    // next_context : rsi
    /* Save */
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , rax)));
    __asm__ ("mov [rdi+%0] , rbx"::"i"(offsetof(struct Registers , rbx)));
    __asm__ ("mov [rdi+%0] , rcx"::"i"(offsetof(struct Registers , rcx)));
    __asm__ ("mov [rdi+%0] , rdx"::"i"(offsetof(struct Registers , rdx)));

    __asm__ ("mov [rdi+%0] , rdi"::"i"(offsetof(struct Registers , rdi)));
    __asm__ ("mov [rdi+%0] , rsi"::"i"(offsetof(struct Registers , rsi)));

    __asm__ ("mov [rdi+%0] , r8"::"i"(offsetof(struct Registers , r8)));
    __asm__ ("mov [rdi+%0] , r9"::"i"(offsetof(struct Registers , r9)));
    __asm__ ("mov [rdi+%0] , r10"::"i"(offsetof(struct Registers , r10)));
    __asm__ ("mov [rdi+%0] , r11"::"i"(offsetof(struct Registers , r11)));
    __asm__ ("mov [rdi+%0] , r12"::"i"(offsetof(struct Registers , r12)));
    __asm__ ("mov [rdi+%0] , r13"::"i"(offsetof(struct Registers , r13)));
    __asm__ ("mov [rdi+%0] , r14"::"i"(offsetof(struct Registers , r14)));
    __asm__ ("mov [rdi+%0] , r15"::"i"(offsetof(struct Registers , r15)));

    __asm__ ("mov [rdi+%0] , rbp"::"i"(offsetof(struct Registers , rbp)));
    __asm__ ("lea rax , [rsp+8]"); /* value of rsp before pushing the return address to the stack */
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , rsp)));
    
    __asm__ ("mov rax , cs");
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , cs)));
    __asm__ ("mov rax , ss");
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , ss)));
    __asm__ ("mov rax , ds");
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , ds)));
    __asm__ ("mov rax , es");
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , es)));
    __asm__ ("mov rax , fs");
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , fs)));
    __asm__ ("mov rax , gs");
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , gs)));

    __asm__ ("pushfq");          // rflags
    __asm__ ("pop [rdi+%0]"::"i"(offsetof(struct Registers , rflags)));
    __asm__ ("pop rax");         // rip
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , rip)));
    __asm__ ("mov rax , cr3");   // cr3
    __asm__ ("mov [rdi+%0] , rax"::"i"(offsetof(struct Registers , cr3)));

Few things to mention here. DON'T USE "qword[]" IN THIS KIND OF SITUATION. Somehow, for whatever unknown reason, when I write "mov qword[rdi+%0] , rax", the compiler adds 8 to the pointer, translating the assembly into "mov qword[rdi+%0+8] , rax." To fix this, we simply have to NOT use qword keyword. I don't know why this occurs, but it's a win that I fixed it somehow.

In line 23, we can see a fascinating trick I learned from this book. Basically, the value of RSP before the switch_context() function call is the RSP before the return address is pushed(which decrements RSP to 8.) Since we are in the naked function, which only pushes the return address to the stack, what we need to get the previous RSP value is to just increment the RSP to 8. Surely, we can do this by using the ADD operation, but the book I read provided a more "elegant" solution: using the LEA operation instead. LEA puts the pointer's address into the register without any memory operation. We can use this to load the address of [RSP+8] to where we desire to be, which effectively stores the value of RSP+8 with no usage of ADD operation!

In line 41, since the call of function pushes the return address into the stack, getting RIP is as simple as just popping the pushed return address from the stack!


    __asm__ ("push [rsi+%0]"::"i"(offsetof(struct Registers , ss)));     // ss
    __asm__ ("push [rsi+%0]"::"i"(offsetof(struct Registers , rsp)));    // rsp
    __asm__ ("push [rsi+%0]"::"i"(offsetof(struct Registers , rflags))); // rflags
    __asm__ ("push [rsi+%0]"::"i"(offsetof(struct Registers , cs)));     // cs
    __asm__ ("push [rsi+%0]"::"i"(offsetof(struct Registers , rip)));    // rip

    ...

    __asm__ ("iretq");

This is the piece of loading parts of the switching code, which has some things that I want to elaborate further. As you know, when the interrupt occurs, the RIP, CS, RFlags, RSP, and SS registers before the interrupt transpires are pushed into the stack, respectively. When the interrupt ends, we perform the IRETQ operation that pops back those registers stored in the stack. We can use this mechanism in context switching to easily manipulate those values of registers. We just need to push the values of registers from the new context just like the interrupt routine does, and simply perform the IRETQ operation at the end. Simple!

Now, we just have to test the context switching to see if it works properly.

    struct Registers current_context , next_context;
    memset(&next_context , 0 , sizeof(struct Registers));
    next_context.rip = (qword)context_switch_test;
    __asm__ ("mov rax , cr3");
    __asm__ ("mov %0 , rax":"=r"(next_context.cr3));
    next_context.rflags = 0x202;
    next_context.rsp = (qword)memory::pmem_alloc(4096 , 16);
    next_context.rbp = next_context.rsp;
    next_context.cs = 0x08;
    next_context.ds = 0x10;
    next_context.es = 0x10;
    next_context.fs = 0x10;
    next_context.gs = 0x10;
    next_context.ss = 0x10;
    next_context.rdi = 0xBABAB01;
    switch_context(&current_context , &next_context);
    ...
}


void context_switch_test(qword test) {
    debug::out::printf("context switch succeed! test = 0x%X\n" , test);
    while(1) {
        ;
    }
}

This code would jump to the context_switch_test() function, with the value of the parameter as 0xBABAB01.

it works!! Now, we're going to the world of paging and virutal memory management!!!!