YmeOS — Let’s Build our own OS !!!😉

Thushara Samaraweera
5 min readSep 27, 2021

#8-User Mode

Hi developer!!!

Welcome back to my 9th article on operating system implementation. In the last article, We discussed Page Frame Allocation, Managing Available Memory, and the Kernel Heap.

If you’re new to my article series you better come from this first article. Otherwise, it would be hard to understand what’s going on.🤔

User mode is almost within reach now, it only takes a few steps to get there.Although these steps might seem easy they way they are presented in this chapter, they can be tricky to implement, since there are a lot of places where small errors will cause bugs that are hard to find.

Segments for User Mode

The very first thing that we need to do is add two more segments to the GDT. They are very similar to what we have done in segmentation stage when we were setting up the GDT for the first time.

Setting Up For User Mode

When we set up things for user mode, There are a few things every user-mode process needs.

  1. Page frames from the RAM for store instructions, data and stack. In here since we are focusing on basic implementation it is fine one page frame for the stack and enough page frames for the instructions or program codes.
  2. The GRUB module’s binary must be copied to the page frames that house the program’s code.
  3. A page directory and page tables are needed to map the page frames described above into memory. At least two page tables are needed, because the code and data should be mapped in at 0x00000000 and increasing, and the stack should start just below the kernel, at 0xBFFFFFFB, growing towards lower addresses. The U/S flag has to be set to allow PL3 access.

Entering User Mode

The only way to execute code with a lower privilege level than the current privilege level (CPL) is to execute an iret or lret instruction - interrupt return or long return, respectively.

To enter user mode we set up the stack as if the processor had raised an inter-privilege level interrupt. The stack should look like the following

[esp + 16] ss ;the stack segment selector we want for user mode
[esp + 12] esp ;the user mode stack pointer
[esp + 8] eflags ;the control flags we want to use in user mode
[esp + 4] cs ;the code segment selector
[esp + 0] eip ;the instruction pointer of user mode code to execute

The instruction iret will then read these values from the stack and fill in the corresponding registers. Before we execute iret we need to change to the page directory we setup for the user mode process. It is important to remember that to continue executing kernel code after we’ve switched PDT, the kernel needs to be mapped in. One way to accomplish this is to have a separate PDT for the kernel, which maps all data at 0xC0000000 and above, and merge it with the user PDT (which only maps below 0xC0000000) when performing the switch. Remember that physical address of the PDT has to be used when setting the register cr3.

For now, we should have interrupts disabled, as it requires a little more work to get inter-privilege level interrupts to work properly .

The value eip on the stack should point to the entry point for the user code - 0x00000000 in our case. The value esp on the stack should be where the stack starts - 0xBFFFFFFB (0xC0000000 - 4).

The values cs and ss on the stack should be the segment selectors for the user code and user data segments, respectively. As we saw in the segmentation chapter, the lowest two bits of a segment selector is the RPL - the Requested Privilege Level. When using iret to enter PL3, the RPL of cs and ss should be 0x3. The following code shows an example:

USER_MODE_CODE_SEGMENT_SELECTOR equ 0x18
USER_MODE_DATA_SEGMENT_SELECTOR equ 0x20
mov cs, USER_MODE_CODE_SEGMENT_SELECTOR | 0x3
mov ss, USER_MODE_DATA_SEGMENT_SELECTOR | 0x3

The segment selector for register ds, as well as the other data segment registers, should be the same as for ss. The mov assembly code instruction can be used to set them up as we have done previously.

After completing these requirements you can execute the iret instructions. should now have a kernel that can enter user mode if everything has been set up correctly.

Using C for User Mode Programs

When C is used as the programming language for user-mode programs, it is important to think about the structure of the file that will be the result of the compilation. Because GRUB knows how to parse and interpret the ELF file format, we may utilize it for the kernel executable. We could compile the user mode programs into ELF binaries if we created an ELF parser. We’ll leave it up to the reader to figure out what to do with this.

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 write user-mode programs. The produced code arrangement in C is more unexpected, and the entry point, main, may not be at binary offset 0 in the binary. One popular workaround is to add a few assembly code lines at offset 0 that call the “kmain” function. Assembly code would be like this.

If this code is saved in a file called start.s, then the following code show an example of a linker script that places these instructions first in executable (remember that start.s gets compiled to start.o):

We can develop programs in C or assembly or any other language that compiles to object files that can be linked with ld with this script, and it’s simple to load and map for the kernel.

When we compile user programs we need following GCC flags to execute it:

Followings flags should be used for linking:

For linking, the followings flags should be used and The option “-T” instructs the linker to use the linker script “link.ld”.

Now your operating system can switch between user mode and kernel mode!

Congratulations!

References:

Thank you very much for reading!

Hope to see you again with next chapter. Till then, STAY SAFE!!!

Happy reading …

-Thushara Samaraweera-

--

--