Build Our Own Operating System #part 09

User Mode

Waruni Lalendra
6 min readSep 27, 2021

Hello everyone! Hope you are doing well. Welcome back to the 9th step of developing our own operating system. Today we are going to discuss about how to run a simple program in our operating system in user mode. I hope you all remember that few weeks ago we execute a program in our operating system in kernel mode. In that section I told you that we will run a program in user mode later. Well, Today is that day! Let’s get start!

User Mode

I hope you have a better idea about what is user mode since I have explained it in the article Integrate-user-modes. I you wish to refresh your knowledge again, the article is here. As you can understand even though we haven’t run a program in user-mode we are already half way there. But the part that we have to configure today is tricky and time consuming. Let’s discuss everything one by one clearly.

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

You need to add these two segments to enable the user mode

I hope you can see where is this change happens. Now the DPL is PL3. But before that all the DPL were set to PL0(kernel mode). But I like to remind you that user program cannot access to kernel space in it’s address space! We need to prevent the user program interfering the kernel space for the security of our operating system which is mandatory. We can user paging for achieve this.

Setting Up For User Mode

There are few things that user mode process required.

  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 binary from the GRUB module need to copied to the above allocated page frames for the program codes.
  3. Above mentioned page frames need to be mapped using page dictionary and page tables. For that we need at least two page tables since code and data must mapped in at 0x00000000 and increasing from there, and the stack should start from below the kernel, at 0xBFFFFFFB, growing towards lower addresses. (The U/S flag needs set to allow PL3 access.)

For our convenient development it is good to store this process information in a struct. you can allocate it dynamically using kernel’s malloc function.

Entering User Mode

We can change the current privilege level by using an interrupt. This diagram describes that, a event in a user program raise an interrupt and the privilege level change from 3 to 1.

Likewise the situation that above diagram shows, we have to use ‘interrupts’ to change the current privilege level(PL0) to lower privilege level(PL3) in our operating system. We use iret or lret instruction for that which means interrupt return or long return. For that, we set up the stack like the processor had raised an inter-privilege level interrupt.

That stack needs following things.

[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

This information will be read by the iret instruction and save them into corresponding registers. Before we execute this iret instruction we should map page directory to our user mode process. Even after this PDT switching happen, the kernel must be continuously execute. For that kernel need to be mapped in the same time.

For that you can create a separate PDT for the kernel, (which maps all data at 0xC0000000 and above) and combine it with the user PDT (which only maps below 0xC0000000) when performing the switch. As we have discuees in our paging lesson the physical address of the PDT has to be save in cr3 register when setting that register.

The eflags register contains a number of unique 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 before 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 register entry on the stack will enable interrupts during user mode.

Until we finish creating inter-privilege level interrupts completely let’s keep this interrupts disable for our convenience.

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 that we have created starts.

The segment selectors for the user code and user data segments should be in cs and ss on the stack, correspondingly. The RPL(Requested Privilege Level) is the lowest two bits of a segment selector, as you already know. The RPL of cs and ss should be 0x3 when using iret to enter PL3. An example can be found in the code below:

    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

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.

Compiling user programs written in C programs to flat binaries rather than ELF binaries is much easier. The produced code structure in C is more unexpected, and the entry point, main function, may not be at binary offset 0 in the binary. One common solution is to place a few assembly code lines at offset 0 that call 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 how to create linker script that places these instructions first in executable.

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

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

Followings flags should be used for linking:

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

Now congratulations! your operating system can switch between user mode and kernel mode! Hope you find this article valuable for you. Thank you so much for reading! Until we meet with another exciting article be motivated! keep Learning!!!

Written by,

R.A.W. Lalendra

Undergraduate in Bsc(hons) Software Engineering

University of Kelaniya Sri Lanka.

--

--

Waruni Lalendra
Waruni Lalendra

Written by Waruni Lalendra

Software Engineering undergraduate at University of Kelaniya Sri Lanka