Project 0

CMSC 412

Due: Monday, Sept. 10, 06:00pm

Introduction

In this course we will implement a succession of cumulative projects. In each project we will be adding some new functionality to GeekOS. GeekOS is a tiny operating system kernel for x86 PCs. Its main purpose is to serve as a simple but realistic example of an OS kernel running on real hardware. To run our OS, we need to use either a real x86 machine or an emulator (simpler); we will use QEMU.

The purpose of project0 is to familiarize you with the GeekOS development environment, including the QEMU x86 emulator and the debugger gdb. The project is to modify GeekOS to impose resource limits on GeekOS processes.

Background

Running GeekOS

To compile GeekOS, untar the project source, change to directory build, and execute make. To run the OS in the emulator execute make run.

Once you have started up GeekOS, you will be taken to the GeekOS shell prompt. That is, the first program that GeekOS runs is shell.exe, to which you can type commands to be run (just like the UNIX shell program). The GeekOS file system is set up so that a number of user programs are installed in the directory /c.  The source code for these programs is in the directory src/user in the distribution. Three such programs are b.exe, null.exe, and long.exe.  The first program simply prints all of its arguments. The second is an infinite loop. The third is a long, but not infinite loop. You can run the programs as you would expect. For example, you run b.exe by typing

     $ /c/b.exe 1 2 3 4
And it would print out the four arguments that you provided. The shell also takes a number of directives. For example, typing exit terminates the shell (and your interaction with the OS). Look at src/user/shell.c to understand the other features of the shell.

You can easily add your own user programs by adding a file to src/user. Any file you add there is automatically compiled into a user program (with the .c suffix replaced by .exe) and put onto the disk of the booted GeeksOS.

Debugging GeekOS

Start the emulator in debug mode with make dbgrun. Then in another terminal, run gdb with make dbg. The debugger will attach to the operating system. The symbols for user programs will not be loaded. In debug mode the system starts paused, so you can enter breakpoints before execution. To begin execution enter continue into the debugger.

Now we'll talk about how GeekOS works.

User processes vs. kernel processes

In writing an operating system, you want to distinguish between the activities that operating systems code are allowed to do and the activities that user programs are allowed to do. The goal is to protect the system from incorrect or malicious activity that a user program may attempt, for example

Preventing these sorts of mistakes or attacks is accomplished by limiting the memory and machine operations that can be accessed by user programs. The x86 processor provides facilities that the operating system can use to enforce such limits. Briefly, the processor can be in kernel mode or user mode (and two others that are not used in GeekOS). In the latter, an attempt by the processor to access unallowed memory or execute an unallowed instruction causes the processor to start executing from a prespecified point (in the OS). User code is executed only in user mode. Kernel code is executed only in kernel mode.

The state of processes

GeekOS runs multiple threads simultaneously by switching the CPU between them. For this, the OS has to store the state of every thread when the thread is not executing, so that the thread can be resumed from where it stopped when it is next run on the CPU. GeekOS has two kinds of threads (processes): kernel threads, that execute OS code and with the CPU in kernel mode, and user threads, that execute user code with the CPU in user mode. The state of a kernel thread is stored in a Kernel_Thread structure (in include/geekos/kthread.h)


struct Kernel_Thread {
    ulong_t esp;                         /* offset 0 */
    volatile ulong_t numTicks;           /* offset 4 */
    int priority;
    DEFINE_LINK(Thread_Queue, Kernel_Thread);
    void* stackPage;
    struct User_Context* userContext;    /* NULL if kernel thread */
    struct Kernel_Thread* owner;
    int refCount;
  ...
}
The Kernel_Thread structure is also used to store the state of a user thread, except that now its userContext field points to a User_Context structure. For a kernel thread, the userContext pointer is NULL. The User_Context is defined in include/geekos/user.h:


struct User_Context {
    /* We need one LDT entry each for user code and data segments. */
#define NUM_USER_LDT_ENTRIES 2

