Build Your Own OS Part 09

Asitha Nuwan
4 min readSep 27, 2021

--

This is the ninth article of the “Build your own Operating System“ article series. In the previous articles of this article series, we talk about how to setting up a development environment, segmentation, input-output handling, virtual memory and paging, and how to implement them. In the sixth article, we covered up the road to user modes. I suggest you to, refer to previous articles, before reading this. It will help you to a better understanding of this article. Let’s go ahead..

1.Segments for User Mode

To enable user mode we need to add two more segments to the GDT. They are very similar to the kernel segments we added

The segment descriptors needed for user mode. The difference is the DPL, which now 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 paging.

2. Setting Up For User Mode

There are a few things 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.

3. Entering User Mode

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

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:

The values cs and ss on the stack should be the segment selectors for the user code and user data segments, respectively. The lowest two bits of a segment selector is the RPL -the Requested Privilege Level. When using iret to enter PL3, the RPL of cs and ss should be 0x3. The following code shows an example:

4.. Using C for User Mode Programs

When C is used as the programming language for user mode programs, it is important to think about the structure of the file that will be the result of the compilation.

The reason we can use ELF [18] as the file format for for the kernel executable is because GRUB knows how to parse and interpret the ELF file format. If we implemented an ELF parser, we could compile the user mode programs into ELF binaries as well. We leave this as an exercise for the reader.

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

Note: *(.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.

Thank you very much for reading!

Hope to see you form the next article.

--

--

No responses yet