Let’s Build an Own Operating System (PrimitiveOS)

Sachin Tharaka
6 min readAug 27, 2021


Part 6- Integrate user modes

Welcome Back!

This is my journey through making a new Operating System named PrimitiveOS.This is the sixth article of the article series and after reading this you can get a proper idea about integrating user modes in an OS.

Before entering this Please read previous parts if you haven’t already done so. In the last article, I had written about interrupts in an OS.

The Road to User Mode….

Now that the kernel boots, prints to screen, and reads from the keyboard — what do we do? Usually, a kernel is not supposed to do the application logic itself, but leave that for applications. The kernel creates the proper abstractions (for memory, files, devices) to make application development easier, performs tasks on behalf of applications (system calls), and schedules processes.

User Mode Vs Kernel Mode

The User mode is the normal mode where the process has limited access. While the Kernel mode is the privileged mode where the process has unrestricted access to system resources like hardware, memory, etc. A process can access I/O Hardware registers to program it, can execute OS kernel code, and access kernel data in Kernel mode. Anything related to Process management, IO hardware management, and Memory management requires processes to execute in Kernel mode.

This is important to know that a process in Kernel mode gets the power to access any device and memory, and same time any crash in kernel mode brings down the whole system. But any crash in user mode brings down the faulty process only.

The kernel provides System Call Interface (SCI), which are the entry points for the kernel. System Calls are the only way through which a process can go into kernel mode from user mode. The below diagram explains user mode to kernel mode transition in detail.

User mode to Kernel Mode switching

To go into Kernel mode, an application process.

  • Calls the Glibc library function.
  • Glibc library knows the proper way of calling System Call for different architectures. It set up passing arguments as per architecture’s Application Binary Interface (ABI) to prepare for System Call entry.
  • Now Glibc calls SWI instruction (Software Interrupt instruction for ARM), which puts the processor into Supervisor mode by updating Mode bits of CPSR register and jumps to vector address 0x08.
  • Till now process execution was in User mode. After SWI instruction execution, the process is allowed to execute kernel code. Memory Management Unit (MMU) will now allow kernel Virtual memory access and execution, for this process.
  • From Vector address 0x08, process execution loads and jumps to SW Interrupt handler routine, which is vector_swi() for ARM.
  • In vector_swi(), System Call Number (SCNO) is extracted from SWI instruction and execution jumps to system call function using SCNO as the index in system call table sys_call_table.
  • After System Call execution, in return path, user space registers are restored before starting execution in User Mode.

To support kernel mode and user mode, the processor must have hardware support for different privilege modes.

For example, ARM processor supports seven different modes.

User mode, in contrast with kernel mode, is the environment in which the user’s programs execute. This environment is less privileged than the kernel and will prevent (badly written) user programs from messing with other programs or the kernel. Badly written kernels are free to mess up what they want.

There’s quite a way to go until the OS created in this book can execute programs in user mode, but this chapter will show how to easily execute a small program in kernel mode.

Loading an External Program

Where do we get the external program from? Somehow we need to load the code we want to execute into memory. More feature-complete operating systems usually have drivers and file systems that enable them to load the software from a CD-ROM drive, a hard disk, or other persistent media.

Instead of creating all these drivers and file systems, we will use a feature in GRUB called modules to load the program.

Getting into hands-on

Step 1

GRUB Modules

GRUB can load arbitrary files into memory from the ISO image, and these files are usually referred to as modules. To make GRUB load a module, edit the file iso/boot/grub/menu.lst and add the following line at the end of the file:

module /modules/program

Now create the folder as the path iso/modules and in the modules folder, we can add these two files as follows.

Step 2

The code that calls kmain must be updated to pass information to kmain about where it can find the modules. We also want to tell GRUB that it should align all the modules on page boundaries when loading them

To instruct GRUB how to load our modules, the “multiboot header” — the first bytes of the kernel — must be updated as follows:

loader.s file

GRUB will also store a pointer to a struct in the register ebx that, among other things, describes at which addresses the modules are loaded. Therefore, you probably want to push ebx on the stack before calling kmain to make it an argument for kmain.

Step 3

Executing a Program

A program written at this stage can only perform a few actions. Therefore, a very short program that writes a value to a register suffices as a test program. Halting Bochs after a while and then check that register contains the correct number by looking in the Bochs log will verify that the program has run. This is an example of such a short program:

Step 4


Since our kernel cannot parse advanced executable formats we need to compile the code into a flat binary. NASM can do this with the flag -f:

nasm -f bin program.s -o program

This is all we need. You must now move the file program to the folder iso/modules.

Step 5

Finding the Program in Memory

Before jumping to the program we must find where it resides in memory. Assuming that the contents of ebx is passed as an argument to kmain, we can do this entirely from C.

The pointer in ebx points to a multiboot structure multiboot.h which describes the structure.

multiboot.h file

The pointer passed to kmain in the ebx register can be cast to a multiboot_info_t pointer. The address of the first module is in the field mods_addr. The following code shows an example:

Step 6

now the main.c file should be updated

However, before just blindly following the pointer, you should check that the module got loaded correctly by GRUB. This can be done by checking the flags field of the multiboot_info_t structure. You should also check the field mods_count to make sure it is exactly 1. For more details about the multiboot structure, see the multiboot documentation [19].

Jumping to the Code

The only thing left to do is to jump to the code loaded by GRUB. Since it is easier to parse the multiboot structure in C than assembly code, calling the code from C is more convenient (it can of course be done with jmp or call in assembly code as well). The C code could look like this:

typedef void (*call_module_t)(void);
/* ... */
call_module_t start_program = (call_module_t) address_of_module;
/* we'll never get here, unless the module code returns */

If we start the kernel, wait until it has run and entered the infinite loop in the program, and then halt Bochs, we should see 0xDEADBEEF in the register eax via the Bochs log. We have successfully started a program in our OS!

Checking the progress

Give make run command in terminal.

After the boch is loaded you can continue the program in terminal

You can see the output in com1.out file which is the string, you are given in the kmain file.

If you have the expected output, Congratulations. You have done this part successfully.

The Beginning of User Mode

The program we’ve written now runs at the same privilege level as the kernel — we’ve just entered it in a somewhat peculiar way.

Thank you for reading. We will meet soon.#staysafe #stayconnected

Reference: https://littleosbook.github.io/ https://intermezzos.github.io/

For any issue with making files, you can follow my repository(Branch integrate user modes)




Sachin Tharaka

Software Engineering, University of Kelaniya, Sri Lanka