Changelog:

Your task

  1. Obtain a copy of a VM or another Linux environment (most likely not OS X, see below) with:
    • a working C and C++ compiler that supports 32-bit x86
    • a recent copy of qemu installed (configured to support full 32-bit x86 emulation).

    On 17 Jan 2020, we fixed a problem in the version of xv6 we distributed that prevented compatibility with some Linux versions (more recent than our VM). If you experienced problesm where xv6 would not boot, try pulling a newer version.

    We have supplied a suitable VM image here (login, if needed, with username ‘student’, password ‘password’; use with virtualbox), or you can use your own. (Our VM image runs Ubuntu 18.04 LTS, if you want to match it as closely as possible in your own. Note that full support for 32-bit compilation on Ubuntu 18.04 requires gcc-multilib)

    If you are comfortable using Linux from the command line, then some students in the past have been happier with the performance of a VM that only provides SSH access (such as what vagrant typically does).

  2. Download and get xv6 to boot in an emulated machine. For how to do this, see the instructions below. Run it and make sure you can run some simple commands in a shell in the emulated machine.

  3. Add a new system call writecount() that takes no arguments and returns the number of times the write system call has been called across all processes. See below for hints about how to do this.

    We do not care how your writecount() system call counts calls to write that fail, such as from having invalid arguments. We also don’t care how it treats simulataneous calls to write() and/or writecount().

  4. Add a new system call setwritecount(int new_count) that takes one integer argument and resets the count returned by the writecount call to new_count. The count should continue incrementing from any calls to write() made afterwards.

  5. Write a program that uses and tests these new system calls. See below for hints on how to do this.

  6. Run make submit to create a .tar.gz archive and upload it to the submission site.

Hints

Getting xv6 running

  1. Download our version of xv6 using git: git clone https://github.com/uva-reiss-cs4414/xv6.git

  2. In the newly created xv6 directory, run make.

  3. Boot xv6 in an emulator using one of the following methods:

    • To boot the OS in an emulator with a graphical user interface, run make qemu.

    • To boot the OS in an emulator without a graphical user interface, run make qemu-nox. (nox probably standards for “no X”; X is short for the X windowing system, the primary graphics system on Unix-like operating systems.)

    You can shutdown the system using the shutdown command or using the key sequence: control-a, then x.

Adding the system call

For this task, you might want to read

Basic steps for adding the system calls:

  1. Create a sys_writecount function (or sys_setwritecount) based on an existing simple system call function like sys_uptime. (For this assignment, you do not need to (but are allowed to) use a spinlock and acquire or release like uptime does since we do not care how your code works with multiple processors.)

    The convention in xv6 is that file-related system calls are in sysfile.c and process-related system calls are in sysproc.c. (I would consider sys_writecount more of a file-related system call.) But this is just a convention, and you can place the system call implementation in any .c file which is part of the kernel.

  2. Add a system call number for your new system call to syscall.h.

  3. Add your sys_writecount to the table in syscall.c.

  4. Edit sys_write. to update a counter read by sys_writecount. You can use a command like grep sys_write *.c to find out where it is.

  5. Edit usys.S and user.h to create a system call wrapper function that invokes your system call from a normal user program.

  6. Follow the same procedure for setwritecount, but to get an argument use the argint function. You can look at sys_kill for an example of how this is used. The first argument to argint is the index of the argument (0 for the first argument, 1 for the second, and so on.)

    Notice that though a system call like kill takes an argument that can be retrieved with argint, the prototype of the corresponding sys_* function is the same as for a system call that takes no argument.

Testing your system call

  1. Using echo.c as a template, create a new program to run your writecount system call and print the results.

  2. Edit Makefile by adding your program to UPROGS, similar to how echo is included on this list.

  3. Run make and then make qemu to boot the OS with your new program included.

  4. If your test program crashes after finishing, you may have forgotten to exit() at the end. (Returning from main() will not work.)

  5. You could run other programs that call write() (e.g. by outputting anything to the console) and/or have your test program make writes (using the write() system call wrapping function directly or by taking advantage of printf() calling write) to verify that the count make sense.

Note on printf implementation

  1. The built-in xv6 printf() implementation, whose source code is in printf.c, is implemented by calling write(), but it often calls write more than one time per call to printf().

Locks not required

  1. sys_uptime uses a spinlock, which it uses to handle the case where xv6 is running on multiple processors or when the system call is interrupted by a timer interrupt. For this assignment, we do not care if you handle this problem. (We will ensure only one process is calling write() at a time when testing.)

  2. An implementation of writecount that correctly handled multiple concurrent calls to write() and/or writecount() would probably use a spinlock around accesses to the counter (from both write and writecount).

Debugging

  1. In the kernel, you can use cprintf() to output messages to the console. This is my primary mechanism for debugging. Outside the kernel you can use printf() to output debug messages.

  2. You can run xv6 under debugger gdb using the instructions below.

  3. The xv6 kernel contains many situations where it asserts that some condition that should always be true is true, and if not, it deliberately crashes the system in what is called a panic. For example, trap.c contains code that panics when an unexpected type of exception occurs while code is running in kernel mode, such as the equivalent of a segmentation fault in kernel code. When this happens, xv6 prints out a message like the following:

    lapicid 0: panic: trap
    80483231 80d48a34 80033423 0 0 0 0 0 0 0 
    

    in this message:

    • 0 is the number of the CPU that was running when the panic occured. Since we always run xv6 in single-core mode, this will generally be 0.
    • trap is the message that was passed to the panic function. Usually, the xv6 code is written so that there is only one call to panic with a particular message, so this will precisely identify where the panic occured.
    • 8048323, 80d48a34, etc. are the hexadecimal addresses of the code that was running when the panic() was called. You can look up these addresses in kernel.asm, which contains the full assembly of the xv6 kernel, interleaved with its machine code, the addresses of each instruction, and the corresponding C code.

xv6 book: note on terminology

CS 3330 and the xv6 book and source code use slightly different terminology related to exceptions:

CS 3330 Term most common xv6 term description
exception trap any event in which the processor transfers control from whatever program was running to the OS (to the “kernel”) at a location chosen by the OS
fault exception a program performs an illegal action, causing control to be transferred to the OS to decide what to do
interrupt interrupt an event external to the program, such as a timer or an input/output device needs the OS’s attention
trap (specific kind) trap an exception deliberately triggered by an instruction; on x86 this is via the int instruction

a note on GDB

  1. It is possible to use GDB with xv6. To do this instead of running make qemu or make qemu-nox, run make qemu-gdb or make qemu-nox-gdb respectively. Then, in another window, run gdb from the xv6 directory, and then run the command source .gdbinit within GDB. Then, within GDB, you can set breakpoints with break function_name (and various similar commands) and start execution of the OS with continue.

a note on OS X

Because xv6 expects ELF format binaries, xv6 will require a cross-compiler on OS X. We recommend (and will support) using a Linux environment instead (such as through a VM) instead, but if you can get it working natively on OS X using a cross-compiler, that’s great (and your fellow students would probably appreciate the information).

Credit

This assignment is based loosely on Arpaci-Dusseau’s initial-xv6 assignment.