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:
The operating system makes the procedure call run_user_code(int registers[NumTotalRegs]) to get back to running user code. This procedure call tells the simulator to load the specified set of registers into the CPU registers and set the execution mode to user mode. Note that these two functions constitute an exit from the OS. By setting the PC to point to a user address and switching modes to user-mode, the next instruction executed is in the user program. The CPU will then run (starting with the instruction in the PC register) until an exception or interrupt occurs, which switches it back into supervisor mode.
Exceptions and interrupts put the operating system back in control of the CPU in the procedures exceptionHandler (ExceptionType which) and interruptHandler (IntType which) respectively. The argument which specifies the type of exception or interrupt (e.g. system call, arithmetic error, timer interrupt, console ready, etc). The registers are saved by the simulator at the time of the interrupt. Their contents may be examined using the examine_registers(int buf[NumTotalRegs]) procedure. You may not return from exceptionHandler() or interruptHandler() to give CPU control back to the user program that had it when the interrupt occurred. Instead, you use run_user_code() with the correct set of registers or you call noop() when you want to idle the machine. In the event that no user code can be run when an interrupt is finished, noop() should be used so KOS can do nothing until the next interrupt occurs. When the interrupt does occur, the state of the registers is meaningless.
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 likeSunOS dagwood 5.7 Generic_106542-18 i86pc i386 i86pcit 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.
KOS communicates with this console through two procedures and two interrupts.
console_read() returns -1 when the user types CNTL-D.
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().
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.
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.
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:
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.
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).
Also, you might take advantage of Heloise's Helpful Hints for Lab #4 as she always seems to have advice to lend.
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.
/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:
#includemain() { 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.