Table of Contents
In this project you will extend the GeekOS operating system to include user mode processes and system calls.
This project will make extensive use of systems programming features of the x86 (a.k.a. IA32) CPU architecture. It will also require you to understand the ELF executable format.
The Intel IA32 Architecture Manuals are the definitive reference for programming x86 CPUs. You can find them at the Intel website; the order numbers for the set are 253665 (Volume 1, Basic Architecture), 253666 and 253667 (Volumes 2A and 2B, Instruction Set Reference), and 253668 (Volume 3, System Programming Guide). For this project, you will need to understand segments and local descriptor tables. These are described in Volume 3. You will want to read Volume 3, Chapter 3 carefully to understand how segments work.
You might need to revise some information in ELF Executable Format documentation, since you'll reuse ELF parsing from Chapter 6, Project 1: Loading Executable Files.
This project will require you to make changes to several files. In general, look for the calls to the TODO() macro. These are places where you will need to add code, and they will generally contain a comment giving you some hints on how to proceed.
In src/geekos/user.c, you will implement the functions Spawn(), which starts a new user process, and Switch_To_User_Context(), which is called by the scheduler before executing a thread in order to switch user address spaces if required.
In src/geekos/elf.c, you will implement the Parse_ELF_Executable() function. This involves reading the program headers of an ELF executable to find the file offset, length, and user address for the executable's text and data segments. Based on this information, you should fill in the Exe_Format data structure passed as a parameter. This data structure will be used by the Spawn() function to determine how to load the executable.
In src/geekos/userseg.c, you will implement functions which provide support for the high level operations in src/geekos/user.c. Destroy_User_Context() frees the memory resources used by a user process. Load_User_Program() builds the User_Context structure for a new process by loading parts of the executable program into memory. The Copy_From_User() and Copy_To_User() functions copy data between the user address space and the kernel address space. The Switch_To_Address_Space() function activates a user address space by loading the processor's LDT register with the LDT of a process.
In src/geekos/kthread.c, you will implement functions which take a completed User_Context data structure and create a thread which is ready to execute in user mode. The Setup_User_Thread() function sets up the initial kernel stack for the process, which specifies the initial contents of the processor registers when the process enters user mode for the first time. Start_User_Thread() is a higher level operation which takes a User_Context object and uses it to start the new process.
To implement memory protection for user processes, this project uses segmentation. With segmentation, each process resides in a single physically contiguous chunk of memory. You can allocate this chunk of memory using the Malloc() function.
In order to provide a private address space, you will need to create segments for code and data of the process. Both segments will refer to the single memory chunk you have allocated for the process. The segments will reside in the local descriptor table (LDT) that you will create for the process. In the LDT, you will define segment descriptors that define the process code and data segments. Both of these should be created at the user privilege level: this will have the effect of that the process will only be able to access the memory in the segments you define in the LDT.
Here is a general list of steps you will need to follow in order to set up the segments and LDT for a process:
Create an LDT descriptor by calling the routine Allocate_Segment_Descriptor()
Create an LDT selector by calling the routine Selector()
Create a text segment descriptor by calling Init_Code_Segment_Descriptor()
Create a data segment descriptor by calling Init_Data_Segment_Descriptor()
Create an data segment selector by calling the routine Selector()
Create an text segment selector by calling the routine Selector()
GeekOS, like other operating systems, uses files to store executable programs. These files are in ELF format. We have provided you with a simple read-only filesystem for this project, which contains several programs that you can use to test user mode. To simplify the process of loading an executable, you will simply load the executable into a single buffer in the kernel. To do this, you can use the function Read_Fully(), whose prototype is in <geekos/vfs.h>. Here is an example of how you would read the executable /c/shell.exe into a kernel buffer:
int rc;
void *buf;
ulong_t fileLen;
rc = Read_Fully("/c/shell.exe", &buf, &fileLen);
if (rc == 0) {
/*
* Read the file successfully!
* buf points to a Malloc'ed buffer containing the file data,
* and fileLen contains the length of the file.
*/
...
}
Loading ELF executables is fairly straightforward. You will need to locate the ELF program headers. These headers will describe the executable's text and data segments. As you parse the ELF executable, you will fill in the fields of an Exe_Format data structure, which is a high level representation of how to load data from the executable file into memory.
Besides loading the executable's text and data segments into memory, you will also need to create two other data structures in the process's memory space.
You will need to create a stack for the new process. The stack will reside at the top of the user address space. You can use the DEFAULT_USER_STACK_SIZE macro in src/geekos/userseg.c as the stack size.
You will also need to create an argument block data structure. This data structure creates the argc and argv arguments that are passed to the main() function of the user process. Two functions are defined to help you build the argument block (prototypes in <geekos/argblock.h>). Get_Argument_Block_Size() takes the command string passed to the Spawn() function and determines how many command line arguments there will be, and how many bytes are required to store the argument block. The Format_Argument_Block() function takes the number of arguments and argument block size from Get_Argument_Block_Size() and builds the argument block data structure in the memory location you have allocated for it.
Note that you will need to take the size of the stack and argument block into account when you decide how much memory to allocate for the process.
As described in Section 3, “Project Synopsis”, you will need to create a thread for the new process. You will need to set up the stack of your new thread to look like it has just been interrupted. This will require pushing the appropriate values onto the stack to match what the hardware would push for an interrupt. The routine Push() (src/geekos/kthread.c) can be used to push individual values onto the stack. The required values should be pushed onto the stack as follows:
Stack Data selector (data selector)
Stack Pointer (end of data memory)
Eflags (IF bit should be set so interrupts are enabled)
Text selector (text selector)
Program Counter (code entry point address)
Error Code (0)
Interrupt Number (0)
General Purpose Registers (esi should contain address of argument block)
DS register (data selector)
ES register (data selector)
FS register (data selector)
GS register (data selector)
System calls are software interrupts made by processes in order to request services from the kernel. In GeekOS, the kernel handlers for system calls are implemented in the file src/geekos/syscall.c. Each system call you must implement is described in this file.
Some of the system calls require you to copy data between the kernel address space and the current process's user address space. This task is carried out by the Copy_From_User() and Copy_To_User() functions whose prototypes are in the header file include/geekos/user.h, and whose implementations are in the file src/geekos/userseg.c. You will need to implement these functions. With segmentation, this is a fairly easy task; once you have verified that the user buffer is valid (i.e., it lies entirely within memory belonging to the process), a call to memcpy() can be used to do the transfer.
When GeekOS boots up, it should start the user mode process in the file /c/shell.exe. This process is the ancestor of all other user mode processes in the operating system. You will need to add the code to start this process and wait for it to exit. See the function Spawn_Init_Process() in src/geekos/main.c.
We have provided several user mode programs with which you can test your project. shell.exe is a simple command interpreter program. It is available on a PFAT filesystem from within GeekOS at the path /c/shell.exe. It should be the first user program loaded by your kernel; for this reason, it is referred to as the init process. Once you have successfully implemented Spawn() and its support routines, and implemented all of the required system call handlers, GeekOS should boot into a familiar command prompt.
Once the init process loads, you can use it to test some other small user mode programs. /c/b.exe prints a message, echoes its command line arguments, and exits. /c/c.exe executes an illegal system call. It will be killed by the kernel.