The XamOS — Let’s Build an OS!!!

Nipuni Perera
8 min readSep 27, 2021

The 9th step of OS development — “User modes ”

Welcome back, Readers!!!

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

Today’s flow:

1️▶️ CPU Rings and Privilege

2▶️ Segments for User Mode

3️▶️ Setting Up For User Mode

4▶️ Entering User Mode

5▶️ Using C for User Mode Programs

▶▶ A C Library

1️▶️ CPU Rings and Privilege

You undoubtedly already know that programs on Intel x86 machines have limited capabilities and that only operating system code can execute specific tasks, but do you understand how this works in practice? The method through which the OS and CPU collaborate to limit what user-mode applications may do at x86 privilege levels. There are four privilege levels, ranging from 0 to 3, and three major resources that are protected:

  • Memory
  • I/O ports
  • The ability to execute particular machine instructions.

An x86 CPU is running under a certain privilege level at any one moment, which controls what programs can and cannot perform.These privilege levels are often described as protection rings, with the innermost ring corresponding to highest privilege.Only two privilege levels, 0 and 3, are used in most contemporary x86 kernels:

The CPU restricts around 15 machine instructions out of hundreds to ring zero. Many others have restrictions on the operands they can use. If permitted in user mode, these instructions might circumvent the protection system or otherwise cause havoc, thus they are reserved for the kernel. A general-protection exception is thrown when they are executed outside of ring zero, similar to when a program accesses incorrect memory locations. Access to memory and I/O ports is also controlled according to privilege level. But first, let’s have a look at how the CPU keeps track of the current privilege level, which includes the segment selectors discussed in the last piece. They are as follows:

The complete contents of data segment selectors are loaded directly into segment registers such as ss (stack segment register) and ds (data segment register) by code (data segment register). This includes the values of the Requested Privilege Level (RPL) column, which we’ll discuss in more detail later. The code segment register (cs), on the other hand, is a magical device. First, it can only be set by instructions that change the flow of program execution, such as call, rather than by load instructions like mov. Second, and more crucially for us, cs contains a Current Privilege Level (CPL) field that is maintained by the CPU itself, rather to an RPL variable that may be modified by code.The CPU’s current privilege level is always equal to this 2-bit CPL field in the code segment register. The Intel papers are a little shaky on this point, and online documents can also be confusing, but that’s the hard and fast rule. A glance at the CPL in cs will tell you the privilege level code is executing with at any time, regardless of what’s going on in the CPU.

It’s important to remember that CPU privileges have nothing to do with operating system users. It doesn’t matter if you’re root, Administrator, visitor, or a normal user. Regardless of the OS user on whose behalf the code executes, all user code runs in ring 3 and all kernel code runs in ring 0. Certain kernel jobs, such as user-mode device drivers in Windows Vista, can be pushed to user mode, however these are merely special processes that perform a duty for the kernel and can typically be destroyed without causing any problems.

When it’s time to return to ring 3, the kernel sends out an iret or sysexit command to exit interrupts and system calls, respectively, leaving ring 0 and allowing user code with a CPL of 3 to resume execution.

User mode is now almost within our reach, there are just a few more steps required 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.

2▶️ Segments for User Mode

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

Index |offset|Name              |Address range          |Type  |DPL
3 |0x18 |user code segment |0x00000000 -0xFFFFFFFF |RX |PL3 4 |0x20 |user data segment |0x00000000 -0xFFFFFFFF |RW |PL3

The DPL is the difference, as it now permits code to run 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.

3️▶️ 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 stage, don’t bother about building up a stack that can expand and decrease; instead, concentrate on getting a simple implementation to function.
  • The GRUB module’s binary must be transferred 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 at0X00000000 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. 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 mallocfunction in the kernel can be used to dynamically allocate this process struct.

4▶️ Entering User Mode

Executing aniret 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 though the CPU had raised an inter-privilege level interrupt to enter user mode. The stack should look something like this:

[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 usermode code to execute

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 0xC0000000and combine it with the user PDT (which only maps below 0xC0000000) upon switching. When establishing the register cr3 , keep in mind that the PDT’s physical address must be utilized.

The eflags register contains a number of distinct flags. The interrupt enable (IF) flag is the most crucial for us. In privilege level 3, the assembly code instruction sticannot 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.

Interrupts should be deactivated for the time being, since getting inter-privilege level interrupts to operate correctly takes a bit more effort.

The value eipon 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, respectively, should be the values cs andsson the stack. The RPL — the Requested Privilege Level — is the RPL of a segment selector, as we saw in the segmentation chapter. The RPL of cs and ss should be 0x3 when entering PL3 usingiret. An example is given in the following code:

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

5▶️ Using C for User Mode Programs

When using C as a programming language for user mode programs, it’s necessary 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 built 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 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. One typical solution is to put a few assembly code lines at offset 0 that call main:

extern mainsection .text
; push argv
; push argc
call main
; main has returned, eax is return value
jmp $ ; loop forever

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

OUTPUT_FORMAT("binary")    /* output flat binary */    SECTIONS
{
. = 0; /* relocate to address 0 */
.text ALIGN(4):
{
start.o(.text) /* include the .text section of start.o*/
*(.text) /* include all other .text sections */
}
.data ALIGN(4):
{
*(.data)
}
.rodata ALIGN(4):
{
*(.rodata*)
}
}

Note that *(.text) will not include the .text section of start.o again.

With this script we can write programs in C or assembler (or any other language that compiles to object files linkable with ld), and it is easy to load and map for the kernel (.rodata will be mapped in as writeable, though).

When we compile user programs we want the following GCC flags:

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

A C Library

It may now be worthwhile to consider creating a tiny “standard library” for your programs. Some of the functionality relies on system calls, while others, such as the string.h routines, do not.

Reference

Thank you very much for reading!

I’ll hope to get back to you with the article no:10 as soon as possible.Till then,

Stay Safe!!! 👋

-Nipuni Perera-

--

--

Nipuni Perera

As a Software Engineering undergrad at the University of Kelaniya SL , I share insights on coding, dev methodologies & emerging tech. Join me on my journey!