CS 111
Winter 2012
Lecture 4 Notes

Scribe: Dennis Li


Hard Modularity

Definition: A type of modularity where modules are prevented from violating interface boundaries. This ensures that bugs and side effects do not propagate to other modules.

How do we implement hard modularity?

I. Client/Server Organization - The modules are separated onto a client and server, which are placed on different machines that can either be real or virtual.

Pros:
The client and server can be on different hosts. They are also protected from each other due to hard modularity.
Cons:
This approach is complicated and therefore is highly unreliable. The client/server model is slow due to the use of more cycles, power, and networking overhead.

	// Example: Client/Server factorial call

	// Client code

	send (fact_name, (m){"!",5} }; 	// 5!
	a = get_response(fact_name);
	if (a.response_code == "OK")
		print (a.val);
	else
		return ERROR;

	// Server code
	for (;;) 
	{
		receive (fact_name, request);
		if (request.opcode == "!") 
		{
			n = request.val;
			int p = 1;
			for (int i = 1, i <= n, i++)	//compute n!
				p *= i;
			response = (m){"OK", p};
		} else
			response = (m){"No Good",0};
		send (fact_name, response);
	}
	
II. Virtualization - We create a virtual machine to run our untrusted code on.

Pros:
This approach also gives us hard modularity. The real and virtual machines can be of completely different architectures.
Cons:
This approach is even slower than the first approach due to the emulation overhead

Example: We can build a virtual machine by creating an x86 emulator that runs on top of another machine. We now can run the factorial code inside the emulator.

	// Example: Factorial call using virtualization

	// x86 Emulator code
	// This code implements the virtual machine
	
	int ip;
	for (;;)
	{
	char ins = mem[ip++];
	switch (decode(ins))
		{
		case pop:
		//etc...
		}
	}

	// Place the factorial code inside virtual machine
	
	r = emul( ... ); //... is the address of the factorial code
	switch (r)
	{
	case STACK_OVERFLOW:
	...
	case OK:
	//Get return value of 'factorial'
	case TOO_MANY_INSTRUCTIONS:
	...
	}
	

This process is very slow since the physical machine must now execute multiple instructions for each instruction in the virtual machine

Since emulators are slow, how can we make virtualization faster?

Hardware Assists

Assuming similar architectures, we will need a virtualizable processor where:

These previous requirements essentially eliminates the need of an emulator!


Hardware Traps

How is control transferred?

When we have a application running inside the virtual machine, normal computations such as:
		a = b*b - c*c
can run at full speed. However what if the application need to issue a system call?
		write(1,"hello, world!\n",13);
These system calls are implemented via a delibrate crash! The INT instruction is used by convention.
		//The following code is executed in the virtual machine
		write(int fd, char const * buf, size_t bufsize)
		{
		asm("int 128"); //128 signifies a system call
		...
		}
The interrupt service routine is a software routine that hardware invokes in response to an interrupt.
Basically, the int 128 instruction is a privileged instruction. It tells the processor to look at the interrupt address table and call the function that correlates to 128 in the table. This function causes a hardware trap and the kernel pushes the following items onto the kernel's stack: After this, the kernel or virtual program can now do what it wants, including executing privileged instructions. To return from the interrupt, the RETI instruction is executed, which restores the previous states.
RETI

Layering

By using layering, we can create levels of abstraction between the hardware and the kernel and then the kernel and the application.

Application Binary Interface (ABI)

The ABI describes the low-level interface between an application program and the OS.
ABI

Instructions:
Priviledged code should be able to access all instructions while unprivileged code should only have access to unpriviledged instructions.

ALU's and Registers:
The virtual machine should be given direct access to these

Primary Memory (RAM):
The virtual machine should be given selective direct access to RAM. Protecting memory is hard (virtual memory) and so we will cover this later in the course.

I/O Registers:
The virtual machine should be given indirect access to these through system calls. We also need to virtualize registers so that each program thinks it has its own set.

How is all this information tracked?

It is the job of the kernel to keep track of all the previous information by using process descriptors

Process descriptors are structures that help describe each process and contain information such as the registers that a process owns. It contains the process id, a copy of the stack, and a copy of the virtual registers. The virtual registers in the process desctriptor are only valid when the process is not running. Instead, when the process is running, the virtual registers in the process descriptor contain garbage values because the actual hardware registers are used. When the process is suspended, the current register values are then stored into the virtual registers in order to save the state.

Process Descriptor

Why do we have to do all this and keep track of processes? Why would a process not be running?

On a single-core machine, at most one process can run at a given time.

n = getpid() // This command gets the pid of the calling process

Forking Basics

How do applications create or destroy processes?

To create a process:

p = fork()
This clones the current process, except: We can have a process wait on its own children by using the following function:
pid_t waitpid(pid_t p, int *status, int options)
		// Example of using waitpid
		
		int* c;
		q = waitpid(p,c,0); // q = -1 if error, q = p if OK
To destroy a process: