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: when the machine starts running (initialization time), when a user program running in the simulator causes an exception (system call, page fault, etc.) and when a device interrupts. Each time this happens, 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
Basically, things will work as follows. There are two object files for the simulator:
When you run the executable, your operating system gains control. This happens in a subroutine called JOS() (no arguments), that you have to write. This state of affairs is not so different from an actual operating system. If you were implementing JOS 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 JOS() and your operating system gains control. From then on, the interaction between the operating system and the simulator is acheived 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. Thus constitutes 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).
Finally, there is a simulator procedure called noop() which tells the simulator to make the CPU do nothing until the next interrupt occurs. In the event that no user code can be run when an interrupt is finished, noop() should be used so JOS 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.
There is an unfortunate quirk between the SPARC's and the MIPs -- in the inimitable words of Professor Wolski, ``they are endians from different tribes.'' In both machines, integers and pointers are 4 bytes each, however their byte order is reversed. Thus, the integer 1 (0x00000001 -- big endian format) on the SPARC is the integer 16777216 (i.e. 0x01000000 -- little endian format) on the MIPS. Only integer, pointer, and short types are reversed -- not single byte types like char. You are, undoubtedly asking "why" 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 something we'll never know -- we simply have to accept it as a given.
Be that as it may, what looks like the integer 1 in JOS will look like 16777216 to the user running his MIPs code. The procedures examine_registers() and run_user_code() actually perform conversion from one to another, so you 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 to little-endian format.
In other words, if you are in JOS 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 perfrom byte swapping when you write integers and pointers into memory from JOS that is going to be read by user code. One example of this is setting argc -- when you set argc in JOS, 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):
If you don't specify a jconsole program with -c, then JOS uses its own standard input/output as the console. This makes life easier if you are using a windowless terminal. However, if you are in an Xwindows environment, you should use jconsole because it will help clarify things.
JOS communicates with the 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.
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 by the simulator of JOS -- in other words, you can only execute them by loading them into JOS.
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.
Copy /home/cs560/labs/lab4/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 test_execs directory (try halt and cpu).
Now, you are to make the following changes to jos.c:
Remember that if the user specifies an address of x, that is not address x in JOS. 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, JOS 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 JOS in such a state that future system calls will break.
The code for step 5 of WHAT_DR_PLANK_DID is in the directory /home/cs560/labs/lab4/Step5. When you get to step 5, it will be a good idea to look at my code, and perhaps copy it and start from there. (More priceless advice from Professor Wolski: ``For those of you tempted to begin your enjoyment of Lab #4 by redefining the counting numbers to start with 5 and simply starting here, realize that you may be asked to expound upon steps 1 through 4 at some future social gathering. It would be gauche not to be prepared, would it not?'')
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.