CS170 KOS

KOS -- An operating system for a simulated MIPS machine

The rest of the labwork in the class is going to focus on writing KOS -- an operating system for a simulated MIPS machine. Obviously we don't have the machinery to let you write a real OS for a real machine, so we're doing the next best thing: we will be writing the operating system for a simulated machine.

One of the problems with this project is that students often get confused concerning which parts of the code are the simulated machine, and which parts are the OS. Students will try to either change or circumnavigate the simulator to fix problems instead of accepting the simulator as a given and working around it in the OS. We will try to keep this from being a problem. To do so, you should try to adopt the following model. The simulator calls the code you write in exactly three situations:

Each time your code can see the user program's register set and main memory as well as any data structures you have defined in your code. So your job is to

And that's it. Nothing more. We'll talk about the best way to organize your code for these three cases as we go along but you should try to keep in mind what the simulator is doing and what the OS (you) is (are) doing.

Basically, things will work as follows. There are two object files for the simulator:

There is also a header file /cs/faculty/rich/public_html/class/cs170/labs/kos_start/start/simulator.h which contains constants and simulator procedure definitions that you will need in your KOS code. The simulator represents the hardware your operating systems is controlling.

The operating system initializes through a C-language subroutine that you write called KOS(). This state of affairs is not so different from an actual operating system. If you were implementing KOS for an actual machine, you'd need to define an initial entry point for the hardware to "jump through" at start-up. In fact, the "boot up" process is when the hardware loads and executes a pre-defined routine that it finds in a place the hardware specifies (e.g. the boot record).

You will link your code to the two object files, and an executable file will be created. When you run the executable, it starts the simulator. Upon instantiation, the simulator calls KOS() and your operating system gains control. From then on, the interaction between the operating system and the simulator is achieved through well defined communication points. The simulator program will call you when the program requires service (which it tells you through an exception) or when a device requires service (through an interrupt). You, however, do not return once called. Instead, you have two ways to return to the simulator after you are finished doing that thing you do. You can call run_user_code() if you want to run (or go back to running) a user program or noop() if you want to "idle" the machine. All exceptions call exceptionHandler() passing in an exception type as an argument, and interruptHandler()) which gets an interrupt type as an argument.

Basically, a computer is a collection of resources (memory, CPU, screen, keyboard, etc) that interact in a specific way. It is the job of the operating system to manage these resources in such a way that enables the user make use of the resources with paradigms that are familiar to him/her (like writing programs and executing them). Typically, each resource exports a low-level hardware interface that consists of control registers, data transfer locations, and a set of interrupt response codes. The resources that the simulator provides are as follows:

For the first few labs, you will load user programs into memory using the subroutine load_user_program(char *filename). Note that this does not execute the program -- it simply loads it into memory.

The Console

In all computers, the screen and keyboard are separate devices that interact with the CPU through interrupts. In KOS, there is a single simulated console device that performs these functions through one API. The simulator "hooks" this device up to your standard in and standard out so that you can type things into the console of your simulated MIPS computer, and see the results. It turns out that gdb also works on the simulator since standard in and standard out are the I/O ports. I found this state of affairs to be extremely handy for debugging.

KOS communicates with this console through two procedures and two interrupts.

To try to make for a realistic interface, the console is able to read/write a character around every 100 user operations.

