Kenneth P. Turvey
Email: kt-web@squeakydolphin.com

Introduction

This tutorial discusses adding a simple "Hello world" system call to the Linux kernel (version 2.4). The reader will be walked through this update with a detailed explanation of each step. The development environment will be explained. It is the hope of the author that this document will assist others just beginning to learn about operating system development.

The development environment

This change can be made to the running Linux kernel on any system, but it is probably a good idea to use a kernel running in an emulator to avoid having to debug on the development system itself. There are a number of options here including, running the kernel under an i386 emulator like VMWare or Plex86, or running User Mode Linux. I have experience with both VMWare and User Mode Linux and both are quite good. I don't have any experience with Plex86 and understand that it is still somewhat unstable (2004). For our purposes User Mode Linux is probably the best choice.

User Mode Linux emulates a complete Linux system in user space. It uses the ptrace system call to trace the program while it is running. Whenever the program makes a system call ptrace intercepts it and runs the emulated system call in its place. It then changes the system call made by the program running under the emulator to a dummy system call (getpid?) and allows it to proceed on to the host kernel. The details of its operation are not really that important. What is important is that it will let you develop kernel modifications from within an emulator without causing instability in your development environment.

Before we get started you will need to download the source code for the Linux kernel and the patches for User Mode Linux. Make sure you can build user mode Linux without any modifications to the configuration. The user mode Linux pages should be of some help. A typical command line would be something like the following.

  
  make ARCH=um dep; make ARCH=um clean; make ARCH=um linux
    

You will probably want to work out networking between your host machine and the emulated environment for future use, but this document will not assume you have completed that step.

Adding the System Call

Declaring the new System Call

The first step to adding our system call will be to assign it a number. Every system call in the Linux kernel is identified by a unique integer value that is passed to the operating system through a software interrupt. The operating system uses this number to determine which system call to execute. Assuming you are developing for the Intel 386 (or later) platform these defines are located in the file, include/asm-i386/unistd.h.

Within this file you will find a long list of define statements that looks something like this:

  
  #define __NR_madvise          219
  #define __NR_madvise1         219     /* delete when C lib stub is removed */
  #define __NR_getdents64       220
  #define __NR_fcntl64          221
  #define __NR_security         223     /* syscall for security modules */
  #define __NR_gettid           224
  #define __NR_readahead        225
    

We will refer our system call as hello_world. At the end of this list add one more entry as follows, but make sure the the number assigned is one larger than the last number on your system.

  
  #define __NR_hello_world        226 
   

We need to tell the operating system what function to call when this number is used to specify a system call. We do this by putting an entry in the table defined in the file arch/i386/kernel/entry.S. The table should look something like this:

  
  .long SYMBOL_NAME(sys_getdents64)     /* 220 */
  .long SYMBOL_NAME(sys_fcntl64)
  .long SYMBOL_NAME(sys_ni_syscall)     /* reserved for TUX */
  .long SYMBOL_NAME(sys_ni_syscall)     /* Reserved for Security */
  .long SYMBOL_NAME(sys_gettid)
  .long SYMBOL_NAME(sys_readahead)      /* 225 */
    

Add your system call, hello_world, at the end of the list, like so:

  
  .long SYMBOL_NAME(sys_hello_world)
    

Its position in the table should be the same as the number you placed in the file, linux/asm/i386/unistd.h.

Implementing the system call

Our system call will implement a kind of interprocess communication, or IPC, albeit a very limited one. It will allow the kernel to say hello to a user process. As such, we will implement it in the linux/ipc/ directory. In this directory create the file hello_world.c. In this file enter the following function:

  
  #include <linux/kernel.h>
  #include <asm/uaccess.h>
  #include <linux/hello_world.h>

  asmlinkage void sys_hello_world(char *buf)
  {
          int num_left; 
          static char *hello = "Hello World";

          printk("Copy hello world to user space.\n");
          num_left = copy_to_user(buf, hello, 12);
          if (num_left)
                  printk("Number of bytes left, %d\n", num_left);
  }
    

Note that the name of the function is the same as the name we put in the header file above with sys_ prepended to it. This is a convention supported by macros in the kernel and you should follow this convention with your own system calls. It must also have the keyword asmlinkage in the definition of the system call.

