第 8 章。系统调用

系统调用

到目前为止,我们唯一做的事情是使用定义完善的内核机制来注册/proc文件和设备处理程序。 如果你想做一些内核程序员认为你想做的事情,例如编写设备驱动程序,这很好。 但是,如果你想做一些不寻常的事情,以某种方式改变系统的行为呢? 那么,你基本上就得靠自己了。

这就是内核编程变得危险的地方。 在编写下面的示例时,我终止了open()系统调用。 这意味着我无法打开任何文件,无法运行任何程序,也无法shutdown计算机。 我不得不拔掉电源开关。 幸运的是,没有文件损坏。 为了确保你也不会丢失任何文件,请在执行 insmodrmmod 之前运行 sync

忘记/proc文件,忘记设备文件。 它们只是次要细节。 真正的进程到内核通信机制,所有进程都使用的机制,是系统调用。 当进程向内核请求服务时(例如打开文件,fork 到新进程,或请求更多内存),这就是使用的机制。 如果你想以有趣的方式改变内核的行为,这就是你应该去的地方。 顺便说一句,如果你想查看程序使用了哪些系统调用,请运行 strace <arguments>

一般来说,进程不应该能够访问内核。 它不能访问内核内存,也不能调用内核函数。 CPU 的硬件强制执行这一点(这就是它被称为“保护模式”的原因)。

系统调用是这个一般规则的例外。 发生的情况是,进程用适当的值填充寄存器,然后调用一个特殊的指令,该指令跳转到内核中预先定义的位置(当然,该位置对用户进程是可读的,但不可写)。 在 Intel CPU 下,这是通过中断 0x80 完成的。 硬件知道,一旦你跳转到这个位置,你就不再以受限的用户模式运行,而是作为操作系统内核 --- 因此你被允许做任何你想做的事情。

进程可以跳转到的内核位置称为 system_call。 该位置的过程检查系统调用号,该号码告诉内核进程请求了什么服务。 然后,它查看系统调用表(sys_call_table)以查看要调用的内核函数的地址。 然后它调用该函数,并在返回后,执行一些系统检查,然后返回到进程(或者如果进程时间耗尽,则返回到不同的进程)。 如果你想阅读这段代码,它在源文件arch/$<$architecture$>$/kernel/entry.S,在行ENTRY(system_call).

因此,如果我们想改变某个系统调用的工作方式,我们需要做的是编写我们自己的函数来实现它(通常是通过添加一点我们自己的代码,然后调用原始函数),然后更改sys_call_table的指针指向我们的函数。 因为我们稍后可能会被移除,并且我们不想使系统处于不稳定状态,所以对于cleanup_module来说,将表恢复到其原始状态非常重要。

这里的源代码是这样一个内核模块的示例。 我们想“监视”某个用户,并printk()在用户每次打开文件时显示一条消息。 为了这个目的,我们将打开文件的系统调用替换为我们自己的函数,称为our_sys_open。 此函数检查当前进程的 uid(用户 ID),如果它等于我们监视的 uid,则调用printk()来显示要打开的文件名。 然后,无论哪种方式,它都会使用相同的参数调用原始的open()函数,以实际打开文件。

init_module函数替换了sys_call_table中的相应位置,并将原始指针保存在变量中。 的cleanup_module函数使用该变量将一切恢复正常。 这种方法是危险的,因为可能有两个内核模块更改同一个系统调用。 假设我们有两个内核模块 A 和 B。 A 的 open 系统调用将是 A_open,B 的将是 B_open。 现在,当 A 插入内核时,系统调用被替换为 A_open,它将在完成时调用原始的 sys_open。 接下来,B 被插入内核,这会将系统调用替换为 B_open,它将在完成时调用它认为是原始系统调用 A_open 的东西。

现在,如果首先移除 B,一切都会很好 --- 它只会将系统调用恢复到 A_open,后者会调用原始的。 但是,如果先移除 A,然后再移除 B,系统将崩溃。 移除 A 会将系统调用恢复到原始的 sys_open,将 B 从循环中切断。 然后,当移除 B 时,它会将系统调用恢复到 认为是原始的 A_open,但 A_open 不再在内存中。 乍一看,似乎我们可以通过检查系统调用是否等于我们的 open 函数来解决这个特定问题,如果是,则根本不更改它(这样 B 在移除时就不会更改系统调用),但这会造成更糟糕的问题。 当移除 A 时,它看到系统调用已更改为 B_open,因此它不再指向 A_open,因此它不会在从内存中移除之前将其恢复为 sys_open。 不幸的是,B_open 仍然会尝试调用不再存在的 A_open,因此即使不移除 B,系统也会崩溃。

