We’ll spend some time in class today setting up our kernel so it can run the init program in user mode.
That means init won’t be able to directly access kernel memory, so it’s running more like a regular process.
Download and extract usermode.tar.gz to get the starter code for this task. We’ll spend time in class filling in some critical pieces, but the starter code should save you some time.
We’ll discuss the role of the global descriptor table and how it connects to user and kernel mode in class.
The starter code includes two files, gdt.h and gdt.c, that allow us to set up a GDT.
We’ll fill in the gdt_setup() function to create and load a GDT that allows us to jump back and forth between kernel and user mode.
We used the provided functions that create descriptors in the GDT, then a short bit of inline assembly to load the new GDT.
void gdt_setup() {
// Zero out the gdt
memset(gdt, 0, sizeof(gdt));
// Create the kernel code and data descriptors
gdt_code_descriptor(KERNEL_CODE_SELECTOR, false);
gdt_data_descriptor(KERNEL_DATA_SELECTOR, false);
// Create the user code and data descriptors
gdt_code_descriptor(USER_CODE_SELECTOR, true);
gdt_data_descriptor(USER_DATA_SELECTOR, true);
// Load the GDT
gdt_record_t record = {
.sz = gdt_size - 1,
.base = gdt
};
__asm__("lgdt %0" :: "m"(record));
}
Make sure you call gdt_setup() from kernel’s _start function.
Please also update your idt_set_entry() function so it uses the appropriate descriptor from the new GDT.
We were using 0x28 because that’s the appropriate selector in the GDT the bootloader set up for us, but our new selector should be KERNEL_CODE_SELECTOR so interrupt handlers run in kernel mode.
Update: there is one other change you need to make to your IDT after switching to user mode. You should set the DPL field to 3 for any interrupt handler that is allowed to run from user mode, which should be all of them. I misread the Intel manual and thought this was specifying what privelege level the interrupt handler runs at, but that’s determined by the selector above.
The starter code includes the source file usermode_entry.s, which provides an assembly function to jump into user mode.
We’ll discuss the implementation of this function in class and walk through the process of using it to jump to the init program’s entry point during a switch to user mode.
Here is an example call to usermode_entry that jumps to the entry point for an ELF file.
The calls to vm_map are setting up space that will be used as the program stack.
// Pick an arbitrary location and size for the user-mode stack
uintptr_t user_stack = 0x70000000000;
size_t user_stack_size = 8 * PAGESIZE;
// Map the user-mode-stack
for(uintptr_t p = user_stack; p < user_stack + user_stack_size; p += 0x1000) {
// Map a page that is user-accessible, writable, but not executable
vm_map(read_cr3() & 0xFFFFFFFFFFFFF000, p, true, true, false);
}
// And now jump to the entry point
usermode_entry(USER_DATA_SELECTOR | 0x3, // User data selector with priv=3
user_stack + user_stack_size - 8, // Stack starts at the high address minus 8 bytes
USER_CODE_SELECTOR | 0x3, // User code selector with priv=3
header->entry); // Jump to the entry point specified in the ELF file
Finally, we need to add a bit more to our GDT to allow user-mode programs to issue system calls that return to kernel mode. We’ll set up a task-state segment (TSS) and add a TSS descriptor our GDT, which is required by the processor whenever an interrupt changes privilege level.
With the changes above we are able to switch to user mode, but system calls and interrupts do not work yet. That’s because the processor expects to find a task-state segment that tells it what stack to use when it switches back to kernel mode. The provided code includes a TSS struct and global, but we need to add a TSS descriptor to the GDT before loading it:
void gdt_setup() {
// Zero out the gdt
memset(gdt, 0, sizeof(gdt));
// Create the kernel code and data descriptors
gdt_code_descriptor(KERNEL_CODE_SELECTOR, false);
gdt_data_descriptor(KERNEL_DATA_SELECTOR, false);
// Create the user code and data descriptors
gdt_code_descriptor(USER_CODE_SELECTOR, true);
gdt_data_descriptor(USER_DATA_SELECTOR, true);
// NEW: create a TSS descriptor
gdt_tss_descriptor(TSS_SELECTOR, &tss);
...
}
After loading the GDT (in the code for gdt_setup) we can then fill in the TSS and load it:
void gdt_setup() {
...
// Zero out the TSS
memset(&tss, 0, sizeof(tss));
// Interrupts delivered while in user mode should use this stack pointer
tss.rsp0 = (uintptr_t)interrupt_stack + sizeof(interrupt_stack) - 8;
// Load the TSS
__asm__("ltr %%ax" :: "a"(TSS_SELECTOR));
}
You’ll need to create an interrupt_stack global to reserve space for a stack.
Here’s the global I used:
// Reserve space for interrupt handlers to use as a stack
uint8_t interrupt_stack[0x8000];
If everything works as expected, you’ll be left with a kernel that runs the init program just as it did before.
We should always check our work, and the easiest way to do that for this task is to make sure memory protection actually works.
We can test memory protection by mapping some memory at a known address and checking whether the init program can access it.
First, we’ll pick an address and map a page at that address that is accessible from user mode.
Do this before loading and jumping to init:
uintptr_t test_page = 0x400000000;
vm_map(read_cr3() & 0xFFFFFFFFFFFFF000, test_page, true, true, false);
Then in init, we’ll try to access that page:
char* test_page = (char*)0x400000000;
test_page[0] = 'h';
test_page[1] = 'e';
test_page[2] = 'l';
test_page[3] = 'l';
test_page[4] = 'o';
test_page[5] = '\n';
syscall(SYS_write, 1, test_page, 6);
If everything is working, init should print “hello” to the terminal.
Next, we’ll modify the test so the test page is mapped as kernel-only instead of being user accessible.
Use the same code, but change the third parameter to vm_map to be false.
With this test, your init program should generate a page fault.