CS45 Lab 2: Implementing System Calls in Linux

Due Sunday Feb 16 before 11:59pm

This lab will be done with your forever CS45 lab partner:
Lab partners and machine assignments

The lab 2 version of: virtual box guide for lab 2


For this project you will add two new system call to the linux kernel and write user-level programs that test your system calls:
  1. The getcurrenttime system call returns the system time value.
  2. The getprocinfo returns information about a running process specified by an argument value.
The goals of this lab are:
  1. To better understand system calls by implementing them in linux and testing them at user-level.
  2. To learn how to read linux kernel code.
  3. To learn some system resources for testing and verifying the correctness of your system calls.
  4. To learn about Linux's PCB data structure (task_struct)
  5. To get some practice thinking about concurrency.
  6. To learn how to use VirtualBox, and how to compile and install modified linux image and header packages.
  7. To learn how to demonstrate the correctness of your solution.


This is the first of several lab projects that involve modifying the Linux kernel. You will use VirtualBox to run and test your modified Linux kernel.

For these projects, you will do your lab work on one specific CS lab machine, and you and your partner will do your development work using a new jointly shared CS account. This account is to be used for CS45 work only (we will remove them at the end of the semester).

You and your partner will log into the machine tow which you have been assigned using your new CS account. It is important for correct networking between the host machine and the virtualbox VM (Virtual Machine), that you do not run on the same physical machine as any other group.

You can ssh into your machine from any other machine in our labs; you don't need physical access to your machine to run VirtualBox on it.

Instructions for setting up your virtualbox VM, compiling, installing, and running Linux kernel code, and ssh'ing and scp'ing between VM and the host machine are available in the: Virtual Box Guide for CS45 Students.

Getting Started
Start by setting up a git repo for your lab2 work. Make sure to add three users to your git repo: you, your partner, and your shared CS45 account.

Both you and your partner should then clone a copy into your private cs45/labs subdirectory. here are the instructions for doing this from lab1.

Then one of you or your partner can copy over some starting point code into your repo, add and push it. I included an example gitignore file with the starting point code. First move it to .gitignore before adding and pushing it (and add in any names of executable files or other files you do not want git to add to your repo).

