Lecture 4: OS Organization

CS111 Scribe Notes

By Edwin Wong
Date of lecture: 1/16/2013


Virtualization

One method of implementing a virtual system with a protected one way transfer of control is with an interpreter.

Advantages:

Disadvantages:



With an interpreter, we want to be able to add on a few instructions to a machine and run it inside its kernel. An x86 machine can then be simulated ontop of a non-x86 machine. The following case does not work as intended because the callee is vulnerable to changes that the caller makes. Remember that we also want a protected one way transfer of control.

 pushl filename 
call unlink // a new instruction
addl $4,sp






In the above case, instead of call unlink, we replace it with an invalid instruction, 242. On an x86 machine, the instruction would not be executed, but instead an interrupt or trap would occur. This allows us to switch to kernel mode, look up the appropriate interrupt type in the interrupt table and execute its code. In the example below, an interrupt is called of type int 0x80, a system call. When the interrupt is executed, the hardware pushes the stack segment (ss), the stack pointer(esp), the processor flags (eflags), the code segment, and the instruction pointer (eip). When reti is executed, the state of the computer to returns back to when the interrupt was executed. If we change, the address pointed to by this interrupt, we can make the kernel execute any code that we want; however, this can be easily fixed by making the interrupt table inaccessible if the caller is found to be suspicious.

interrupt table



Priviledged vs Unpriviledged Instructions:

Priviledged instructions can only be executed by the kernel. User applications are unable to do so, which adds a layer of protection against malicious attacks or accidentally executing harmful instructions by a user application. Ultimately the kernel is free to protect itself from user applications, but not the other way around.

Priviledged Instructions

Unpriviledged Instructions: movl, addl, mull, call, ret

Virtualization Processor- The Layers

Abstraction layers in computer architecture allow for individual components to build from each other, and hides unnecessary information. In the image below, the I/O devicer reads a stream of bytes stored in the hardware, which can then be used by user applications.

layers

x86 systems follows the microkernel approach,where there are separate layers for the memory, the driver, file systems, and applications. In Linux and many Unix based systems, there are only two layers, the Kernel and the unpriviledged. The two layer method is only benefitial if the kernel is perfect and has no bugs. The extra layers of the x86 adds more security at the expense of performance and simplicity.


I/O: How to read bytes from disk?

At the C level:

insl(ID, char* buffer, bytes) 





At the machine level:
movl $39, %eax 
movl $192, %ebx
lea buf, %ecx
movl $256, %edx
int 0x80 //syscall interrupt



In the left assembly code, a few registers save the given immediate values and byte addresses then the interrupt 0x80 calls the kernel. At that point, the kernel can then start reading from disk. Although this works, there are too many operations and is too low leveled for the needs of most programmers.



At the High level:
char * readline(File *f){
	movl $119, %eax
	movl f, %ebx  low level register
	int 0x80	
}


How do we implement a high level object using low level concepts?

1) use a pointer to an actual object
2) use an integer as a handler by the OS



Problems with readline?

Because the readline function is too high level, we could consider a lower level implementation that reads sectors.
void read_sector (int device_no, char* buf, int size_sectors)




The read_sector function takes a device number specifying the defice, a char buffer to store the retrieved data, and an integer specifying the number of bytes to read. The problem with this implementation is that, the device number is not a portable method of locating the data that you want to read. Instead, use a file descriptor. The function also requires a sector size variable, but it is very difficult to know the number of bytes in a sector. In addition, the kernel would also need to buffer because the physical memory size can be different than the number of bytes allocated; thus slowing down the function.


A Better Implementation:

We can combine the high level function, read, and a low level function, lseek to perfom I/O from disk. With this implementation, a user has the freedom to find exactly where to start reading and the number of bytes to read from that offset.
 off_t lseek(int fd, off_t offset, int flag)





lseek allows the repositioning of the offset in the file specified by the file descriptor. If it succeeds, it returns the offset location, otherwise it returns -1 to indicate an error. In most implementations, such as the x86, off_t is a signed integer, meaning that for one call to lseek, the maximum offset is 2GiB. A user is also given the option to set the offset past the range of the file.
 ssize_t read(int fd, char* buf, size_t count) //count=# of bytes to read




The read function attempts to read up to count bytes from the filedescriptor into the buffer; it returns the number of bytes read on success, otherwise it returns -1. Note that the type of count is size_t, which means that a user can specify up to 32 or 64 bit unsigned number of bytes. However, the return type of the read function is ssize_t, which is a signed version of size_t. read can then only read up to 2GiB of data for a 32bit machine, which is only half of the maximum bytes that can be specified in count.

Other problems: