Building your own Operating System (Week 09)
This is the ninth article in this series. In this article, I’m going to talk about user modes. In the previous article, I explained page frame allocation. You can check it out by clicking here. For those coding and implementation staff, I’m following the guide “The little book about OS development” by Erik Helin and Adam Renberg.
In contrast to kernel mode, user mode is the environment in which the user’s programs execute. Because this environment has less privileges than the kernel, user programs will be unable to interfere with other programs or the kernel.
Segments for User Mode
To enable user mode we need to add two more segments to the GDT.
The difference is the DPL, which now allows code to execute in PL3. The segments can still be used to address the full address space; however, just utilizing them for user mode code will not protect the kernel. We’ll need paging for that.
Setting Up For User Mode
Every user mode process requires a few things:
- Page frames for code, data and stack. At the moment it suffices to allocate one page frame for the stack and enough page frames to fit the program’s code.
- The binary from the GRUB module has to be copied to the page frames used for the programs code.
- A page directory and page tables are needed to map the page frames described above into memory. Because the code and data should be mapped in at 0x00000000 and rising, and the stack should start immediately below the kernel, at 0xBFFFFFFB, and expand towards lower addresses, at least two page tables are required.
Entering User Mode
Executing an iret or lret instruction interrupt return or long return, respectively is the only option to run code with a lower privilege level than the current privilege level (CPL). We build up the stack as though the CPU had raised an inter-privilege level interrupt to enter user mode. The stack should look something like this.
Following that, the instruction iret will read these values from the stack and fill in the appropriate registers. We need to go to the page directory we put up for the user mode process before we can run iret. It’s crucial to note that after switching PDT, the kernel must be mapped in order to continue running code. One method to do this is to create a separate PDT for the kernel that maps all data above 0xC0000000 and combine it with the user PDT upon switching. When establishing the register cr3, keep in mind that the PDT’s physical address must be utilized.
The interrupt enable (IF) flag is the most crucial for us. In privilege level 3, the assembly code instruction sti cannot be used to enable interrupts. Interrupts cannot be activated once user mode is entered if interrupts are disabled while entering user mode. Because the assembly code instruction iret sets the register eflags to the equivalent value on the stack, setting the IF flag in the eflags entry on the stack will allow interrupts in user mode.
The value eip on the stack should point to the user code’s entry point, which in our instance is 0x00000000. The value esp on the stack should be 0xBFFFFFFB, which is where the stack begins (0xC0000000–4).
The segment selectors for the user code and user data segments should be cs and ss on the stack, respectively. The RPL (Requested Privilege Level) is the lowest two bits of a segment selector. The RPL of cs and ss should be 0x3 when using iret to enter PL3. An example may be found in the code below:
Using C for User Mode Programs
Allowing user mode programs to be written in C but compiling them to flat binaries rather than ELF binaries is one way to make it easier to build user mode applications. The resulting code structure in C is more unexpected, and the entry point, main, may not be at binary offset 0 in the binary. To do that we need to use this code named “start.s”.
Then this following linker script places these instructions first in executable.
Now we can build programs in C or assembly using this script, and it’s simple to load and map for the kernel.
We need to add these GCC flags to compile user programs.
-m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles
Also, we need to add these flags for linking.
-T link.ld -melf_i386 # emulate 32 bits ELF, the binary output is specified
# in the linker script
I think you all get a good idea about user modes. You can also check my Github Repo using this link.
Thank you for reading and hope to see you in the next article as well!