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 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.

Cowboys and Endians (Historical Only) This section is included for historical and informational reasons. Since we have switched to Linux on Intel hardware, there is no longer an issue. if you are interested in such things, please read this section, but feel free to skip down to the section entitled The Console.

There is an unfortunate quirk that the Intel and MIPS share -- they are both little endian machines. SPARC processors, however, are big endians. On both variety of machines integers and pointers are 4 bytes each, however little endians us a byte order that is reversed from the way people in the western world think of reading things. Thus, the integer 1 (0x00000001 -- big endian format) on a processor like the SPARC is the integer 16777216 (i.e. 0x01000000 -- little endian format) on the MIPS or Intel. Only integer, pointer, and short types are reversed -- not single byte types like char. You are, undoubtedly asking "why" either with respect to big or little endians (big is typically more intuitive for people). The answer is steeped in the lore and hardware design machismo exhibited by early pioneers on the mini- and micro- computer frontiers. Worse, it turns out that the MIPS can run in either big or little endian mode. Why the implementers chose little endian mode for their simulator is undoubtedly related to the matter of size.

Be that as it may, what looks like the integer 1 in KOS will look like 16777216 to the user running his MIPS code if you are running your simulator on a SPARC. To determine what variety of machine you are using, type

uname -a

If the line looks like
SunOS dagwood 5.7 Generic_106542-18 i86pc i386 i86pc
it is an Intel box. If is reads something like

SunOS ella 5.8 Generic_108528-12 sun4u sparc SUNW,Sun-Blade-100

it is a SPARC. It is not a requirement that you make this lab work on SPARC machines. We don't have that many and we are getting rid of them. However, if you find that using one is easier for you, or you want to make your code really portable, then you will need to do the format conversion (from little endian in the MIPS simulator to your KOS code) explicitly.

The procedures examine_registers() and run_user_code() actually perform conversion from one to another, so don't have to worry about byte-swapping in registers. However, in memory you do have to worry about this. So when you are writing a value into the user program's memory that will be accessed when the program runs, and that value is an integer, pointer, or short, you need to convert it little-endian format.

In other words, if you are in KOS and you set 4 bytes starting with memory location main_memory+8 to be the integer 1, then if the user code tries to read an integer (or pointer) in memory location 8, it will read the value 16777216. Thus, you have to perform byte swapping when you write integers and pointers into memory from KOS that is going to be read by user code. One example of this is setting argc -- when you set argc in KOS, you'll have to byte-swap it so that the user code reads the correct value. The following procedures perform byte-swapping (convert from big to little endian and back):

On the Intel boxes, though, these functions do nothing -- the are noops -- since both the MIPS and Intel machines are little endian machines. If you use them, everything will work just fine. They automatically figure out whether a conversion is needed or not.

My advice is to get your code running first without the conversion routines and then, if you have the time and inclination, try to figure out where the conversions should go. I put this section in just to warn you in case you find yourself trying to run on a SPARC machine.

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/class/cs170/test_execs/src/, which are compiled into the executable files in /cs/class/cs170/test_execs/. 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/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/class/cs170/test_execs/ directory (try halt and cpu).

    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 Unix'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/class/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 #4 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/class/cs170/test_execs/src/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/class/cs170/test_execs/src/Make.install

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

    -bash-2.05b$ make -f Make.install good_test 
    -bash-2.05b$ /cs/class/cs170/xcomp/decstation-ultrix/bin/gcc -c -I/cs/class/cs170/xcomp/include -G0 good_test.c
    -bash-2.05b$ /cs/class/cs170/xcomp/decstation-ultrix/bin/ld -G0 -T /cs/class/cs170/xcomp/support/noff.ld -N -L/cs/class/cs170/xcomp/lib -o good_test.coff /cs/class/cs170/xcomp/support/crt0.o /cs/class/cs170/xcomp/support/assist.o good_test.o /cs/class/cs170/xcomp/support/libc.a /cs/class/cs170/xcomp/support/libsys.a
    -bash-2.05b$ /cs/class/cs170/xcomp/coff2noff/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
    -bash-2.05b$ /bin/rm good_test.coff
    -bash-2.05b$

    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. In the code, I've set up a global varaible to hold the name of the file to execute ("a.out") and its argumens.

    static char *Argv[2]={"good_test",NULL};

    Now we're ready:

    coltrane:~/src/cs170/lab4> ./kos

    KOS booting... done.


    Probing console... done.
    #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 4 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:

    coltrane:~/src/cs170/lab4> ./kos -d e

    KOS booting... done.


    Probing console... done.
    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

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


    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.