Build your own OS (part 9)

Pasan Devin Jayawardene
5 min readSep 27, 2021

--

user modes

Hello everyone! Welcome to the fifth instalment of my OS implementing series, in which I demonstrate how to build an operating system from scratch. Now if you haven’t followed the previous four articles I strongly recommend that you follow them before reading this one. I’ll link those articles at the end of this article for your convenience.

User Mode

User mode is almost here; we just need to take a few more steps to get there. Although these processes appear simple in this chapter, they can be difficult to apply because there are many areas where little errors can lead to defects that are difficult to identify.

Segments for User Mode

We need to add two more segments to the GDT to allow user mode. They’re pretty similar to the kernel segments we added to the GDT when we built it up in the segmentation chapter:

The DPL is the difference, as it now permits code to run in PL3. The segments can still be used to address the whole address space; however, simply using them for user mode code will not protect the kernel. We’ll need paging for that.

Setting Up For User Mode

Every user mode procedure requires a few things:

  • 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. At this point, don’t bother about putting up a stack that can expand and decrease; instead, concentrate on getting a basic implementation to work.
  • The GRUB module’s binary must be copied to the page frames that house the program’s code.
  • To map the above-mentioned page frames into memory, you’ll require a page directory and page tables. Because the code and data should be mapped in at 0x00000000 and increasing, and the stack should start just below the kernel, at 0xBFFFFFFB, and increase towards lower addresses, at least two page tables are required. To allow PL3 access, the U/S flag must be set.

It could be more convenient to save this data in a struct that represents a process. The malloc function in the kernel can be used to dynamically allocate this process struct.

Entering User Mode

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).

We build up the stack as if the processor 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 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 above 0xC0000000 and merge it with the user PDT (which only maps below 0xC0000000) when switching. When establishing the register cr3, keep in mind that the PDT’s physical address must be utilized.

In section 2.3 of the Intel handbook [33], the register eflags contains a collection of distinct 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.

Interruptions should be deactivated for the time being, as inter-privilege level interrupts take a little more work (see the section “System calls”).

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 — the Requested Privilege Level — is the lowest two bits of a segment selector, as we saw in the segmentation chapter. The RPL of cs and ss should be 0x3 when using iret to enter PL3. An example can be found in the code 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’re 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.

Using C for User Mode Programs

When using C as a programming language for user mode programs, it’s crucial to consider the file structure that will be created as a result of the compilation.

Because GRUB understands how to parse and interpret the ELF file format, we may utilize it as the file format for the kernel executable. We could compile user mode applications into ELF binaries if we created an ELF parser. We’ll leave it up to the reader to figure out what to do with it.

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 solution is to place a few assembly code lines at offset 0 that call main:

If you store this code in a file named start.s, the following code shows an example of a linker script that places these instructions first in the executable (remember that start.s gets compiled to start.o):

Note that start.o’s.text section will not be included in *(.text).

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 (.rodata will be mapped in as writeable, though).

The following GCC flags are required when compiling user programs:

-m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs

For linking, the followings flags should be used:

-T link.ld -melf_i386  # emulate 32 bits ELF, the binary output is specified
# in the linker script

The option -T instructs the linker to use the linker script link.ld.

I hope you enjoyed today’s article and put it into action. I’ll see in the next article where I’ll be showing you how to integrate user modes to our OS. Until then Goodbye and Stay Safe!

-Pasan Devin Jayawardene-

--

--

Pasan Devin Jayawardene
Pasan Devin Jayawardene

Written by Pasan Devin Jayawardene

Associate Software Engineer - RnD & AI

No responses yet