Build your own operating system #9_user_modes

Sadisha Nimsara
6 min readSep 27, 2021

Welcome to the 9th article of this article series. In this article, we are going to discuss about user modes. In previous article, we discussed about how you can implement page frame allocation into your operating system.

Lets Go…

User mode

User mode is currently nearly inside our range, there are only a couple of more advances needed to arrive. Although these steps may appear to be simple, but the way they are introduced in this part, they can be interesting to carry out, since there are a ton of spots where little mistakes will cause messes with that are elusive.

Segments for User Mode

In order to enable user modes in our operating system, we need to add two more segments to our GDT(Global Descriptor table). They are very similar to the kernel segments we added when we set up the GDT in the chapter about segmentation:

segments

As you can see, the main difference is the DPL. Now, which will allows code to execute in PL3. The segments can still be used to address the entire address space, just using these segments for user mode code will not protect the kernel. For that, we need to use paging.

Setting up for user mode

There are a few major things that every user mode process needs:

  • 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. Don’t worry about setting up a stack that can be grow and shrink at this point in time, focus on getting a basic implementation work first.
  • 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. 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.

It might be easy to store this information in a struct representing a process. This process struct can be dynamically allocated with the kernel’s malloc function.

Entering user mode

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

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

Then the instruction iret will 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.

The register eflags contains a set of different flags. Most important for us is the interrupt enable (IF) flag. The assembly code instruction sti can’t be used in privilege level 3 for enabling interrupts. If interrupts are disabled when entering user mode, then interrupts can’t be enabled once user mode is entered. Setting the IF flag in the eflags entry on the stack will enable interrupts in user mode since the assembly code instruction iret will set the register eflags to the corresponding value on the stack.

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 qualities cs and ss on the stack ought to be the fragment selectors for the client code and client information sections, separately. As we found in the division part, the least two pieces of a section selector is the RPL — the Requested Privilege Level. When utilizing iret to enter PL3, the RPL of cs and ss ought to be 0x3. The accompanying code shows a model:

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 register ds, and the other data segment registers, should be set to the same segment selector as ss. They can be set the ordinary way, with the mov assembly code instruction.

We are now ready to execute iret. If everything has been set up right, we should now have a kernel that can enter user mode.

Using C for User Mode Programs

If you intend to user C language for writing the there are few things that you need know. The first thing is the output format and the structure of the object file after the compilation.

Because GRUB knows how to parse and interpret the ELF file format, we can use this format as the file format for the kernel executable. We could compile user mode programs into ELF binaries if we implemented an ELF parser. But this method is much complex. But there is another simpler way rather than this.

One thing we can do to make it easier to develop user mode programs is to allow the programs to be written in C, but compile them to flat binaries instead of ELF binaries. In C the layout of the generated code is more unpredictable and the entry point, main, might not be at offset 0 in the binary. One common way to work around this is to add a few assembly code lines placed at offset 0 which calls main:

extern main

section .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*)
}
}

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.

Catch you in the next article.

Thank you..!

--

--