By Danny Cheung, Ahmed Shama, Peter Wu
Thursday Oct 5, 2006
Today's Big Topics Include:
Recap from previous lecture: what do interfaces need to be designed for? Performance, Robustness, Neutrality, Simplicity.
Different Kinds of Layering
The first two are associated with caching and prefetching.
int fact(int x) { if (x == 0) return 1; else return x * fact(x - 1); }
In memory, we would use the stack to implement this. To get a better picture of the memory layout and the different types of memory, refer to the following diagram of a 4GB block of byte-addressed memory. As the heap and stack memories expand, they grow towards each other.
In most languages (C, C, Java, etc), every function can only return once (In other words, return at most one value). For example, if we call fact(2,400,000), then that function calls fact(2,399,999) and suspends the previous one. See call/cc (alternate link) for examples from Scheme, where functions can actually return more than once!
On an x86 machine, factorial in assembly looks like this. (The addresses are not real, but the code is.)
10: fact: cmpl 4(%esp), $0 # if argument does not equal to 0, jump to .L2 11: jne .L2 12: movl $1, %eax # set %eax, which we use for the return value, to 1 13: ret # pop the argument of the stack 14: .L2 movl 4(%esp), %eax # copy argument to register %eax 15: decl %eax # decrement the register; subtract from the argument 16: pushl %eax # push argument onto stack 17: call fact # recursive call to fact 18: addl $4, %esp # gets rid of argument 19: mull 4(%esp), %eax 20: ret
In line 10, the cmpl instruction compares whether the argument, 4(%esp), equals 0. The esp register represents the stack pointer, and 4(%esp) represents 4 bytes off the stack pointer.
Now let's call fact(5)
0: pushl $5 # argument pushed onto stack 2: call fact 6: addl $4, %esp
This returns the value 120. The following figure shows how the stack would look like after all the values have been popped onto the stack. This stack grows down.
The call command does the following:
The ret command:
Now suppose that in the assembly program of factorial, we inserted the following line between lines 10 and 11:
movl $0, 12(%esp)
This would replace the '5' that's located 12 bytes from the current stack pointer with a '0'. Now the function would return the value 0. Or, suppose instead that we inserted the line:
movl $0, 8(%esp)
This would move the stack pointer and replace '6' (the return address) with 0, changing the return value to 0
Because functions can tamper with other functions' state, function call modularity isn't looking like hard modularity!
A safe language like Java prevents buffer overflows. It does not, however, solve the infinite loop issue, because of the halting problem.
Buffer Overflow
We saw an example of this earlier, in the above scenario where we inserted extra lines of x86 code and moved the stack pointer. In another example, someone submitting a comment on microsoft.com could manage to take control of the entire Web site. Suppose the code to process the comment was as follows:
int process_comment(int fd) { // fd is a file descriptor char buf[100]; int i = 0, c; while ((c = readc(fd)) != '\n') { buf[i++] = c; } /* format using ASP XHTML... blah blah; */ display; }
Someone could take over the Web site by overflowing the buffer. Instead of submitting English text, one could submit x86 code that overflows buf, overwriting the stack's return address and other information stored on the stack. The website is compromised!
Privileged Instructions
Another example comes from the MIPS-X machine, designed from 1984-1987, which had the following command:
0xCFC00000 == hsc Halt and Spontaneously Combust
The semantics are "The processor stops fetching instructions and self destructs. Note that the contents of Reg(31) are actually lost."
This is a joke (although it really appears in the MIPS-X instruction set manual), but the point still stands. What happens when you call a function that executes an instruction that can cause damage to your program or to the machine?
Soft Modularity = Fate Sharing. A bug in one module affects others; this is another example of propagation of effects. We want hard modularity.
Hard modularity can be accomplished through the following.
Clients and services interact only by sending messages to one another. Clients send requests, and services respond to those requests.
This accomplishes the following:
For example, if a client sends a message and does not receive a response, a timeout protocol will prevent the client from waiting indefinitely. This prevents the previous problems with soft modularity from occurring.
One suggestion for the counting French words problem is to get 3 computers, with each one accomplishing a different task:
Here, the links abstraction would be the message-sending channels. This setup limits error propagation; if one machine crashes or goes into an infinite loop, the other machines can still operate. The interactions are clearly defined and limited, and machines can disregard any interaction/message that violates the predefined conventions. Yet, is there a problem with this setup? Yes, modularity is determined by resources (i.e. money). This is very bad: esepcially when processing power is so abundant, good robust programs should be available to everyone, not just people with mucho cash! The solution to this is virtualization.
Virtualization is the process of simulating another object's interface. A virtual version behaves roughly as the real version. We need three abstractions for each virtual machine:
In our example with the three computers, virtualization would eliminate two computers by replacing them with processes and virtualize them into one machine. However, the modularity is not 100% perfect, because the three virtual machines are physically tied to each other. For example, a power outage would cause all three processes to fail. Nevertheless, the modularization is much harder than in the simple function call case: for instance, the processes will not be able to scribble over each other's memory.
In order to achieve virtualization, we need help from the processor. We need a virtualizable processor. For example:
hsc and other privileged instructions have to trap into the kernel. In other words, the kernel gets control when the virtual computer executes.We have now virtualized every part of the Von Neumann computer. Each of these virtual computers are called a process.
A process is like a virtual computer. A process is: a program in execution in an isolated domain. A program is a collection of instructions that achieve a specific function.
In this setup, the message channel are the files. The same interface (i.e. read and write) that applies to the disk can be used for message channels! Unix's big idea: Everything is a file.
translate < file | wordcount
The bad way to do it:
Steps 3 through 5 are bad, because we're changing the processes' state after they have already started. This violates virtualization; the process could already be in a different state from its original state. Instead, we change the message channel using two Unix functions:
pid_t fork() // makes a copy of the current process called the child process int execvp(const char *file, char *const argv[]); // replaces current process with a new program (EXCEPT: file descriptors)
To distinguish between the two copies (parent and child) of the process, fork returns different values in both programs. The child process will return a 0 and the parent process returns the child pid. If a failure occurs, the return value will be -1.
execvp takes two arguments: *file should point to the file name to be executed, and argv[] is an array of pointers to arguments of the file that we want to execute. If execvp returns -1, then an error has occurred.