Skip to content

Project 7 - Extra Credit Project ​

This is fine

In this project, you are tasked with building your own Linux kernel with a brand new system call compiled into it, and then calling it from a user space program.

Learning Outcomes ​

  • 2.2 Explore the system call interface
  • 2.3 Show an understanding of the difference between user and kernel space

Grading Rubric ​

Make sure and review the class grading rubric so you know how your project will be graded.

Overview ​

This project is extra credit and can be used to replace a missed project or boost your grade. You will need to attend office hours to get help with the project.

Older textbooks and blog posts show how to add a system call from a loadable kernel module by patching the system call table at runtime. That does not work on a modern kernel. The system call table is no longer exported to modules and it lives in read-only memory, so the only supported way to add a system call is to compile it into the kernel itself. That is what we will do.

You will need a Linux machine that you are allowed to break. A virtual machine or a cloud instance is perfect. Do NOT use the machine that you need to finish your other classes. If you use AWS, EC2 instances built on the Nitro system have a serial console, so you can watch your kernel boot and fix problems even when the network never comes up. This is a fantastic opportunity to learn how to leverage the cloud to do some low-level debugging!

WARNING

The steps below are for Fedora 44 or newer on an x86_64 machine. Other distributions have the same tools under different package names. A full kernel build takes a long time and needs about 20GB of free disk space, so start early.

Task 1 - Setup ​

Follow the steps below to get your repository all set up and ready to use. The steps below show you how to use and set up GitHub Codespaces. You are not required to use Codespaces. All the steps below can be completed on Onyx (the CS lab machines) or on your personal machine if you prefer.

Create your repository from the template ​

The starter repository is a GitHub template, so you make your own copy of it instead of forking it.

  1. Open the starter repository: https://github.com/shanep/makefile-project-starter
  2. Click the green Use this template button and choose Create a new repository.
  3. Pick your personal GitHub account as the owner and name the repository cs452-p7.
  4. Click Create repository.

Your new repository is not a fork, so it has no upstream remote. That is on purpose: everything you need is already in your copy.

Start a new Codespace ​

We will use GitHub Codespaces to do most of our coding. Codespaces is just VS Code in the cloud. This makes it really easy to set up a developer environment and code from any computer that has a browser and internet connection! From your new repository click Code, then the Codespaces tab, then Create codespace on master.

Start Codespace

If you are asked to install recommended extensions, click "install". You may not be asked to install extensions if you are already syncing your account.

Codespace extensions

INFO

If you work on Onyx or your own machine instead, clone your repository with git clone and make sure you have gcc (or clang), make, and gcovr installed. The Codespace comes with all of these. The file docs/onyx.md in your repository has notes on using Onyx.

Get to know the starter ​

Here is what you get in the starter repository.

  • src/main.c - the main function for the executable
  • src/lab.h and src/lab.c - the library code that both the executable and the tests use
  • tests/lab-test.c - your unit tests, written with the Unity test framework
  • tests/harness/ - the Unity framework itself, don't edit these files
  • README.md - you will fill this out before you submit
  • scripts/create-submission-report.sh and .github/workflows/ - continuous integration and the submission report

The Makefile builds every C file in src/ and tests/. The test build defines TEST, and src/main.c uses that to rename its main function so it does not clash with the main in tests/lab-test.c. Keep these lines at the top of src/main.c in every project.

c
#ifdef TEST
#define main main_exclude
#endif

These are the make targets you will use the most. Run make help to see them all.

CommandWhat it does
make allBuilds all four versions of the project listed below
make checkRuns the unit tests in build/tests/myapp_t
make leakRuns the debug executable with Address Sanitizer leak checking on
make leak-testRuns the unit tests with Address Sanitizer leak checking on
make reportRuns the unit tests and creates a code coverage report in build/report
make cleanDeletes the build directory

make all creates four programs.

  • build/release/myapp - the optimized executable, compiled with all the warning flags
  • build/debug/myapp_d - the executable compiled with Address Sanitizer
  • build/tests/myapp_t - the unit tests compiled for code coverage
  • build/debug-test/myapp_td - the unit tests compiled with Address Sanitizer

If there is no src/main.c then make all skips the executable and only builds the tests. That is how the projects that are 100% unit tests work.

WARNING

make check, make leak, make leak-test, and make report run whatever is already in build/. They do not recompile your code. Run make all after every change or you will be testing old code. Also, make all prints "Builds completed" even when one of the builds failed, so scroll up and read the output.

Task 2 - Build the kernel ​

Here are the steps to build and install the kernel to make sure everything is working correctly before you change anything.

  1. Install all the required dependencies and build tools as described in the minimal requirements in the kernel documentation. These tools will be in different packages depending on your distribution. For Fedora, you can run the following command:

    bash
    sudo dnf install -y git gcc make flex bison perl bc elfutils-libelf-devel openssl openssl-devel dwarves llvm clang lld
  2. Make a new directory to clone the kernel source into

    bash
    mkdir -p ~/kernel && cd ~/kernel
  3. Clone Linus's tree from git.kernel.org. The --depth 1 flag skips the history, which saves a lot of time and disk space.

    bash
    git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
    cd linux
  4. Generate a configuration file from the kernel you are running right now. The localmodconfig target starts from your current configuration and turns off every module that is not loaded, so the build is much faster. It will ask about options that are new since your kernel was built, piping in yes "" takes the default answer for each one. The last command adds -cs452 to the name of your kernel so you can tell it apart from the stock kernel.

    bash
    yes "" | make localmodconfig
    ./scripts/config --set-str LOCALVERSION "-cs452"
  5. Build and install the kernel and its modules.

    bash
    make -j"$(nproc)"
    sudo make modules_install
    sudo make install
  6. Reboot, pick your new kernel in the boot menu if it is not the default, and confirm that you are running it.

    bash
    uname -r

    The version that is printed should contain -cs452. A kernel built from a git checkout that is not on a release tag gets a + on the end, so something like 6.18.0-rc3-cs452+ is fine. If your new kernel does not boot, reboot into the stock Fedora kernel, which is still installed, and figure out what went wrong.

