Develop Your Own x86 Operating System(OS) #10
If you can remember we start walking in the road to user mode from the #7 article onwards. Their we advanced our OS so that it can execute a program at the same privilege level as the kernel. Now, we are near the destination of our road, User mode.
If you haven’t read the previous article of the article series Develop Your Own x86 Operating System(OS), it’ high time to read it. You can read it from here.
In the above mentioned article where we starts walking towards user mode, I have explained what is user mode. So, without any further explanations let’s jump on to the implementation of user mode.
Segments for User Mode
In the article on the segmentation (#5) we set up the GDT and added kernel segments. In a similar way, in order to enable user mode we need to add two more segments to the GDT. They are:
The difference we can bring with these segments is the Descriptor privilege level (DPL), which allows code to execute in privilege level 3 (PL 3). The segments can still be used to address the entire address space, but just using these segments for user mode code will not protect the kernel. Therefore, For that purpose we need paging.
Setting Up For User Mode
We can list 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.
- 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 while 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 is easy to store this information in a struct representing a process and also this process struct can be dynamically allocated with the kernel’s malloc function.
Entering User Mode
The only way to execute code with a lower privilege level than the current privilege level (CPL) is to implement an iret (interrupt return) or lret (long return) instruction.
To enter the user mode we should 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
The instruction iret will then 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.
In order to continue executing kernel code after we’ve switched PDT, the kernel needs to be mapped in. By maintaining 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 we can accomplish this. Note that the physical address of the PDT should be used when setting the cr3 register.
The register eflags contains a set of different flags and for us the most important one is the interrupt enable (IF) flag. 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 is somewhat difficult to get inter-privilege level interrupts to work properly but we will implement this in an upcoming article.
The value eip on the stack should point to the entry point for the user code - 0x00000000 and the value esp on the stack should be where the stack starts - 0xBFFFFFFB (0x00000000-4) in our implementation.
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 (Requested Privilege Level). When using iret to enter PL3, the RPL of cs and ss should be 0x3. The following code shows 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 register ds, and the other data segment registers, should be set to the same segment selector as ss. They can be set with the mov assembly code instruction which is the ordinary way we have used.
We have now set all the features needed to execute iret. If everything of the above process has been set up right, we should now have a kernel that can enter user mode.
Using C for User Mode Programs
If we are going to use C as the programming language for user mode programs, it is a must to think about the structure of the file that will be the result of the compilation.
The reason we can use ELF 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. This may not be easy as it seems.
One action we can take 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 way to circumvent this is to add a few assembly code lines placed at offset 0 which calls main:
; push argv
; push argc
; main has returned, eax is return value
jmp $ ; loop forever
The following code show an example of a linker script that places these instructions first in executable (start.s gets compiled to start.o):
With this script we can write programs in C or assembler, 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.
Hope you have gain some knowledge on user mode. Let’s meet with the next article of Develop Your Own x86 Operating System(OS) series. Thank you so much for reading!!!!!!!!!!