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 :
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(¤t_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!!!!