This system call uses two functions provided by the kernel, printk and copy_to_user. The function, printk, works just like printf and sends the message to the console. We just use this to note that we intend to copy our message to user memory. The function is declared in linux/kernel.h so we have to include this file. The second function, copy_to_user, copies memory from the kernel to the user programs. It makes sure that the memory to which it is about to copy is already in RAM and not swapped out to disk, and then it performs the copy, much like memcpy. It may return a number of bytes left to copy if it is interrupted, but it probably won't for this small a copy. It is declared in the include file asm/uaccess.h, so this file is also included.

The last file we included was our own header file that declares our system call for user programs. If our system call was more complicated then we might have defines or inline functions in this header as well, but for our simple hello world application all we need is the following code to be entered into the new file, include/linux/hello_world.h

  
  #ifndef __LINUX_HELLO_WORLD_H
  #define __LINUX_HELLO_WORLD_H

  #include <linux/unistd.h>

  _syscall1(void,hell_world,char*,buf);

  #endif
    

The macro _syscall1 is one of a family of macros used to ease declaring our system calls to user programs. For now you don't need to worry about its expansion, just how to use it. The first argument is the return value of our system call. The second it its name, the last two are the type and name of the argument it requires respectively. There are separate macros for system calls with different numbers of arguments. These are named _syscall2, _syscall3, _syscall4, etc.

Adding the files to the Makefile

The Linux build system is nearly completely automated, but you still must tell it that the object file, hello_world.o, is necessary to implement your new system call. We do this by adding it to a line in the pertinent Makefile. The ipc/Makefile should contain a line like this:

  
  obj-y   := util.o 
    

Change it to require your new file.

  
  obj-y   := util.o hello_world.o
    

User Mode Linux

User mode Linux requires some additional changes. Each platform you wish to implement your system call on will have similar changes. First we need to declare the function that implements our system call so that the emulator can reference it. We do this by adding it to the extern declarations at the beginning of the file. You will see these lines in the file arch/um/kernel/sys_call_table.c:

  
  extern syscall_handler_t sys_setfsuid;
  extern syscall_handler_t sys_setfsgid;
  extern syscall_handler_t sys_pivot_root;
  extern syscall_handler_t sys_mincore;
  extern syscall_handler_t sys_madvise;
  extern syscall_handler_t sys_fcntl64;
  extern syscall_handler_t sys_getdents64;
    

At the end of this list add your new system call:

  
  extern syscall_handler_t sys_hello_world;
    

Farther down in this file is a table that maps the number you assigned to your new system call to the function that you just declared. We need to add a mapping for our new system call. You should see entries that look like this:

  
  [ __NR_fstat64 ] = sys_fstat64,
  [ __NR_fcntl64 ] = sys_fcntl64,
  [ __NR_getdents64 ] = sys_getdents64,
  [ __NR_security ] = sys_ni_syscall,
  [ __NR_gettid ] = sys_gettid,
  [ __NR_readahead ] = sys_readahead,
    

At the end of this list add your newly developed system call:

  
  [ __NR_hello_world ] = sys_hello_world,
    

We will also need to modify a line earlier in the file that specifies the number of the system call with the largest number in the system. Since we have added our system call as the last in the table, it now has the largest number of any system call in the operating system. Modify the line that looks like this (the system call with the largest number may be different in your version of the kernel).

  
  #define LAST_GENERIC_SYSCALL __NR_readahead
    

Change it to read:

   
  #define LAST_GENERIC_SYSCALL __NR_hello_world
    

Testing the New System Call

Build the kernel just as you did earlier. You will need to run make ARCH=um depend again because you have changed the dependencies of the kernel. After you have successfully built your kernel you should run it. If your development environment is different from you testing environment, or you are using User Mode Linux as recommended, you will need to make sure that the header files modified above also exist on the test system in the /usr/include/linux directory.

We need to develop a test program that uses our system call. A simple test program might be:

  
  #include <stdio.h>
  #include <linux/hello_world.h>

  int main(int argc, char *argv[])
  {
          char buf[25];

          hello_world(buf);
          puts(buf);
  }
    

Compile and run this on your test system. It should output a friendly greeting and send a note to the console that it copied some memory to user space.