我可以想到两种方法来防止这个问题。 第一种是将调用恢复为原始值 sys_open。 不幸的是,sys_open 不是/proc/ksyms中的内核系统表的一部分,所以我们无法访问它。 另一个解决方案是使用引用计数来防止 root 用户在模块加载后 rmmod 模块。 这对于生产模块来说是好的,但对于教育示例来说是不好的 --- 这就是我没有在此处执行此操作的原因。

示例 8-1. syscall.c

/*  syscall.c 
 * 
 *  System call "stealing" sample.
 */


/* Copyright (C) 2001 by Peter Jay Salzman */


/* The necessary header files */

/* Standard in kernel modules */
#include <linux/kernel.h>   /* We're doing kernel work */
#include <linux/module.h>   /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

#include <sys/syscall.h>  /* The list of system calls */

/* For the current (process) structure, we need
 * this to know who the current user is. */
#include <linux/sched.h>




/* In 2.2.3 /usr/include/linux/version.h includes a 
 * macro for this, but 2.0.35 doesn't - so I add it 
 * here if necessary. */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif



#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>
#endif



/* The system call table (a table of functions). We 
 * just define this as external, and the kernel will 
 * fill it up for us when we are insmod'ed 
 */
extern void *sys_call_table[];


/* UID we want to spy on - will be filled from the 
 * command line */
int uid;  

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
MODULE_PARM(uid, "i");
#endif

/* A pointer to the original system call. The reason 
 * we keep this, rather than call the original function 
 * (sys_open), is because somebody else might have 
 * replaced the system call before us. Note that this 
 * is not 100% safe, because if another module 
 * replaced sys_open before us, then when we're inserted 
 * we'll call the function in that module - and it 
 * might be removed before we are.
 *
 * Another reason for this is that we can't get sys_open.
 * It's a static variable, so it is not exported. */
asmlinkage int (*original_call)(const char *, int, int);



/* For some reason, in 2.2.3 current->uid gave me 
 * zero, not the real user ID. I tried to find what went 
 * wrong, but I couldn't do it in a short time, and 
 * I'm lazy - so I'll just use the system call to get the 
 * uid, the way a process would. 
 *
 * For some reason, after I recompiled the kernel this 
 * problem went away. 
 */
asmlinkage int (*getuid_call)();



/* The function we'll replace sys_open (the function 
 * called when you call the open system call) with. To 
 * find the exact prototype, with the number and type 
 * of arguments, we find the original function first 
 * (it's at fs/open.c). 
 *
 * In theory, this means that we're tied to the 
 * current version of the kernel. In practice, the 
 * system calls almost never change (it would wreck havoc 
 * and require programs to be recompiled, since the system
 * calls are the interface between the kernel and the 
 * processes).
 */
asmlinkage int our_sys_open(const char *filename, 
                            int flags, 
                            int mode)
{
  int i = 0;
  char ch;

  /* Check if this is the user we're spying on */
  if (uid == getuid_call()) {  
   /* getuid_call is the getuid system call, 
    * which gives the uid of the user who
    * ran the process which called the system
    * call we got */

    /* Report the file, if relevant */
    printk("Opened file by %d: ", uid); 
    do {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
      get_user(ch, filename+i);
#else
      ch = get_user(filename+i);
#endif
      i++;
      printk("%c", ch);
    } while (ch != 0);
    printk("\n");
  }

  /* Call the original sys_open - otherwise, we lose 
   * the ability to open files */
  return original_call(filename, flags, mode);
}



/* Initialize the module - replace the system call */
int init_module()
{
  /* Warning - too late for it now, but maybe for 
   * next time... */
  printk("I'm dangerous. I hope you did a ");
  printk("sync before you insmod'ed me.\n");
  printk("My counterpart, cleanup_module(), is even"); 
  printk("more dangerous. If\n");
  printk("you value your file system, it will ");
  printk("be \"sync; rmmod\" \n");
  printk("when you remove this module.\n");

  /* Keep a pointer to the original function in 
   * original_call, and then replace the system call 
   * in the system call table with our_sys_open */
  original_call = sys_call_table[__NR_open];
  sys_call_table[__NR_open] = our_sys_open;

  /* To get the address of the function for system 
   * call foo, go to sys_call_table[__NR_foo]. */

  printk("Spying on UID:%d\n", uid);

  /* Get the system call for getuid */
  getuid_call = sys_call_table[__NR_getuid];

  return 0;
}


/* Cleanup - unregister the appropriate file from /proc */
void cleanup_module()
{
  /* Return the system call back to normal */
  if (sys_call_table[__NR_open] != our_sys_open) {
    printk("Somebody else also played with the ");
    printk("open system call\n");
    printk("The system may be left in ");
    printk("an unstable state.\n");
  }

  sys_call_table[__NR_open] = original_call;
}