Handy References:
Week 1 Lab Audio
Week 2 Lab Audio
Lab 2 Goals:
- Gain an understanding of the OS side of system calls and practice writing your own system calls.
- Learn to read the Linux kernel's source code (a little!) and interact with important data structures (e.g., task_struct).
- Produce userspace test cases to evaluate your kernel code implementation.
- Practice building and deploying the Linux kernel.
Overview
For this lab, you'll be adding two new system calls to the Linux kernel. The first will retrieve the current time and return it by copying memory into the calling userspace process. The second will modify the PCB (in Linux, the task_struct) of a chosen process so that it no longer appears in the process list.
Requirements
- System call 1: getcurrenttime
You should implement a getcurrenttime system call that takes two parameters: a flag that specifies whether or not to print when executing and a pointer to a struct timespec. Your implementation should print a simple debugging status message using printk() if the flag evaluates to true. It should then verify that the pointer given in the timespec is valid, and if so, lookup the current time and copy it into the pointer's destination.
To a userspace application, the call might look like:
/*
* pflag: if non-zero print inside the system call (using printk)
* tspec: pointer passed to your system call, the current time
* will be "returned" through this parameter
* returns: 0 on success, -1 on error
*/
long getcurrenttime(int pflag, struct timespec *tspec)
On success, your system call should return 0. If it fails, it should return an error code that describes what went wrong. You are responsible for detecting and reporting the following errors:
- Verifying that the struct timespec pointer is writable userspace memory using access_ok. If this fails, you should return the constant -EINVAL (invalid argument).
- Copying the time value to the userspace timespec struct using copy_to_user. If this fails, you should return the constant -EFAULT (bad address).
Note that in userspace, you will not see the error value directly in the return value of your system call. Instead, you'll see -1 and the errno variable will be set to the value your system call returned. This behavior will allow you to use perror() to decode the error message.
The Linux kernel has many functions for dealing with time. A good place to start looking at the available functions is in kernel/time/, which has files like timekeeping.c and time.c. The corresponding headers, which you'll need to include in your system call's .c file to use the time functionality, are in include/linux. The ktime.h header seems to include most of the other headers that may be of interest.
- System call 2: stealth
The ps ax command will print a list of all processes on the system. You can use grep to help narrow the list (e.g., ps ax | grep vim to show only processes with 'vim' in the name). The leftmost column of the output is the process id (PID), and the rightmost column is the process's name. In Linux, ps reads the information about all processes from a special pseudo-file system in /proc. If you execute ls /proc, you'll see lots of directories named with a number, each of which corresponds to a PID. The files in those directories contain information about the corresponding processes. Note that these files are not stored permanently on any disk. The info is all stored in the OS's data structures, and the /proc file system is simply the interface it uses to make that information available to users.
You should implement a stealth system call that will hide (or unhide, if called a second time) a process from being listed in the /proc file system. By hiding the process in /proc, it will no longer show up in ps's output list.
To a userspace application, the call might look like:
/*
* pid: the process id of the process whose stealth status should be toggled
* returns: 0 on success, -1 on error
*/
long stealth(pid_t pid)
On success, your system call should return 0. If it fails, it should return an error code that describes what went wrong. You are responsible for detecting and reporting the following errors:
- Verifying that the provided PID matches a real process by looking for the corresponding process's task_struct. If this fails, you should return the constant -ESRCH (no such process).
To implement your stealth system call, you'll need to add a flag representing a process's stealth status to the Linux task_struct, which can be found on line 1390 of include/linux/sched.h. You'll also need to update the INIT_TASK macro in include/linux/init_task.h to initialize your new flag when a new task_struct gets created. Your stealth system call should toggle this flag's value. When accessing instances of the task_struct, you need to be holding locks. I would suggest looking around at other places where task_structs are used to see how the locking is used.
You will also need to edit the implementation of the /proc file system so that it skips over any processes whose stealth flag is set. The /proc implementation lives in the fs/proc directory, with fs/proc/root.c being the "main" file (this is unlikely to be the file where you make your changes, but it should help you to get started with discovering how the file system works).
- Userspace test applications
Having implemented your system calls, you need to test them! You should write two small userspace test applications, one for each of your two system calls, to test that each works as intended. Your test programs should attempt to test as many cases as possible (e.g., calls that will generate errors in addition to correct runs).
In the kernel, each system call is given a unique integer, and you'll need to know the integer assigned to your system calls (see "Defining System Calls" below). To invoke your new system call, you can use the syscall() function. The first parameter is the system call number, and then you can pass as many additional arguments as your system call needs. For example, to call getcurrenttime(), you could invoke:
int pflag = 1;
struct timespec ts;
int result = syscall(326, pflag, &ts); // Assumes 326 is the number you assigned to getcurrenttime()
Since the above method of making system calls is ugly, I would suggest defining a simple macro for each call to give it a more reasonable name:
#define getcurrenttime(arg1, arg2) syscall(326, arg1, arg2)
/* Now you can call getcurrenttime() normally: */
int result = getcurrenttime(pflag, &ts);
Checkpoint
For the checkpoint, you should be able to demonstrate that:
- You have added a system call to the Linux kernel.
- You can invoke your system call using a small userspace test program.
- You have made non-trivial progress implementing one of the two required system calls outlined above. Your job is to convince me that you've worked on it. I don't have any particular milestone that I'm looking for...
Defining System Calls
To add a new system call, you'll need to define it in a few places:
- Add a new system call number to the kernel.
The Linux kernel associates a unique integer with each system call, so you'll need to assign a new value for each of your two system calls. For the x86 architecture, the values are stored in a table in the file arch/x86/entry/syscalls/syscall_64.tbl. You can use any integer that isn't already in use (the next free value is 326). You will only be operating your kernel in 64-bit mode, so the 32-bit/64-bit distinction doesn't have much impact. You should follow the same naming format for the other columns (e.g., name the system call normally and then again with sys_ prepended to the front of it).
- Add your system call prototype to the syscalls header file.
Next, you'll need to declare your system call in the file include/linux/syscalls.h. The function names should be prefixed with sys_, and they should return the same type as all the others (asmlinkage long). For pointers coming from userspace in parameters (e.g., the struct timespec in getcurrenttime), you need to add "__user" to the type declaration. Take a look at the other system calls for an example.
- Add the code for your system call.
Finally, you'll need to implement your system call! You should add a new .c file to the kernel directory (e.g., kernel/stealth.c). In that file, you'll need to #include kernel headers to get access to helper functions, like printk(). I would suggest always including:
#include <linux/errno.h> // For error constants.
#include <linux/kernel.h> // For printk().
#include <linux/syscalls.h> // For syscall macros.
Each of your system calls may also need other headers that are specific to their purpose (e.g., getcurrenttime will need asm/uaccess.h for the access_ok and copy_to_user functions).
You can define your system call using the SYSCALL_DEFINEX macro, where X is the number of parameters your system call takes. For example, suppose you were adding a system call named testcall that takes two arguments, an integer and a pointer to a userspace string. Your definition would look like:
SYSCALL_DEFINE2(testcall, int, int_param, char __user *, string_param) {
/* Body of system call implementation goes here.*/
/* If an error occurs, return a negative constant that starts with E
* (e.g., EINVAL or ESRCH). */
if (error) {
return -EINVAL;
}
/* On success, return zero. */
return 0;
}
After implementing the system call, you need to make sure it gets compiled and used. Since you added a .c file, you'll need to tell the kernel's build system to build and incorporate the corresponding .o file. The easiest way is to add file.o to kernel/Makefile in the obj-y section at the top.
Tips
- When making changes to existing Linux source files, you may want to make a backup copy of the original file first. That way, if your changes break the build, you can easily revert to the original.
- The Linux task_struct has a field named "comm" that contains a string with the process's program name. It may be helpful to print that using printk() when you're debugging your stealth call.
- You can see the kernel's printk() output by executing the dmesg command.
Submitting
Please remove any excessive debugging output prior to submitting.
To submit your code, commit your changes locally using git
add and git commit. Then run git push while in your lab
directory.
Please ONLY submit any Linux source files that you have modified or added along with a
README.md file containing the paths of those files within the source tree. DO
NOT submit the entire Linux kernel source tree. Please add any userspace testing code to the
"userspace" directory.
For example, suppose you:
- modify include/linux/sched.h
- add a new file kernel/newfile.c
- write a userspace test program named test-feature.c
You should submit test-feature.c in the provided userspace directory. You should submit sched.h and newfile.c, along with a README.md, in the root of the repository. The README.md should contain the path of each file (relative to your kernel's base directory), e.g.:
sched.h: include/linux/sched.h
newfile.c: kernel/newfile.c