WVB Operating System(Ninth Stage)

Hello! Readers. Welcome back to my ninth article on operating system implementation. In the last article, We discussed Page Frame Allocation, Managing Available Memory, and the Kernel Heap. This article is going to explain how to enable the user modes in an operating system.

User mode is almost within reach now, it only takes a few steps to get there. They can be difficult to implement since there are numerous areas where little errors might lead to bugs that are difficult to locate. We need to add two more segments to the GDT to allow user mode. When we set up things for user mode, There are a few things every user-mode process needs.

  1. Code, data, and stack all have their own page frames. For the time being, allotting one-page frame for the stack and enough page frames to fit the program’s code is sufficient.
  2. The GRUB module’s binary must be copied to the page frames that house the program’s code.
  3. In order to map the page frames mentioned above into memory, you’ll need a page directory and page tables. Because the code and data should be mapped in starting at 0x00000000 and increasing at least two-page tables are required, and the stack should begin just below the kernel, at 0xBFFFFFFB and rise towards lower locations.

We build up the stack as if the processor had raised an inter-privilege level interrupt to enter user mode. The stack should resemble the one below.

Executing an “iret” or “lret” instruction interrupt return or long return respectively is the sole option to run code with a lower privilege level than the current privilege level (CPL). Following that, the instruction “iret” will read these values from the stack and fill in the appropriate registers. We need to shift to the page directory we put up for the user-mode process before we can run “iret”. It’s vital to remember that after switching PDT, the kernel must be mapped in order to continue running code. One approach to do this is to create a separate PDT for the kernel that maps all data at 0xC0000000 and higher, then merge it with the user PDT when switching.

The register “eflags” contains a set of different flags, The interrupt enable (IF) flag is the most critical for us. In privilege level 3, the assembly code instruction “sti” cannot be used to enable interrupts. Interrupts cannot be enabled once user mode is entered if interrupts are disabled upon entering user mode. Because the assembly code instruction “iret” sets the register “eflags” to the matching value on the stack, setting the IF flag in the “eflags” entry on the stack will enable interrupts in user mode.

The segment selectors for the user code and user data segments should be cs and ss on the stack, respectively. We know that the lowest two bits of a segment selector are RPL (the Requested Privilege Level). When using “iret” to enter “PL3”, the RPL of “cs”and “ss” should be “0x3”. Example code look like below.

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 the traditional method. We are now ready to put “iret” into action. We should now have a kernel that can enter user mode if everything has been set up correctly.

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 shows an example of a linker script that places these instructions first in executable

Using this script we can write programs in C or assembler, and it is easy to load and map for the kernel. The following GCC flags are required when compiling user programs.

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

Thank you for reading !

References:

Software Engineering Undergraduate at University of Kelaniya Sri Lanka