$ cp ~newhall/public/cs45/lab02/* .
$ ls
  Makefile  README syscall_impl_startingpt.c tester.c
$ mv gitignore .gitignore
$ git add .gitignore
$ git add *
$ git commit
$ git push origin master 
The starting point code contains a starting point for a user-level test program that you can use to test one or both of your system calls: feel free to write multiple test programs, this is just one, incomplete, example that includes some header files that may be useful. Also, note the comment in the Makefile about setting the -I include directory path so that it can find the header files for your version of the kernel. I've made a guess at what you may name it, but make sure it is correct for what you actually do name it.

You may need to add additional #includes to your test program to include any header files you add to the kernel that contain definitions that are needed by user-level programs. For example, if you define a new struct type and a user-level program passes one by reference to a system call, the struct type needs to be defined in a .h file that the user-level program includes.

Implementation Details
For all labs in this course, I strongly suggest you and your partner work together on all (or almost all) parts. For this lab I suggest first implementing the getcurrenttime system call together and after it is debugged and working, then implement the getprocinfo system call together.

System call 1: getcurrenttime

You will add your new system call in a separate file in the kernel. The getcurrenttime system call returns the value of the kernel xtime variable to the user-level program that calls it. It should take 2 argument values, the first is a flag that if non-zero, will print out the time value at kernel-level. The second, is a struct timespec that is passed by value to getcurrenttime. getcurrenttime fills in the fields of the pasted struct to "return" the xtime value to the caller. The function prototype for this system call (what it would look like to the user-level program calling it) would the following:
 *  flag: if non-zero print out the time at kernel level
 *  tval: passed by reference to system call, the current time
 *        will be "returned" through this parameter
 *  returns: 0 on success, -1 on error 
long getcurrenttime(int flag, struct timespec  *tval)
NOTE: as you modify existing kernel modules, I recommend first making a copy of of the .c or .h or .s file as orig_filename.c, then modify filename.c. This way if you really break something you can easily go back to the original kernel source. In the worst case you can always grab a new copy of the linux source, but it is nice to not have to do that every time you really mess up one file and what to start over.

To get some ideas of how to implement the system call function, look at other example code in kernel/ that access xtime, and look at the linux/time.h header file for interface functions and type definitions.

Steps for implementing a system call:

There are three general steps to implementing a system call:

  1. Adding a new system call number to the kernel.
  2. Adding an entry to the kernel's system call table, associated with the system call number, that specifies which kernel function to invoke when there is a trap with this system call number. For the architecture we are building, the system call table is automatically generated from information in other kernel files. However, this is not true for every architecture (this is an architecture-specific part of the kernel).
  3. Adding code to the kernel that implements the system call functionality. This may include adding header files for new type definitions used by the interface to your system call. The implementation may require copying system call arguments from user to kernel space on entry, and then copying them back from kernel space to user space before returning from the system call.

Details: In this example, I'm adding a new system call named getsecretcode that takes has one int parameter and one int passed by reference that the kernel fills with a secret code. It returns 0 on success and an error value otherwise.

  1. Adding a new system call number to the kernel
  2. Adding an entry to the system call table

    Changes for 64-bit x86 (this is our platform):

    • in linux- add a new entry:
        #define __NR_getsecretcode                             299
        __SYSCALL(__NR_getsecretcode, sys_getsecretcode)
      If you look in the file linux-, you can see how the 64-bit system call table is generated. The change that you make in unistd_64.h will result in header files being added to include/asm-generic and in include/asm-x86/ with your new system call information in it.

    Changes for 32-bit x86. You DO NOT need to make these changes for building on our system; this is just to show you that this part of the kernel is architecture-specific, and to port your system call to other platforms you would need to modify all the architecture-specific files:

    • in arch/x86/kernel/syscall_table_32.S add a new entry:
          .long sys_rt_tgsigqueueinfo     /* 335 */
          .long sys_perf_event_open
          .long sys_getsecretcode
    • in linux- add:
          #define __NR_perf_event_open    337
          #define NR_syscalls 338
  3. Implement your system call: add source and header files, and add function prototypes for it

    1. at the bottom of linux- add a function prototype for your system call:
        #ifndef sys_getsecretcode
        asmlinkage long sys_getsecretcode(int flag1, int flag2);
    2. if you have new type definitions that are needed at user-level, add a new header file in linux- (e.g. getsecretcode.h). We don't need to do this for the getsecretcode system call (and you don't for getcurrenttime).

      When adding new header files to the kernel, make sure to use good .h file boilerplate code, also you can specify parts of the header file that are only visible to code at the kernel level by putting those parts between #ifdef __KERNEL__ and #endif:

       * file: getsecretcode.h with really good comments
      #ifndef __GET_SECRET_CODE_H__
      #define __GET_SECRET_CODE_H__
      // add any type definitions etc. that may be needed at user-level here
      // look at other .h files in here to see some examples:
      #define MYSECRETCODE_MAX  1234567   // kernel and user level can use 
      #ifdef __KERNEL__
      // anything between "#ifdef __KERNEL__"  and "#endif"  
      // is only visable at kernel-level
      #define MYSECRETCODE_ANSWER 13     // only available at kernel level
      #endif   // #ifndef __GET_SECRET_CODE_H__
    3. add a new file to the kernel that contains the implementation of your system call (e.g. getsecretcode.c). You should add the file in linux-source- Your system call function must have "asmlinkage" prepended to its header and a "sys_" prefix to its name. However, use the SYSCALL_DEFINEX macros to generate this for you:
      SYSCALL_DEFINE2(getsecretcode, int , pflag, int __user *, codeval) {
      See the macro definitions for SYSCALL_DEFINEX in include/linux/syscall.h. These generate function definitions for system calls with different numbers of parameters. The above will generate something like the following: For example:
      asmlinkage long sys_getsecretcode(int pflag, int __user *codeval)
      // asmlinkage: tells gcc to pass parameters on the stack
      // __user: means the address value is from user-space 
      Here is an example:
       * file: getsecretcode.c : with a very awesome comment
      #include <linux/kernel.h>   // can only be included at kernel-level
      #include <asm/uaccess.h>    
      #include <linux/syscalls.h> 
      // you also may need to include other header files based on what 
      // types and functions your system call implementation needs to use
      // (linux/time.h might be useful for getcurrenttime)
      // if you added a .h file then, you may want to use some of its defs
      // in your system call implementation, so make sure to include it:
      #include <linux/getsecretcode.h> 
      /* getsecretcode system call: 
       *    an awesome comment 
      SYSCALL_DEFINE2(getsecretcode, int , pflag, int __user *, codeval) {
         /* to print a debug message use printk, which is similar 
          * to printf: the kernel's stdout goes to the console and
          * to the files: /var/log/[syslog/kern.log]
         printk("Inside system call getsecretcode\n");
         /* copy argument values that are passed by reference from user 
          * space to to kernel space:
          * (1) first call access_ok() to check if the space
          *     pointed to by thetime is valid
          * (2) then call copy_from_user() to copy to kernel space
          * note: pflag's value is passed on the stack so it does not need to be 
          *       copied to kernel space...its value can be grabbed off the stack
         /* If you access any kernel variables that could be
          * modified by interrupt handlers that interrupt our syscall,
          * or by other processes simultaneously running in kernel mode,
          * then you need to put some synchronization around their access.
          * For existing kernel objects, there should be a lock or semaphore 
          * you can use (in inlcude/linux there are interface files to 
          * spinlock, seqlock, and semaphore)  For new kernel state you add, you 
          * need to add spinlock or semaphores (this is likely not necessary 
          * for lab2 where you are accessing existing kernel state)
         /* copy pass by reference "return" values to user-space
          *  (1) first call access_ok() to check if the space
          *      pointed to by codeval is valid (if you have not already done so)
          *  (2) then call copy_to_user() to copy the value to user space
          *  You CANNOT directly access the space pointed to by codeval.
          *  Instead you need to copy values to/from kernel space to the
          *  space pointed to by the pass-by-reference parameters.
          /* a successful return from the system call */
          return 0;
    4. Modify linux-source- to include your new file. Add a line like this:
      obj-y = ...
              async.o  getsecretcode.o   # add to the end of this list
      obj-y += groups.o 
      Then, re-build the linux kernel.

Testing your system call

Specific implementation hints:

System call 2: getprocinfo

After you have the getcurrenttime system call implemented and tested, next implement a system call named getprocinfo. It takes two arguments: a process identifier value; and a reference to a process information struct that you need to define in a new header file. getcurrenttime fills in the field values of the second argument with information about the specified process. If the pid argument is 0, then getprocinfo should "return" information about the calling process, otherwise it should "return" information about the process with a matching pid value. The system call returns 0 on success, and one of the following error values otherwise (feel free to add additional error return values): (ESRCH, EINVAL, ... , are defined in linux/errno.h). Take a look at how other system calls return error values to figure out how your code should do this.

Start by defining a the proc_info_struct in a new header file that you create in include/linux/. The struct should have the following fields:

  pid_t pid;                /* pid of process */
  pid_t parn_pid;           /* pid of its parent process */
  pid_t gid;                 /* group id */
  unsigned long user_time;  /* total CPU time in user mode*/
  unsigned long sys_time;   /* total CPU time in system mode*/
  long state;               /* its current state */
  unsigned long long sched_avg_running; /* its scheduled ave running time */
  unsigned int time_slice;   /* its scheduling time slice */
  unsigned int policy;       /* its scheduling policy */
  unsigned long num_cxs;     /* number of context switches it has had  
                                (sum of voluntary and involuntary cxs) */
  int num_children;          /* the number of child processes it has */
  char prog[16];             /* its exec'ed file name (e.g. a.out) */
You can fill in these values by accessing a process' task_struct that is defined in include/linux/sched.h. Field values in the proc_info_struct that correspond to null pointer values in the process' task_struct should be set to -1. Not all field values in your struct match the names of fields in the task_struct, so you may need to read through some code and/or try some things out before you get the right values for these fields. Additionally, some values may need to be obtain indirectly via task_struct fields. Types are defined in types.h files in architecture neutral and architecture specific subdirectories of include. For example: linux-source- Errors are defined in linux-source-

When dealing with pointer parameters, you need to make sure that your system call doesn't dereference bad addresses: the kernel should never crash when the user passing in a bad address value to a system call. Also, remember that you need to explicitly copy values passed-by-reference to/from kernel space from/to user space. Use the functions access_ok, copy_from_user, and copy_to_user. Look at other system calls to see how these are used. A few things to help you determine what to do and if your system call returns correct information:

What to Handin
Submit the following using cs45handin Only one of you or your partner should submit your tar file via cs45handin. If you accidentally both submit it, send me email right away letting me know which of the two solutions I should keep and which I should discard. You can run cs45handin as many times as you like, and only the most recent submission will be recorded:
  1. A README file with the following information:
    1. You and your partner's names
    2. The total number of late days that you have used so far
    3. A list of the linux source files you modified (list the complete path name of each file)
    4. The location of the .deb packages for your kernel solution. Include the machine and path to the .deb files. For example:
      DO NOT MODIFY THESE AFTER THE DUE DATE. If you want to rebuild your kernel after the due date, use a brand new --append-to-version flag so that it will not overwrite these files.
    5. A list of any features that you do not have fully implemented

  2. Copies of every kernel source and header files that you modified or added for this lab. I only want the files you modified or added, do not submit a tar file containing the entire kernel source tree.

  3. Your user-level test program(s), and Makefile. Make sure that you submit at least one test program that contains multiple calls to your new system call that demonstrates how your system call handles valid and invalid input.

Demo and Preparing for Demo
You and your partner will sign up for a 15 minute time slot where you will run your test program for me and show me that it works. A demo sign-up sheet will be outside my office door.

During your 15 minute demo slot you and your partner will demonstrate that your solution works. It is up to you to determine how to demonstrate this to me. When I meet with you, your kernel should be up and running on VirtualBox (unless there is something that happens during the boot process that you want to show me), and your demo should be ready to run; if you spend your entire demo slot setting up VirtualBox, then I can only conclude that your solution does not work.

A demo is something that you and your partner should practice before you give it; you want to make sure that it runs correctly and that it demonstrates that your solution is correct and complete. Make sure that you are demo'ing both how your system call works under normal conditions and how you are handling error conditions.

You will want to show me one thing at a time, and discuss what is happening after each thing. As a result, do not just run a big script of commands and show me the output. Instead, have a script of commands (or tests), but run them one and a time and think about what they show and how you can show me what they show. Also, be prepared to answer questions during your demo about your implementation and about your test programs.

Often times demonstrating that your solution works means that you will need a way to run a version of your kernel with debugging output enabled, and you may need to show via unix commands or /proc information that your system calls obtain the correct information or do the right thing. In addition, for most demos, you will want to write one or more interactive demo applications (menu driven program), where you choose from a menu of options for invoking your system call(s), execute a system call, examine system state or kernel output to verify that it did the right thing, then choose the next system call to execute, and so on; you want to be prepared to discuss and to demonstrate the effects after any single system call.

Links to Useful Resources