This simulated activity is pretty close to the way things actually work (or used to work) on real machines. consle_write() simulates an interface to the "screen" which can only accept so many characters per second. In a real machine, it would probably let you pass a buffer and a length, but the principle is the same -- you write and then wait for a synchronous interrupt saying that the device is ready to write again. The same is true for console_read() except the interrupt is unsolicited meaning that it does not come in response to something your OS has done. Instead, when the interrupt hits, your OS should know that a character is ready to be read from the keyboard device and should call console_read().

  • There is no timer interrupt in the simulator for the first lab.

    User Programs for KOS

    Okay -- this is the weird part. Put on your weird glasses and listen closely. The simulator simulates an old machine that was made by the Digital Equipment Corporation is the late 80's and early 90's. It actually simulates the hardware so well that it can "run" programs compiled for the actual machine. In fact, the rumor is that the developers had the real machine and made sure that any program that ran on it successfully would also run on the simulator.

    The problem is that to run a program on your simulator, it must be a program that would run on this old piece of hardware. Programs compiled for the Intel boxes on campus, however, will not work since they are built to run on Intel running Linux and not MIPS running Ultrix. If we had a working version of the MIPS/Ultrix box, we'd give you all logins and you could compile your own test codes there, copy them to your Intel box, and load them into your simulator to test them.

    We solve this problem using a cross-compiler. The good people at Gnu Software have made a version of gcc that will build programs on our Intel system that would run on the MIPS/Ultrix box if we had one.

    User programs for KOS must be compiled using the cross-compiler. If you use just the regular gcc you will get a version that will run on your system but not the simulated MIPS machine.

    You may use the application programs that your TAs have thoughtfully provided you with test code, found in /cs/faculty/rich/cs170/test_execs/, which are compiled into the executable files in the same directopry. There is also a Makefile to show you how this is done.

    Note that the protection of these executables is not r-x. That is because these are not executable on our machines. They are only executable in the context of KOS -- in other words, you can only execute them by loading them into KOS.


    Loading User Programs in KOS

    The basic steps for loading a user program in KOS is:

    When you start initializing the stack, you need to follow a few rules. The stack will grow toward lower memory addresses. Thus, when you want to initialize the stack with certain values (like argv and envp), you should put those values in memory locations at the top end of the user's memory, and then set StackReg to be just below these memory locations. As a convention, the user program will look for argc in main_memory[StackReg+12], argv in main_memory[StackReg+16], and envp in main_memory[StackReg+20]. It will not make use of any memory values greater than main_memory[StackReg] for allocating call frames on the stack.


    Alignment

    As with most modern computers, the MIPS simulator assumes that all 4-byte quantities like integers and pointers are aligned. This will be an issue when you are setting up argc and argv. There is a quirk of the simulator that causes bizarre behavior if you use the top 8 bytes of memory. Thus, do not use them. When you start stuffing stuff into the stack, do not stuff anything into these eight bytes.

    The Assignment

    Look at start/kos.c. This is a rudimentary KOS implementation that loads the file a.out into user memory and executes it. The a.out program must be very restricted -- the only external procedure that it may call is _exit(). It does not have to call _exit() however, because _exit() is called automatically whenever main() returns.

    Copy /cs/faculty/rich/public_html/class/cs170/labs/kos_start/start/* into your own area, and compile it so that you can run it. Test it on some a.out files that you copy from the /cs/faculty/rich/cs170/test_execs/ directory (try halt and cpu -- not all of the tests will work because they make system calls you haven't implemented yet).

    Now, you are to make the following changes to kos.c:

    1. Implement restricted versions of the system calls read() and write(). The syntax of read() and write() are exactly like Linux's read() and write(). Read their man pages if you are not familiar with read() and write() already. Now, implement read() so that it reads from the console whenever the first argument is zero (i.e. standard in), and implement write() so that it writes to the console whenever the first argument is one or two (standard out or standard error). Any other first argument should return with an error.

      The way that the system call driver is written in the simulator is that if you return a negative value to a system call, it will return -1 to the user program, and set the errno value to be the returned value times -1. This is how you can return different errors from system calls. See an old errno.h from Sun OS for an example set of errnos. (As of this writing, Fedora Core's Linux keeps its list of errnos in /usr/include/asm-generic/errno-base.h and /usr/include/asm-generic/errno.h.)

      Remember that if the user specifies an address of x, that is not address x in KOS. It is the user's address x.

      An important fact of OS design is that users can do incorrect things, but the operating system can't. Thus, KOS should be ready for any arguments to read() and write(), and if the arguments are incorrect, it must deal with that gracefully (i.e. return with an error) and not abort or dump core or leave KOS in such a state that future system calls will break.

    2. Change KOS further so that it will call a given file with a given set of arguments. These can be compiled into the program, but in an easy-to-change place (i.e. as global variables in kos.c). Now, not only should KOS load the file and execute it, but it should set up memory so that the program sees the arguments as argc and argv.

    In short, what you hand in should allow cross-compiled codes, like cat, cat1, cat80, hw, argtest, and cpu in /cs/faculty/rich/cs170/test_execs/ to read and write standard in, standard out and standard error and that work with the console. Your code should also allow the program a.out to take arguments (although they can be hard coded for now in a global variable in KOS).


    Structuring your code

    A step-by-step description of how you file cook_book (due to Jim Plank). It is not required that you do things this way, but it is in your best interest to follow this set of guidelines -- it shows the design and development philosophy used by an expert programmer, it it will relieve you of a lot of design stress, and will position you well for the future labs.

    Also, you might take advantage of Heloise's Helpful Hints for Lab #2 as she always seems to have advice to lend.


    Appendix -- Various Things


    Simulator Subtleties

    The simulator is built so that when you do something incorrect it will quit. However, some of the error messages will not be very helpful. For example, you are not allowed to return from an interrupt or exception and let the simulator pick up where it left off. An error message will be printed if you try to do so. Instead of returning, you must call run_user_code() or noop(). The former will cause the machine to switch to user mode, load the registers, and begin executing code. The noop() function will put the machine into idle mode and wait for an interrupt to occur. Another example of something else you are not allowed to do is write a character to the console while it is already trying to write one out (that is before the interrupt occurs). For example the following code will not work:

            console_write('H');
    console_write('i');

    It will stop the program with the following message:

            Assertion failed: line 107, file "machine/console.c"
    Abort

    Hopefully the file name will clue you in on where you made an error.


    The end of the user's address space

    Do not use the last 8 bytes of the user's address space. Start the stack at least 8 bytes from the limit. I don't know why -- just do it.

    Debug Flags

    There are various flags that you can use when invoking kos. You can list them out by executing kos help. The only one of these that might be of any help is the -d e option. This causes a debug message to be printed out when an exception or interrupt occurs.


    Writing Your Own Programs for KOS

    You may wish to write and compile your own programs to test your KOS. These will need to be compiled using the cross-compiler. To go about this you can follow the format presented in

    /cs/faculty/rich/cs170/test_execs/Makefile

    You'll only be able to do simple programs at this point.

    Consider good_test.c:

    main()
    {
      int i;
      char buf[30];
    
      for (i=0;i<5;i++){
        sprintf(buf,"#%d: cs170 is a great class!!\n",i);
        write(1,buf,strlen(buf));
      }
      
    }
    
    

    This is a rather trivial program that writes the sentence to the screen along with the value of i. I compiled it using

    /cs/faculty/rich/public_html/class/cs170/labs/kos_start/Makefile.xcomp

    into good_test. The compilation procedure is quite a bit more complicated than a simple gcc call:

    make -f Makefile.xcomp
    /cs/faculty/rich/cs170/xcomp/bin/decstation-ultrix-gcc -c -I/usr/include
    -I/cs/faculty/rich/cs170/xcomp/include -G0  evil_test.c
    /cs/faculty/rich/cs170/xcomp/bin/decstation-ultrix-ld -G0 -T
    /cs/faculty/rich/cs170/xcomp/lib/noff.ld -N -L/cs/faculty/rich/cs170/xcomp/lib
    -o evil_test.coff /cs/faculty/rich/cs170/xcomp/lib/crt0.o
    /cs/faculty/rich/cs170/xcomp/lib/assist.o evil_test.o
    /cs/faculty/rich/cs170/xcomp/lib/libc.a
    /cs/faculty/rich/cs170/xcomp/lib/libsys.a
    /cs/faculty/rich/cs170/xcomp/bin/coff2noff evil_test.coff evil_test
    numsections 3 
    Loading 3 sections:
    	".text", filepos 0xd0, mempos 0x0, size 0x3090
    	".data", filepos 0x3160, mempos 0x3200, size 0x1200
    	".bss", filepos 0x0, mempos 0x4400, size 0x200
    /bin/rm evil_test.coff
    /cs/faculty/rich/cs170/xcomp/bin/decstation-ultrix-gcc -c -I/usr/include
    -I/cs/faculty/rich/cs170/xcomp/include -G0  good_test.c
    /cs/faculty/rich/cs170/xcomp/bin/decstation-ultrix-ld -G0 -T
    /cs/faculty/rich/cs170/xcomp/lib/noff.ld -N -L/cs/faculty/rich/cs170/xcomp/lib
    -o good_test.coff /cs/faculty/rich/cs170/xcomp/lib/crt0.o
    /cs/faculty/rich/cs170/xcomp/lib/assist.o good_test.o
    /cs/faculty/rich/cs170/xcomp/lib/libc.a
    /cs/faculty/rich/cs170/xcomp/lib/libsys.a
    /cs/faculty/rich/cs170/xcomp/bin/coff2noff good_test.coff good_test
    numsections 3 
    Loading 3 sections:
    	".text", filepos 0xd0, mempos 0x0, size 0x3090
    	".data", filepos 0x3160, mempos 0x3200, size 0x1200
    	".bss", filepos 0x0, mempos 0x4400, size 0x200
    /bin/rm good_test.coff
    

    I included the -f switch so that make would know which makefile I intended to use. After doing many fascinating things which I won't be going over it produces good_test. Now to run it we need to tell KOS what to execute.

    Now we're ready:

    ./kos
    
    KOS booting... done.
    
    
    Probing console... done.
    a.out loaded
    new sp: 1048508
    #0: cs170 is a great class!!
    #1: cs170 is a great class!!
    #2: cs170 is a great class!!
    #3: cs170 is a great class!!
    #4: cs170 is a great class!!
    Program exited with value 5.
    Machine halting!
    
    
    Cleaning up...
    Ticks: total 19572, idle 14500, system 10, user 5062
    Disk I/O: reads 0, writes 0
    Console I/O: reads 0, writes 145
    Paging: faults 0
    
    Success !!

    What sorts of programs can you run on KOS at this point? Very very simple ones. Basically for loops and simple calls to read and write will just about test the limits of lab 2 KOS. Programs that use system calls that you haven't implemented yet will indeed break. The program evil_test.c will hopefully demonstrate this:

    #include 
    
    main()
    {
      int i;
      char *buf;
    
      buf=(char *)malloc(30*sizeof(char));
    
      for (i=0;i<5;i++){
    
        sprintf(buf,"#%d: CS170 is a great class!!\n",i);
        write(1,buf,strlen(buf));
      }
    
    }
    
    


    Note that this is essentially the same program as we saw in good_test. Instead of a static character buffer, however, I'm using malloc(). This is what I got when I ran it:

    ./kos
    
    KOS booting... done.
    
    
    Probing console... done.
    a.out loaded
    new sp: 1048508
    Machine halting!
    
    
    Cleaning up...
    Ticks: total 50, idle 0, system 10, user 40
    Disk I/O: reads 0, writes 0
    Console I/O: reads 0, writes 0
    Paging: faults 0
    

    I put the -d e flags in there to get KOS to report that it had intercepted an unknown system call, namely that to getpagesize(). It is the call that malloc() makes to getpagesize() which causes my KOS executable problems. You'll deal with this in great detail in the next lab.

    ./kos -d e
    
    KOS booting... done.
    
    
    Probing console... done.
    a.out loaded
    new sp: 1048508
    Unknown system call
    Machine halting!
    
    
    Cleaning up...
    Ticks: total 50, idle 0, system 10, user 40
    Disk I/O: reads 0, writes 0
    Console I/O: reads 0, writes 0
    Paging: faults 0
    
    Notice the "Unknown system call" message.

    How did I figure out is was getpagesize() that caused the halt?

    To figure out what system call is causing a problem use gdb and set a breakpoint at the line in exception.c that prints the debug statement:

    DEBUG('e', "Unknown system call\n");
    
    which is line 80 in my solution, run it, and then print out the variable type.
    gdb kos
    GNU gdb (GDB) Fedora 7.5.1-42.fc18
    Copyright (C) 2012 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later 
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "x86_64-redhat-linux-gnu".
    For bug reporting instructions, please see:
    ...
    Reading symbols from /cs/faculty/rich/src/cs170.labs/kos-4/kos...done.
    (gdb) b exception.c:80
    Breakpoint 1 at 0x8049a7e: file exception.c, line 80.
    (gdb) run -d e
    Starting program: /cs/faculty/rich/src/cs170.labs/kos-4/kos -d e
    
    KOS booting... done.
    
    
    Probing console... done.
    a.out loaded
    new sp: 1048508
    
    Breakpoint 1, exceptionHandler (which=SyscallException) at exception.c:80
    80				DEBUG('e', "Unknown system call\n");
    Missing separate debuginfos, use: debuginfo-install glibc-2.16-34.fc18.i686
    (gdb) print type
    $1 = 64
    
    . This tells you that the system call that was unrecognized in evil_test was 64.

    To figure out which call corresponds to 64 look in the file simulator.h and find the system call numbers.

    #define SYS_getpagesize 64
    
    What happened here is that evil_test calls malloc() which calls the getpagesize() system call on Dec Ultrix in order to know how to allocate dynamic memory for malloc() to use.

    Miscellaneous

    You will want to begin working from the start directory. Don't copy over any of the executables or object files. Rather just keep soft links to them. In case bugs are found, they can be fixed and you will always be referencing the latest versions of the code.