CSE 422S: Studio 4

Linux System Calls


`What does it mean by speak, friend, and enter?' asked Merry.

'That is plain enough,' said Gimli. `If you are a friend, speak the password, and the doors will open, and you can enter.'

The Fellowship of the Ring, Book 2, Chapter 4

System calls are the fundamental, most stable interface that is provided by the operating system. They are how user programs request the vast majority of kernel actions: creating, reading, and destroying files; allocating and freeing dynamic memory; executing new programs, etc.

In this studio, you will:

  1. Make system calls with the libc wrapper
  2. Make system calls with the native Linux interface
  3. Write your own system calls

  4. Optionally, explore an advanced benchmarking method, writing a system call to access the Raspberry Pi's cycle count register.

Please complete the required exercises below, as well as any optional enrichment exercises that you wish to complete. We encourage you to please work in groups of 2 or 3 people on each studio (and the groups are allowed to change from studio to studio) though if you would prefer to complete any studio by yourself that is allowed.

As you work through these exercises, please record your answers, and when you finish them each and every person who worked on them should please log into Canvas, select this course in this semester, and then upload a file containing them and also upload any other files the assignment asks for, and submit those files for this studio assignment (there should be a separate submission from each person who worked together on them).

Make sure that the name of each person who worked on these exercises is listed in the first answer, and make sure you number each of your responses so it is easy to match your responses with each exercise.


