Programs that exhibit modularity are easier to understand and debug. A common way to introduce modularity into a program is by dividing it up into functions that call one another. However, this approach is classified as soft modularity because implementation errors can cause unwanted interactions that go beyond the specified interfaces. To introduce more enforced boundaries or hard modularity into a program, one can implement client-server virtualization.
The simplest way to implement client-server virtualization is to write a simulator of the x86 architecture. The simulator will include:
The downside to this implementation is that it is too slow.
The architecture and simulated architecture should be the same or similar. The instruction set is partitioned into dangerous and safe.
Hardware has 2 modes:
By convention, if an application runs a certain privileged instruction, a system call is executed.
In unprivileged mode, the INT instruction will trap. The kernel code will execute in privileged mode when the trap occurs. This is referred to as a protected transfer of control.
Hardware: INT x pushes
ss stack segment
esp stack pointer
eflags (e.g.: privileged?)
cs code segment
eip instruction pointer
error code
eip = trap[0x80]
Software: By convention, in Linux the system call number is stored in %eax.
Arguments are placed in %ebx, ecd, %edx, %esc, %ebp
How does one return from a system call?
CALL --> RET
INT --> RETI pops everything off the stack. The return value is stored in %eax.
However, this method is slow compared to function calls. Recent versions of Linux use a different calling convention.
SYSENTER sets cs, eip, ss, esp to values in machine-specific privileged registers
SYSEXIT does the opposite
A process is a program in execution on an isolated domain that can be manipulated in Unix by issuing system calls.
pid_t getpid(void); gets the correct process type
/usr/include/systypes.h typedef int pid_t
pid_t fork(void); "clones" the current process
int main(void) {
for (int i = 0; i < 3 i++)
fork();
return 0;
}
The child differs from the parent in terms of the return value of fork(), the pid/ppid, and file descriptors. In addition, the child has no file locks or pending signals.
int execvp(char const *file, char *const *argv);
returns -1 always, because if it has returned then it hasn't totally changed the program.
void _exit(int)
exit status 0-255, 0 is success. Doesn't destroy process object.
pid_t waitpid(pid_t pid, int *status, int flags);
A process can be runnable or exited.
$ date -u
Jan 15 2015 ...
$ which date
/usr/bin/date
bool print_date(void) {
pid_t fork();
if (p < 0) return false;
if (p == 0) {
execvp("/usr/bin/date", (char *[]{"date", "-u", 0});
exit(127);
}
int status;
if (waitpid(p, &status, 0) < 0 )
return false;
return (WIFEXITED(status) && WEEXISTSTATUS(status) == 0);
}
int posix_spawnp(pid_t *restrict pid, char const *restrict file, posix_spawnfile_actions_t *file_acts, posix_spawnattr_t *restruct attrp, argv, envp)