Principles of Operating Systems

CMSC 421 - Spring 2020


Adding a System Call to the Linux Kernel (Version 5.5)

Introduction

Project 1 in this course will involve adding several system calls to add interesting functionality to the Linux kernel. In preparation for this task, this guide has been developed to show you how to go about adding a simple system call to the kernel. This guide will focus on the 64-bit x86 architecture, however similar steps could be taken on other CPU architectures that are supported by the Linux kernel.

The system call added to the kernel in this guide is basically pointless, but it demonstrates the process in a relatively simple manner. All the system call developed in this guide does is print a simple "Hello World!" string to the kernel's log.


System Call Implementation

To start out, make a new directory in the root of your kernel source tree. For the purposes of this example, let's just call the directory hello. Create a new file in that directory called hello.c.

In the hello.c file created above, let's implement the system call itself. Copy/paste the following code into the source file...

#include <linux/kernel.h>
#include <linux/syscalls.h>

SYSCALL_DEFINE0(hello) {
    printk("Hello World!\n");
    return 0;
}

As mentioned before, this system call doesn't do anything particularly interesting. All it does is print the string "Hello World!" to the kernel's message log. We do not have a direct printf to use within kernel space. While it would be possible to use the write system call to print to the calling process' stdout, I'll leave that as an exercise to the reader.

There are a couple of things to notice about this system call code. First of all, you should note that it returns 0 in all cases (as a long). This notifies the calling process that the system call succeeded and that no errors occurred. In fact, any positive return value from a system call is treated as a successful return. If the system call were to indicate an error occurred, a negative number should be returned — specifically a negated value from the <errno.h> header file should be returned, indicating what type of error occurred. The system C library will automatically take that negative value and put the appropriate value in the errno variable in user-space (and return -1 from the function). Another point to notice is that the system call is defined in a slightly strange looking way. While it is possible to define the code for system calls with more standard-looking declarations, due to various changes added to the kernel source code for working around the Meltdown and Spectre vulnerabilities, it is better to do things this way now. Plus, this type of declaration helps to ensure that the system call can be used on other architectures than just x86-64, where system calls may be annotated in some special way. The SYSCALL_DEFINE0 part is actually a macro that expands out to make the declaration of the function. It will define the function to return a long, which is used to return any errors the function may encounter and it will also be declared to have 0 arguments. There are similar macros like SYSCALL_DEFINE1 that annotate that the function will take one argument (and so on for other numbers of arguments too).

After creating the system call, we must create a Makefile in the syscall's directory to build the code when the kernel is built. Since the kernel has a relatively nice build system, the Makefile is extremely short and simple. Copy and paste the following into a new file named Makefile in the hello directory:

obj-y := hello.o

With that, we have completed all of the "new" code that needs to be added to the kernel.


Kernel Modifications for the New System Call

To ensure that our new system call code gets compiled, we must first add in the new hello directory to the normal kernel build system. To do this, open up the root level Makefile. Look for the following section of the Makefile (which should start at line 1016 on a clean copy of the 5.5 kernel source code):

ifeq ($(KBUILD_EXTMOD),)
core-y		+= kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/

vmlinux-dirs	:= $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
		     $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
		     $(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y)))

You must modify the core-y line to add our new directory. Modify it to look like this:

core-y		+= kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ hello/

Next, open up the include/linux/syscalls.h file. We must modify this file to provide the prototype of the system call we've added. This would be needed, for instance, if we were to need to call the system call from other kernel code or system calls. It isn't strictly necessary for this particular system call, but it is still a good idea. Go to the bottom of the file and add the following right before the #endif:

asmlinkage long sys_hello(void);

Finally, we must add the new system call to the system call table. Open the arch/x86/entry/syscalls/syscall_64.tbl file, and go to the part of the file right before the beginning of the "x32-specific system call numbers" and add the following after the clone3 line (which should be line 359 of the unmodified 5.5 kernel):

436	common	hello			__x64_sys_hello

In this file, the first column represents the system call number. The second column indicates which ABI the system call is a part of. For x86-64, your options here are "common", "64", and "x32". Most of the time, you want to use the "common" option to make it so the system call will work regardless of which mode is currently in use. The third column is the descriptive name of the system call. The last column is the name of the stub function that the kernel build will generate to call your system call. This should start with __x64_ followed by the name of the actual function you've created. Every column except the second one (the ABI column) should have unique values for every system call.

If you are using a different kernel version, you may need to adjust that line slightly to account for any new/removed system calls in relation to version 5.2.11. Simply put, take the number on the line before and increment it by one to add the new system call. Make a note of the number of the system call, as you will need it later.

You have now finished modifying the kernel. All that's left now is to build and install your new kernel, reboot into it, and test it out. I'll leave the building, installing, and rebooting steps as an exercise to the reader here.


Testing Your New System Call

To test out your new system call, you must build a user-space program to call it. Create a new file (in your home directory) and copy/paste the following content into it:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <linux/kernel.h>
#include <sys/syscall.h>

#define __NR_hello 436

long hello_syscall(void) {
    return syscall(__NR_hello);
}

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

    rv = hello_syscall();

    if(rv < 0) {
        perror("Hello syscall failed");
    }
    else {
        printf("Hello syscall ran successfully, check dmesg output\n");
    }

    return 0;
}

Note that if you had a different number for your system call in the previous step (i.e, you're not using a clean copy of the 5.5 kernel source), then you will need to adjust the __NR_hello line. The only interesting part of this code is the part where the system call is called. This is done by making use of the syscall macro, provided by the C library. This function-like macro accepts an integer argument to note what system call to perform, along with any number of arguments to pass to the system call. So, if the hello system call accepted an argument (with a name, for instance), you would simply add that argument to the list passed there, after __NR_hello.

Compile the program with your system C compiler (gcc) and run it. The program will print out the success message if the syscall runs correctly, and you should see the "Hello World!" message if you look at the output running the dmesg program as root. If you get an error message from the program (which should usually say "Function not implemented"), make sure that you have made all the changes in the guide properly, built the modified kernel, installed it and rebooted into it before running the test program.

If you've gotten this far successfully, congratulations, you've written your first Linux kernel system call!

Further exploration

The system call developed in this tutorial is rather boring and doesn't really do anything of major interest. There are ways this could be improved. For instance, you could use the write system call within the kernel to write the greeting string to the user-space program's standard output. Another, more interesting modification would be to modify the system call to accept a string to greet the user by name. This would complicate the system call quite a bit, as you would have to handle proper copying of the argument into kernel-space, however it would make a very worthwhile exercise for anyone who wants to develop a non-trivial system call.


Last modified Thursday, 23-Jan-2020 15:06:47 EST