CMSC 412 Programming Assignment #1

Due February 26, 2002 (10:00 AM)

Introduction

In this project you will extend the GeeksOS operating system to include user mode processes and system calls. Chapter 6 of the IA-32 System Programmers manual will likely be helpful in explaining how to manage the processor.

 

When GeekOS boots up, it should start the user mode process in the file C:\a.exe.  This process is the ancestor of all other user mode processes in the operating system.

Adding User Mode

You will need to create user mode processes.  There are two steps to creating a user mode process.  First, you must load a program for the process to run.  Second you should create a thread and setup the memory manager to protect the processes from other processes.

 

To load a program, you will need to write the function loadElfProgram(char *programName). Programs are stored on disk and to read them you will need to call the appropriate file system routines.  However, since the “real” file system for the project is not yet written (you will write it later in the semester), we have provided you a temporary file system called pfat (pseudo FAT file system).  From within your kernel, you will be able to read files by calling the routine int ReadFileAndAllocate(char *fileName, char **buffer) which is defined in pfat.c. This routine will allocate memory, read the file named filename into that memory, and set buffer to point to the beginning of the buffer holding the file. You will also need to initialize the IDE subsystem of the operating system by calling the routine Init_IDE from the main thread of your OS kernel.

 

When a program is stored in a file, it contains several parts that describe the program itself, how much data it will have and where the first instruction is located.  There are several standards that describe how a program is stored on disk.  The one we will use in this class is a file format called ELF.  The figures below show the layout of an ELF binary. It consists of an elf header, followed by a program header, followed by the program instructions and then the initialized program data. Your loadElfProgram routine will read a file into memory and then setup the correct information a  struct User_Program that describes the program. The ELF header and program header have structures defined in elf.h that you can use.

 

Elf Header

Program Header (Text segment)

Program Header (Data segment)

….

Program Instructions

Program Initialized Data

 

Format of Elf Binary

 

Field

Description

Ident

String to identify what’s in the file

Phoff

Offset of first program header

Phnum

Number of program headers

Entry

Address of first instruction in program

 

Useful Elf Header Fields

 

Field

Description

Offset

Offset of region within file

Vaddr

Address for start of region

MemSize

Length of region

 

Useful Fields in Program Header

 

The other step in creating a user program is to compile it and store it onto the file system that the kernel can read.  Since your operating system is not yet ready to run the compiler, you will use a “cross compiler” to create the files and store them into the file system before booting the operating system.  The directory UserProgs contains a couple of test programs, and a makefile that will load them into the file system (really the file called hd.img).

 

To create a user mode you will write the function Create_User_Process (struct * User_Program). It will create a new user mode process, setup the stack as well as the text and data segments for the process. 

 

To create a user mode process, you will also need to set up various types of data structures that describe the memory and privilege level associated with your new process. The steps required are:

 

1)      Create an LDT descriptor by calling the routine Allocate_Segment_Descriptor()

2)      Create an LDT selector by calling the routine Selector()

3)      Create a text segment descriptor by calling Init_Code_Segment_Descriptor

4)      Create a data segment descriptor by calling Init_Data_Segment_Descriptor

5)      Create an data segment selector by calling the routine Selector()

6)      Create an text segment selector by calling the routine Selector()

 

 

You will also need to call Create_Thread to create a thread to be associated with the user process. 

 

Finally, you will need to setup 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 (kthead.c) can be used to push individual values and the routine Push_General_Registers (kthread.c) will push all of the general-purpose registers at once. The layout of the stack should be:

 

Stack Data Selector (data Slector)

Stack Pointer (end of data memory)

Eflags

Text Selector (text Selector)

Program Counter (entry Addr)

Error Code (0)

Interrupt Number (0)

General Purpose Registers

Ds (data Selector)

Es (data Selector)

Fs (data Selector)

Ga (data Selector)

 

 

Adding System Calls

 

Your kernel should support the following system calls:

 

Name

Call #

Kernel Function

User Space Function

SYS_NULL

0

Sys_Null

int Null( void );

SYS_EXIT

1

Sys_Exit

void Exit( void );

SYS_PRINTSTRING

2

Sys_Print_String

int Print_String( const char* message);

SYS_GETKEY

3

Sys_Get_Key

Keycode Get_Key( void )

SYS_SPAWN

4

Sys_Spawn_Program

int Spawn_Program(char* program );

SYS_WAIT

5

Sys_Wait

int Wait( unsigned int pid );

 

As discussed in class, the way a user program makes a system call is to trap into the operating system.  To make this process a bit easer for user programs, you will write a library (called libuser.c) that will implement that user space functions listed in the table above.  Each function implementation will make an appropriate trap into the operating system.  Inside the operating system kernel, you will extend the function Syscall_Handler to call the various Sys_XXX kernel functions listed in the table depending on the system call number.  The signature (parameters and return values) of the Sys_XXX calls should be the same as their corresponding user space function.

 

The Sys_Exit call should terminate the user mode process that calls it.  The Sys_Print_String call should print the passed string to the screen.  The Sys_Get_Key call should block the process until a key has been pressed, and then return the key that was pressed.  If two processes call this system call at once, only one should get the character (which ones doesn’t matter). The Sys_Spawn_Program call creates a new user process running the program that is passed as its first paramter. The return value from this call should the process id of the new processes created, or –1 if it was not possible to create the process. The Sys_Wait system call should block the calling process until the process id that is passed as the first parameter has exited.

 

To pass data from user mode to kernel mode you will need to copy memory from user space to kernel space as part of your system call handler.  Likewise if you wish to transfer data from the kernel back to user mode you will need to copy the memory.

 

Requirements

Submit the program using the submit command.  You should submit a tar file containing all of the code (even the code we provided and you did not modify) for your kernel.  Do not include the binaries, object code, or the bochs disk images in your tar file (i.e. do a gmake clean before submitting the files).

 

News Files

Ide.{c,h}             IDE Device Driver

Pfat.c               pseudo FAT filesystem code

Elf.h                 interface to read a program from a file

User.h             start of a user program context

UserProgs            a directory of user programs