Task 3 - Add a system call ​

Now that you know you can build and boot your own kernel we can add a new system call named cs452_hello. It takes an integer, prints a message to the kernel log, and returns the integer plus one.

  1. Add the system call to the bottom of kernel/sys.c. The SYSCALL_DEFINE1 macro creates a system call that takes 1 argument.

    c
    SYSCALL_DEFINE1(cs452_hello, int, number)
    {
        pr_info("cs452_hello called by pid %d with %d\n", task_pid_nr(current), number);
        return number + 1;
    }
  2. Add the prototype to include/linux/syscalls.h, next to the other sys_ prototypes.

    c
    asmlinkage long sys_cs452_hello(int number);
  3. Add your system call to the x86_64 system call table in arch/x86/entry/syscalls/syscall_64.tbl. Find the last common entry before the x32 entries that start at 512 and add a new line with the next unused number. Write that number down. You will need it in the next task. For example, if the last entry was 470 your line would look like this:

    text
    471	common	cs452_hello		sys_cs452_hello
  4. Repeat steps 5 and 6 from Task 2 to build, install, and boot your new kernel.

Task 4 - Call it from user space ​

There is no C library wrapper for a system call that you just made up, so you will call it with the generic syscall function. Clone the repository you created in Task 1 onto the machine running your kernel, delete the get_greeting function from src/lab.c and its test from tests/lab-test.c, and replace src/main.c with a program that does the following. Keep the #ifdef TEST lines at the top of src/main.c.

  • Calls syscall with your system call number and an integer
  • Prints the return value
  • Prints an error message with perror if syscall returns -1
c
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

// Change this to the number you picked in Task 3
#define SYS_cs452_hello 471

Run your program on your new kernel and then run sudo dmesg | tail to see the message that your system call printed to the kernel log. Then reboot into the stock Fedora kernel and run your program again. You should see syscall fail with the error Function not implemented (ENOSYS), because the stock kernel has no idea what your system call number means.

Task 5 - Document your work ​

Fill out the README.md in your repository and add a new section named ## Kernel Changes that includes the following.

  • The output of git diff from your kernel source directory
  • The output of uname -r on your new kernel
  • The output of your program and sudo dmesg | tail on your new kernel
  • The output of your program on the stock kernel
  • A short explanation of what happens between the call to syscall in user space and your function running in kernel space, and why the stock kernel returns ENOSYS

Final Task - Submit your code ​

Now that you have completed all the tasks, the only thing left to do is to create a submission report and upload it to Canvas so you can receive a grade for all your hard work.

Update your README ​

Open up README.md and fill in every section.

  • Your name, email, and class section at the top
  • Known Bugs or Issues - anything that does not work
  • Experience - your struggles and breakthroughs with the project
  • Analysis - only if the project asks for one, otherwise delete the section

Check your build ​

Run the same commands that the continuous integration (CI) workflow runs and make sure you get a clean build with no warnings, all tests passing, and no Address Sanitizer errors.

bash
make clean
make all
make check
make leak-test

Then run make report and look at the coverage numbers at the bottom of the output. The grading rubric explains how coverage is graded.

Push and check CI ​

Commit and push all your work.

bash
git add --all
git commit -m "Finished the project"
git push

Open your repository on GitHub, click the Actions tab, then Continuous Integration (CI), and confirm that the run for your last push is green. If it is not, open the run, read the output, and fix the problem.

Create the submission report ​

  1. In the Actions tab click Create Submission Report Via GitHub Action.
  2. Click Run workflow and run it on the master branch.
  3. Wait for the run to finish and then refresh your repository. You will now have a file named submission-report.docx that contains your README, the build output, the test results, the coverage report, the Address Sanitizer report, and all your code.
  4. The workflow added a commit to your repository, so run git pull in your Codespace (or wherever you cloned the repository) before you make any more changes. If you skip this, your next push will be rejected.

DANGER

Do NOT edit the generated report. The report ends with a hash of its contents and any changes will be reported as academic dishonesty. If something in the report is wrong, fix your code, push, and run the workflow again.

GitHub Actions is down

If GitHub Actions is down, or the workflow hangs for more than 5 minutes, you can generate the report on Onyx instead. Follow the steps in docs/onyx.md in your repository, which install gcovr and run scripts/create-submission-report.sh.

Submitting ​

Download submission-report.docx from GitHub and submit it to Canvas. You can view your own submission in Canvas, so open it and make sure everything looks right. Your grade will be updated after the due date (and late window) has passed.

Rust for Linux (RFL) ​

If you want to go even further, the kernel now supports modules written in Rust.

Released under the MIT License.