Heloise's Helpful Hints for CS560 Lab #4

Aunt Heloise


JOS -- A Tasty and Nutritious to Start Your Day

When cooking up a batch of JOS, there are a few ways to make your creation a crowd pleaser instead of something that makes your family and guests say "please pass the Windows-NT." Here are things I do when I want my JOS Lab #4 to to keep them coming back for more.


Gathering and Using your Utensils

Before you begin, it will be best if you have all of your utensils near you and you are proficient in using them. We have provided a series of modules that make this lab MUCH easier to complete. In particular, you will very much want to understand how to use the dllist routines and (extremely important) the CBThreads package (see the lecture notes for cbthreads lectures). Without these tools, you are really missing some of technology's greatest time savers.


Compiling JOS and The Code Structure

You may be confused about how the simulator, your JOS code, and a user program fit together. I know I was. Here is the presentation of JOS that helped me make the most tasty Lab #4. The thing to realize is that the simulator and your code are being combined to simulate the behavior of a DEC MIPS machine and its OS. To understand how this works, you probably need to think back to your CS160 class, your CS360 class, or any positive personal experience you've had with assembly language programming. Recall that each assembly language instruction changes a small part of the machine's "state" (registers, condition codes, memory, etc.) If you think about it for a minute, you could probably write a C program that defines a variable for each piece of machine state, and then walks through an assembly language program one-instruction-at-a-time making the same state changes that the hardware would have made. For example, consider the register set and a fictitious assembly language instruction "ADD R1 R2 R3" that adds the contents of R1 to the contents of R2 and puts the results in R3. You can imagine defining R1, R2, and R3 as integers and writing C code that does this if it encounters the string "ADD R1 R2 R3." If you do this in a detailed enough way, and if you are willing to read the instructions in the format that they are stored in an actual Unix binary (as opposed to as strings) you have built a machine simulator. The file /home/cs560/lib/linux/libsim.a that you need to load with does exactly this for MIPS binaries that have been compiled for the DEC Ultrix version of Unix.

The next thing to appreciate is that when a program makes a system call, it is really issuing a special assembly instruction called a TRAP instruction. It is up to the operating system to define where it expects to find the arguments to the system call when a TRAP is executed. Usually, some set of registers (like 5, 6, and 7 maybe?) are chosen. When the compiler compiles in a system call, it arranges for the arguments to be loaded into the right registers before the TRAP is issued. So what happens when the simulator "simulates" a TRAP instruction?

The answer, in our case, is that your code gets control through the exceptionHandler() subroutine. That is, you are actually writing the part of the simulator that deals with a TRAP instruction and you are writing in C.

Indeed, the way I visualize the problem is to think of a simulator as having being written, but which is missing a couple of modules. So your job, then, becomes to write code that is loaded with the code you are given to complete the full simulation. Think of it as a simulator with a hole in it that you must fill in. The next question, then "what parts am I given?"

The libsim.a and main_lab1.o (which you must load with) combine to give you

If someone were to give you a MIPS binary (called "a.out" maybe?) that was compiled for DEC Ultrix, and you were to tell the simulator to load it into main_memory, and then you were to set the simulator's program counter to the first instruction and to initialize your OS, then the simulator could start to run your program. It will call your OS when it reaches a TRAP instruction or when your simulated console device interrupts.

One point of confusion here, though, might arise over the difference between compiling your simulator+OS and cross-compiling so you can make your own MIPS/DEC Ultrix binaries. YOU compile your OS code for Solaris using gcc. The simulator (libsim.a) you are given is compiled for Solaris using gcc as is main_lab1.o. We also have a special version of gcc available that let's us build binaries for the MIPS processor running DEC Ultrix. The directory /home/cs560/test_execs/i686-pc-linux-gnu contains a bunch of C programs that have been compiled with this special version of gcc. You should use them as the programs to load when you are running your simulator+OS.


Structure of Lab #4

You are really being asked to accomplish four independent tasks in Lab #4:

You should think of these as separate assignments, but do them in this order as each builds skills used in the next. You are strongly encouraged to follow WHAT_DR_PLANK_DID as it is a step-by-step list of things to accomplish in order to finish the lab. Understand each step thoroughly before moving on and try to see where each of the four tasks begins and ends.

Here are some realizations that may help you make things go more smoothly.

OS Initialization

The key thing here to realize is that the simulator is expecting your code (the OS kernel) to do its business, store off anything it will need to remember when the next exception or interrupt occurs, and then call run_user_code(). When run_user_code() executes, your code is done. Anything you store in a global variable (like the ready queue) will be preserved, any blocked threads will still be blocked, but the currently running thread dies an ignominious death. The structure, then, that minimizes the ignominy of your OS is as follows:

exception called
     	.
	.
	.
you cbthread_fork what ever it is you need to get done
	.
	.
	.
the exception handler does a cbthread_joinall() and calls
a routine that eventually calls run_user_code().  
Notice the thread synchronization structure. The cbthread_joinall() won't return until all of your other threads have either successfully called cbthread_exit() or have blocked themselves on a semaphore. That is, when there is no more work for the kernel to do, the cbthread_joinall() fires and your code goes back into user mode.

Writing the Console

There isn't much to say here other than what Dr. Plank has in his recipe. The high-level realization to have is that the semaphore is really being used as a blocking lock. That is, a P() call locks the console, and a V() call unlocks it so another character can be written. Also, you are asked to use a second semaphore to implement exclusive access to the console. Ask yourself why that might be.

Reading the Console

Here, things are a bit different. The structure that is advocated is that you create a reader thread that consumes characters from the console and buffers them in kernel space. Many actual hardware devices work in this way with respect to unsolicited interrupts. The piece of hardware typically has a very small buffer (one character in this case). Your kernel buffer lets the system pull characters out and store them until a process can consume them. Also, the most elegant solution to this part of the lab takes advantage of the ability of semaphores to count how many "wake-ups" have occurred. If you implement this part of the assignment using a semaphore as a lock around a separate counter, it will work, but you may wish to review semaphores a bit.

Initializing the User Program's argc and argv[]

This part of the lab is, by far, the most exotic and flavorful. It is really pretty straight forward, but at the same time it is fraught with dangerous undertones. Basically, you need to realize two things in order to make it a pleasant experience. First, you should try and visualize where argc and argv[] are going to live and how it is that the MIPS simulator will find them. Here is a really bad picture of their neighborhood. They live on "the wrong side of the stack."

00000:	|---------------|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
	|		|
    	|		|
sp:	| used by C  	|
	| used by C	|
	| used by C 	|
	| argc  	|
	| &argv[0]	|----
	| &envp[0]	|   |
   ----	| argv[0]	|<---
   |	| argv[1]	|
   |	| argv[2]	|
   |	|   .     	|
   |	|   .		|
   |	|   .		|
   |	| argv[argc-1]	|
   |	| NULL		|
   ---->| string0	|
	| string1	|
	| string2	|
	|   .     	|
	|   .		|
	|   .		|
	| string.argc-1	|
	|---------------|
Study this picture. Go ahead. Close your eyes. Now, visualize it. This is the organization that the MIPS/DEC Ultrix process-launching mechanism assumes will be in place when a program is initiated. Bigger addresses are at the bottom of the figure, by the way. Yes, it is weird, but it is less weird than writing the string values backwards. Think about it. Anyway, the stack (which grows from bigger addresses to smaller addresses) grows up in this figure.

And that is it. Follow Dr. Plank's recipe and keep these helpful hints in mind for a fluffy and perky Lab #4 every time.