How To Build An Operating System: User Mode - Part 09
Hello everyone!. This is the 9th article of this building an own operating system article series. Please read the first seven articles in this article series if you didn’t read those. It will help you to understand this article more. Let's jump into today's chapter.
In this article, we are going to discuss user mode. We are only a few steps away from our destination but these steps are hard and tricky. So I hope you will follow these steps very carefully with me. We talked about user-mode briefly in previous articles. You can refer to that article here. Here we will discuss that more deeply.
To enable user mode, we should add two more segments to the GDT and they are very similar to the kernel segments we added in previous articles.
The segment descriptors needed for user mode as following:
Index Offset Name Address range type DPL
3 0x18user code segment 0x00000000 - 0xFFFFFFFFRX PL3
4 0x20user data segment 0x00000000 - 0xFFFFFFFFRW PL3
DPL now allows code to execute in PL3. We need paging to protect the kernel because just using these segments won’t protect it.
Setting Up For User Mode
- Page frames for code, data, and stack. It suffices to allocate a one-page frame for the stack and enough page frames to fit the program’s code. Get basic implementation work first.
- The binary from the GRUB module should be copied to the page frames used for the program's code.
- Above mentioned page frames need to be mapped using a page dictionary and page tables. For that, we need at least two-page tables since code and data must be mapped in at
0x00000000
and increasing from there, and the stack should start from below the kernel, at0xBFFFFFFB
, growing towards lower addresses. The U/S flag needs set to allow PL3 access.
Entering User Mode
The only way to execute code with a lower privilege level than the current privilege level is executing an iret
or lret
instruction — interrupt return or long return.
After setting up the stack as if the processor had raised an inter-privilege level interrupt to enter user mode the stack should look like this:
[esp + 16] ss ; the stack segment selector for user mode
[esp + 12] esp ; the user mode stack pointer
[esp + 8] eflags ; the control flags want to use in user mode
[esp + 4] cs ; the code segment selector
[esp + 0] eip ; instruction pointer of user mode to execute
The instruction iret
will read these values from the stack. And then it will fill in the corresponding registers. We need to change the page directory we set up for the user-mode process before we execute iret
. You need to continue executing kernel code after switched PDT. And the kernel needs to be mapped in. To accomplish this we need to have a separate PDT for the kernel, which maps all data at 0xC0000000
and above and merges it with the user PDT which only maps below 0xC0000000
when performing the switch. The physical address of the PDT has to be used when setting the register cr3
.
The eflags register contains a set of different flags. The interrupt enables (IF) flag is the most important for this. 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. 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 the user-mode. We should have interrupts disabled for now. It requires some work to get inter-privilege level interrupts to work correctly. The value eip
on the stack should be pointing to the entry point for the user code - 0x00000000
. The value esp
on the stack should be where the stack starts - 0xBFFFFFFB
(0xC0000000 - 4
).
Respectively the values cs
and ss
on the stack should be the segment selectors for the user code and user data segments. The lowest two bits of a segment selector is the Requested Privilege Level. The RPL of cs
and ss
should be 0x3
when using iret
to enter PL3.
Here is an example:
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 other data segment registers, and the register ds
should be set to the same segment selector as ss
. With the mov
assembly code instruction, they can be set the ordinary way. Now we can execute iret
. Now the kernel should be able to enter user mode.
Using C for User Mode Programs
We write user-mode programs in C and compile them to flat binaries. In this way, the generated code is more unpredictable. And the entry point, main
, may not be at offset 0 in the binary. Adding few assembly code lines placed at offset 0 which calls main
will solve this as below.
extern main section .text
; push argv
; push argc
call main
; main has returned, eax is return value
jmp $ ; loop forever
save this code in a file called start.s
.Here is an example for 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*)
}
}
Now we can write programs in C or assembler or any other language that compiles to object files linkable with ld
. As well as it is easy to load and map for the kernel.
We want the following GCC flags when we compiling user programs:
-m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector - nostartfiles-nodefaultlibs
The 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
.
This is all for this article. Hope you get the idea and see you with the next article and there, we will be discussing File Systems as the next part.
Thanks for reading!.
References,