Required Exercises

  1. As the answer to the first exercise, list the names of the people who worked together on this studio.

  2. First we're going to make a system call through the libc wrapper. This is the standard way that user programs make calls to the kernel. You can find a complete list of system calls in the manual pages with the command man 2 syscalls.

    Boot up your Raspberry Pi, open up a terminal window, and outside of the linux source directory in which you are keeping built versions of the Linux kernel make a new directory for your userspace programs (you will use sftp to transfer files from shell.cec.wustl.edu into this directory).

    Log into shell.cec.wustl.edu, and (outside the linux source directory if you have put a copy of it there as well) also create a new directory for your user space programs. In that directory create a new file called lib_call.c. In Linux, all users have an associated user ID number, and the Linux library functions getuid and setuid can be used to get and set that id, respectively. In that file, write a short C program that reads the user ID and prints it out, attempts to set it to 0 (the root uid) and indicates whether or not that attempt was successful, and then again reads and prints out the user ID.

    Read the man 2 getuid and man 2 setuid man pages to understand (1) what syntax is used to call those functions and what types of values they return, and (2) what header files you need to include in order for your program to compile.

    Since the call to setuid may fail, please do proper unix style error checking: store the return value from setuid into a variable, and follow it up with a test like:

    if( return_val != 0 ) printf("Error: setuid failed! Reason: %s\n", strerror(errno));

    Again, you can use the appropriate (section 3) man pages to determine which header files to include for those additional library functions: man 3 printf (not man printf or man 1 printf), man 3 strerror, and man 3 errno (not man errno or man 1 errno).

    Save your program, and try to compile it on shell.cec.wustl.edu, as in:

    gcc lib_call.c -o lib_call

    which if successful will produce a binary file called lib_call that you can run in a terminal window. Fix any coding mistakes (or missing header files, etc.) in your program, until it compiles successfully with no errors or warnings, and then run your program on shell.cec.wustl.edu, as in:

    ./lib_call

    As the answer to this exercise, copy and paste the output from running your program on shell.cec.wustl.edu.

  3. From the userspace programs directory terminal window on your Pi, sftp into shell.cec.wustl.edu, navigate to the appropriate directory, and get the lib_call.c file. Quit sftp and compile and run your program. As the answer to this exercise, please copy and paste the output of running the program on your Raspberry Pi, along with a brief description of any differences you noticed in the output of the program (or the compilation, etc.) on your Pi vs. on shell.cec.wustl.edu.

  4. Now we're going to modify your program to use the native Linux syscall interface as documented in man 2 syscall (singular - this is a different page than man 2 syscalls - plural). You are strongly encouraged to continue to develop and store your code on shell.cec.wustl.edu so that if your Pi freezes up you don't lose your work, and then when it's time to move things over to the Pi, use sftp to do so.

    Copy your lib_call.c file into a new file called native_call.c, and in that new file replace the calls to getuid and setuid with calls to syscall. To do so, you will have to determine their ARM architecture specific system call numbers by looking at the linux source file arch/arm/include/uapi/asm/unistd.h and at the generated header file arch/arm/include/generated/uapi/asm/unistd-common.h in your kernel source directory. Note that it is good programming practice, and also makes your code more portable (e.g., between shell.cec.wustl.edu and your Raspberry Pi) if you use the manifest constants for them (e.g., __NR_getuid and __NR_setuid) instead of hard-coding the numbers for them directly in the code you write. To do that, you may need include the header file asm/unistd.h in your program.

    Compile your program, fixing any problems and recompiling as needed, and run it on your Raspberry Pi. As the answer to this exercise, copy and paste the output from running your new (native call) program on your Raspberry Pi.

  5. Now we will begin the process of writing two new system calls into the Linux kernel, each of which will print a distinct message into the kernel log.

    Note:When making changes to the linux source code, add a comment like //CSE422 MMDDYY before each section (where MMDDYY is the month, day, and year on which the change is being made). This, along with generating file diffs, will make it easier for you to keep track of the changes made to the kernel, and to revert them if needed.

    There are five distinct tasks we need to accomplish to do this:

    1. Declare a C function prototype inside the kernel for each new syscall
    2. Write a C function implementation for each new syscall
    3. Define a new system call number for each new syscall
    4. Update the ARM architecture system call table with each new syscall
    5. Update the total number of syscalls value that stored by the kernel

    First, ssh into shell.cec.wustl.edu using your WUSTL Key id and password, then log into the Linux Lab cluster by running qlogin -q all.q Then, navigate into your linux source directory, (which should be in /project/scratch01/compile/your-username), then into the linux directory under it (or whatever you renamed it to, from the long name produced by the linux .tar.gz file) then into include and then into the linux directory Before modifying any files, at least make a backup of any file you will be changing, as in:

    cp syscalls.h syscalls.h.MMDDYY

    where MMDDYY is the current month, day, and year (or, use svn or git or another version control system, and commit whenever you have a working version).

    Declare two new function prototypes, one that take no arguments and one that takes a single int argument, at the bottom of include/linux/syscalls.h (make sure to put them before the closing #endif). You can use the prototype for sys_getuid as a template for doing this. Make sure that you use void in the argument list to indicate no arguments, since in some versions of C a function declared f(void) (which takes no arguments) is not the same as a function declared f() (which inidicates that the function may take any number of parameters of unknown type).

    As the answer to this exercise, show the two function prototypes you've added.

  6. Second, create a new file for each new syscall's implementation, in arch/arm/kernel/. The naming convention for a file that only implements a system call is to call the file by the syscall name, e.g., if our function that takes no arguments is called sys_noargs, you would create the file sys_noargs.c (additionally, there are other places we could have put this file, but since we're only adding this call for the (ARM architecture) Raspberry Pi, this is an appropriate place to put this code). Make a second file for your second system call in the same directory, using the same naming convention.

    For the implementation of the function that takes no arguments, copy and paste the contents of the file found here. Take a moment to look through this file. Notice that the function declaration isn't a normal declaration, but actually is made through the macro SYSCALL_DEFINE0 (the zero in the name of the macro comes from the fact that this syscall takes zero arguments, and is defined in include/linux/syscalls.h).

    For the implementation of your second syscall, use the code in sys_noargs.c as a template. You'll need to change the SYSCALL_DEFINE0 macro to reference the function name you came up with, as well as the fact that this function accepts an integer parameter. You do this by passing the type and the name of the parameter to the macro, as in:

    SYSCALL_DEFINE1( your_name, int, param_name )

    In the body of this syscall, use the kernel function printk to print a message that contains the value of the parameter that was passed to it. You can use printk much as you would use printf. Be sure to return a proper return value from this function.

    As the answer to this exercise, show your implementation of the second function.

  7. We now need to make sure the two new files are included in the Linux build process. The kernel has a robust build system, so this is actually pretty easy. Edit the file named Makefile (after, of course first making a copy of it named Makefile.MMDDYY) in the same directory as your source code files (which should be arch/arm/kernel/) and add our two new files to the end of the object file list, which starts on the line with obj-y (make sure you do not add your files after a \ character, as this specifies the start of a new line). Then, change the file extensions for your files from .c to .o (this implicitly tells the build system to generate the object (.o) file it needs by compiling the source (.c) file that has the same name except for the file extension). Be sure to add only the names of the files, with space characters in between them - as you may guess from the instructions above about where to put the names, formatting rules for how a Makefile is laid out can be a bit picky.

    As the answer to this exercise, show the newly modified line of the Makefile, that lists the new .o files to generate.

  8. The implementation of your system calls is almost done, but now we need to add them into the kernel's system call interface. To do this, you will modify arch/arm/tools/syscall.tbl to tell the kernel to install our new system calls into the hardware's system call table.

    Before modifying this file, make a copy of it called syscall.tbl.MMDDYY. Then, at the bottom of the original syscall.tbl. file, create two new lines, following the scheme used throughout the file. You will need to increment the value in the first column to allocate unique numbers for each of your new system calls. Furthermore, note the format of the last two columns: the system call name, followed by the actual function definition, which by convention is the name prefixed with sys_.

    As the answer to this exercise, show the changes you added to the syscall.tbl file.

  9. At this point the kernel is fully modified and can be recompiled. Because the recompiling process may take several minutes, if you would like to complete the enrichment exercise to implement a system call that reads from the cycle count register, you may want to complete that now, before you recompile the kernel and install it on the Raspberry Pi. It's not wise to try and rebuild only part of the kernel, so in the base linux source directory (the one with subdirectories arch, init, drivers, etc.) you should first issue the command make clean to remove the artifacts from your previous build.

    Then, to differentiate your new kernel from the one you built in the last studio, we'll modify the kernel LOCALVERSION. Rather than using the menuconfig interface like we did last time, this time we will modify the configuration directly. In the base directory, edit the file called .config (the leading period means that this is a hidden file that is not normally displayed) and modify the CONFIG_LOCALVERSION string to reflect the fact that this new kernel implements the extensions from this syscall studio.

    You should now build and install your new kernel following the steps outlined in studio 1 (starting with step 6, after you had already unpacked the Linux distribution, set up the configuration, etc.) and studio 2 (starting with step 7 after you had already successfully set up and booted into your Raspberry Pi). Once your new kernel is booted and running on your Raspberry Pi, open up a terminal window and run the command uname -a to confirm the new version is running (you should see the new LOCALVERSION string you used). As the answer to this exercise please give the output that was produced by running uname -a on your Pi.

  10. You are now ready invoke your new system calls.

    First, use sftp to retrieve arch/arm/include/generated/uapi/asm/unistd-common.h from your kernel source directory. Open this file on your Raspberry Pi. You should notice two new #define __NR_ statements at the end of the file corresponding to your new system calls.

    Next, copy this file to the appropriate include directory for GCC:

    sudo cp unistd-common.h /usr/include/arm-linux-gnueabihf/asm

    Now, copy native_call.c to a new file called new_call.c, and in that new file replace the native calls that invoke the getuid and setuid syscalls with the appropriate native calls to the new functions you wrote (i.e., replace the syscall number for getuid with the __NR_ manifest constant for the new syscall that takes no arguments and replace the syscall number for setuid with the one for the new syscall you added that takes one argument). Please note that you probably will need to declare and initialize an int variable in your new_call.c code to pass to your syscall that takes one argument - the syscall interface may not correctly invoke your implementation of that syscall if you just pass in a (constant) numeric value.

    On your Raspberry Pi, compile and run this new program, and after running it check the system log by issuing the command dmesg. As the answer to this exercise, copy and paste the lines of the system message log that show the messages that were output to it by your new syscall functions.

  11. Things to Turn In:

    You will need to submit files with the differences ("diffs") for the kernel source code files you modified. To create a diff, use the diff program with the backup versions of the source code you modified.

    For example: diff -up unistd.h unistd.h.MMDDYY > unistd.h.diff

    For this studio, you will submit copies of the following files:

    If you didn't keep an original copy for any of these files, you can find them in the original source code package.


    Optional Enrichment Exercises

    As the answer to any of these optional exercies that you would like to try, please briefly describe what you learned (and give answers to any questions the exercise contains).

  12. In addition to the two system call invocation interfaces you used above (the libc wrapper and the native syscall function), implement a third interface that directly utilizes the assembly routines necessary to invoke a system call. See the slides from today's lecture to see the steps that need to be taken to invoke a system call on ARM. You will need to make use of the gcc inline assembly function via the GNU asm extension.

    If you want to know more about the machine-level actions that occur during a syscall, you can start with the detailed explanation given in the manual to the native system call interface, man 2 syscall.

    The kernel code that contains the entry point for system calls is found in arch/arm/kernel/entry-common.S.

  13. We will later discuss several methods that Linux provides for timekeeping and benchmarking program execution. One method, which is not specific to the Linux kernel, but is instead provided directly by the Raspberry Pi hardware platform, involves reading from the Cycle Count Register (CCNT) on the Raspberry Pi's ARM CPU. This register is not accessible directly by user-space programs. However, since it is accessible by the kernel, we can write a system call that allows a program to request the value from the kernel.

    First, you will need to download a header that defines architecture-specific functions for interracting with the Raspberry Pi's performance monitors. Please download this file, and place it in the arch/arm/include/asm directory in your Linux kernel source tree.

    As you did in Step (5) above, modify your include/linux/syscalls.h header to declare another function prototype, this time taking a single unsigned long long * argument. We named the function sys_read_ccnt.

    Like in Step (6), create a new file for your syscall's implementation in arch/arm/kernel. You may use the provided file sys_read_ccnt.c.

    Similar to Step (7), add your new file to the end of the object file list in arch/arm/kernel/Makefile.

    As in Step (8), add your new system call to arch/arm/tools/syscall.tbl.

    Proceed with Exercise (9) above to compile your kernel with your new system calls, and install the kernel on your Raspberry Pi.

    Proceed with Exercise (10) to write a program that implements your new system calls. In particular, call your system call twice to read two cycle counts in a row. As the answer to this exercise, please say how many cycles it takes to run the system call.

  14. The read_ccnt system call that you just implemented is unsafe. It allows the caller to pass a pointer to any address, to which the system call will then write the current cycle count. Because an arbitrary address may be passed, including one that may not be writable by the userspace process itself, it provides a means for circumventing the kernel's memory protection mechanisms, which can lead to memory corruption.

    Modify the system call to avoid this issue. You will need to use the copy_to_user() function described on page 76 of Robert Love's Linux Kernel Development.

    Have the system call return the -EFAULT error code if the copy fails.

    As the answer to this exercise, please provide the code for your new implementation of the system call.


Page updated Sunday October 11, 2020, by Marion Sudvarg and Chris Gill.