    /*
     * Each user context contains a local descriptor table with
     * just enough room for one code and one data segment
     * describing the process's memory.
     */
    struct Segment_Descriptor ldt[NUM_USER_LDT_ENTRIES];
    struct Segment_Descriptor* ldtDescriptor;

    /* The memory space used by the process. */
    char* memory;
    ulong_t size;

    /* Selector for the LDT's descriptor in the GDT */
    ushort_t ldtSelector;
   ...
};

Most of the User_Context structure deals with the memory layout for the user thread, and you don't need to understand that for now (not until project 2). You will have to modify User_Context to contain some bookkeeping information as part of this project.

User threads are created using the routine Start_User_Thread (in src/geekos/kthread.c). This takes as its first argument the User_Context of that thread. This context is created by the Sys_Spawn function (in src/geekos/syscall.c).

System Calls

The operating system kernel presents an interface to user processes for the services it can perform on their behalf. For example, a user process may want to print a message to the console, create another user process to execute a user program, terminate itself, etc. The service is implemented by an OS kernel function. However the user process does not call the kernel function directly. Instead it issues a system call, which makes the processor enter kernel mode and execute the kernel function, at the end of which the processor switches back to user mode and control returns to the user program.

In GeekOS, a user process issues a system call via a trap instruction (specifically, trap 0x90); the identity of the system call routine and/or its arguments are stored in the processor registers. This mechanism is typically wrapped in a standard C library routine, hence hidden from the typical C programmer.

When a user process issues a system call, the trap causes the routine Syscall_Handler (in geekos/trap.c) to be invoked. This routine examines the current value in the register eax and calls the appropriate kernel routine to handle the syscall request. The value in eax is called the syscall number. The routine that handles the syscall request is passed a copy of the caller's context state, so the general registers (ebx, ecx, edx) can be used to pass parameters from the user program to the handler routine and to return values to the user program. This context state is defined in the struct Interrupt_State (in geekos/int.h).

The routines that implement GeekOS system calls are in geekos/syscall.c. The Sys_Spawn code is implemented here, along with code for other system calls, like Sys_Exit for the system call used by a process to terminate itself. If you are curious how system calls are invoked from user processes, take a look at src/user/shell.c in your project distribution; you'll be modifying this program in your next project.

Creating User Processes - Sys_Spawn()

We now give more detail for a particular OS service. A user program calls the C library function Spawn_Program() (defined in src/libc/process.c). This function issues a trap with syscall number SYS_SPAWN (see DEF_SYSCALL in include/geekos/syscall.h). This trap causes the kernel function Sys_Spawn() (defined in geekos/syscall.c) to be executed. The function calls Spawn() (defined in the file src/geekos/user.c) that is used to launch user programs. FYI, this Spawn() function does the following (see the comments in user.c as well)

Sys_Spawn returns the process id (pid) of the new thread.

Project Requirements

The purpose of this project is to modify GeekOS to impose resource limits on user processes. in particular, on the number of system calls that a user thread can make. Specifically:

  1. Add a system call function Sys_Limit corresponding to system call number SYS_LIMIT.
  2. Add a wrapper C library function Limit(int resource, int limit), where resource identifies a resource and limit is a limit on the resource. The valid arguments are resource is 0 (indicating system calls) and limit is a non-negative integer.
  3. Add a user program syscall.c that takes two arguments, say L and P. The program should call Limit(0,L) and then issue P null system calls. (A null system call is one with syscall number SYS_NULL.)

Limiting Number of System Calls

In order to limit the number of system calls by a user thread, you need to modify the User_Context structure (defined in include/geekos/user.h) to include a counter for the number of system calls the user thread has performed. Then you should modify the kernel to update this counter each time it performs a system call on this thread's behalf. For the system call that exceeds the limit (i.e., the N+1st system call), Sys_Exit should be called instead to kill the user thread.

Creating a user program

Look at the code provided in src/user/* to see how to add a user program and how to use the system call interface provided.

Grading Criteria

Unpack and compile correctly and we can see that you've made a good effort at the project 20
Adding a system call to limit the number of system calls correctly 30
Limiting number of system calls correctly 20
Testing syscall.exe L P 30
   
Total 100