Operating Systems 2018F: Tutorial 1

From Soma-notes

In this tutorial you will be learning about 1) the difference between system calls, function calls, and library calls and 2) how processes are created.

Getting started

To do the following exercises, bring up a terminal on a system running Linux, preferably Ubuntu Linux 18.04. In the SCS labs, run the COMP24XX virtual machine in VirtualBox. (Log in as user student, password student.) If you wish to use this VM from your own laptop, you can download it and import the appliance into VirtualBox. Note that the import may generate an error or two regarding network interfaces; if you just delete everything but the first interface it should work fine.

For further help, please talk to the TAs in your lab.

Function calls, library calls, and system calls

For hello.c, syscall-hello.c, and later csimpleshell.c (after you've done the part below on csimpleshell) do the following (substituting the appropriate source file for prog.c):

  1. Compile the program prog.c using gcc -O2 prog.c -o prog-dyn and run prog-dyn. What does it do?
  2. Statically compile and optimize prog.c by running gcc -O2 -static prog.c -o prog-static. How does the size compare with prog?
  3. See what system calls prog-static produces by running strace -o syscalls-static.log ./prog-static. Do the same for prog-dyn. Which version generates more system calls?
  4. See what library calls prog-static produces by running ltrace -o library-static.log ./prog-static. Do the same for prog-dyn. Which version generates more library calls? (If ltrace isn't installed, run sudo apt-get install ltrace)
  5. Using the nm command, see what symbols are defined in prog-static and prog-dyn.
  6. Run the command gcc -c -O2 prog.c to produce an object file. What file was produced? What symbols does it define?
  7. Look at the assembly code of the program by running gcc -S -O2 prog.c. What file was produced? Identify the following:
    • A function call
    • A system call (if any)
    • A global/static variable (if any)
    • A local variable
  8. Disassemble the object file using objdump -d. How does this disassembly compare with the output from gcc -S?
  9. Examine the headers of object file, dynamically linked executable, and the statically linked executable using objdump -h
  10. Examine the contents of object file, dynamically linked executable, and the statically linked executable using objdump -s
  11. Re-run all of the previous gcc commands adding the "-v" flag. What is all of that output?
  12. (optional) Look up the documentation for each of the system calls made by the static versions of the programs. You may need to append a 2 or 3 to the manpage invocation, e.g. "man 2 write" gets you the write system call documentation.

Creating processes, running executables

For this part you will be playing with and modifying csimpleshell.c by Enrico Franchi. The source is also listed below.

  1. In csimpleshell, change the prompt to be the current user (e.g., "student $"), as reported by the USER environment variable.
  2. Make csimpleshell not call wait() on the command if there is an & as the last argument to a command; instead, just return another prompt. Can you see the zombies that are now produced?
  3. Change the execvp() call to execve(). Where do you get the extra argument? (NOTE: When you switch to execve() you will have to specify the full path to commands, e.g. /bin/ls not ls.)
  4. Add an environment variable called LASTCOMMAND that contains the last command that was executed by csimpleshell. This environment variable should be passed on to each new program that is run. How can you check that your code works?
  5. Use sigaction() and waitpid() to create a signal handler for SIGCHLD that prevents the creation of zombies for background commands.
  6. (Advanced) Implement I/O redirection for STDIN (<) and STDOUT (>). Do the same for arbitrary file descriptors (e.g., 2>).


Code

hello.c

#include <stdio.h>

int main(int argc, char *argv[]) {

        printf("Hello world!\n");

        return 0;
}

syscall-hello.c

#include <unistd.h>
#include <sys/syscall.h>

char *buf = "Hello world!\n";

int main(int argc, char *argv) {
        size_t result;

        /* "man 2 write" to see arguments to write syscall */
        result = syscall(SYS_write, 1, buf, 13);

        return (int) result;
}

csimpleshell.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#define BUFFER_SIZE 1<<16
#define ARR_SIZE 1<<16

void parse_args(char *buffer, char** args, 
                size_t args_size, size_t *nargs)
{
    char *buf_args[args_size]; /* You need C99 */
    char **cp;
    char *wbuf;
    size_t i, j;
    
    wbuf=buffer;
    buf_args[0]=buffer; 
    args[0] =buffer;
    
    for(cp=buf_args; (*cp=strsep(&wbuf, " \n\t")) != NULL ;){
        if ((*cp != '\0') && (++cp >= &buf_args[args_size]))
            break;
    }
    
    for (j=i=0; buf_args[i]!=NULL; i++){
        if(strlen(buf_args[i])>0)
            args[j++]=buf_args[i];
    }
    
    *nargs=j;
    args[j]=NULL;
}


int main(int argc, char *argv[], char *envp[]){
    char buffer[BUFFER_SIZE];
    char *args[ARR_SIZE];

    int *ret_status;
    size_t nargs;
    pid_t pid;
    
    while(1){
        printf("$ ");
        fgets(buffer, BUFFER_SIZE, stdin);
        parse_args(buffer, args, ARR_SIZE, &nargs); 

        if (nargs==0) continue;
        if (!strcmp(args[0], "exit" )) exit(0);       
        pid = fork();
        if (pid){
            printf("Waiting for child (%d)\n", pid);
            pid = wait(ret_status);
            printf("Child (%d) finished\n", pid);
        } else {
            if( execvp(args[0], args)) {
                puts(strerror(errno));
                exit(127);
            }
        }
    }    
    return 0;
}