8.1. 系统调用

到目前为止,我们所做的只是使用完善的内核机制注册 /proc 文件和处理设备的对象。如果只是想写一个设备驱动,这些内核程序员设定的方式已经足够了。但是,你不想做一些不寻常的事吗,想使你的系统看起来不一样吗?当然,这取决你自己。

这里可是一个危险的地方。下面的这个例子中,我关闭了系统调用 open()。这意味着我无法打开任何文件,执行任何程序,连使用 shutdown 关机都不行,关机只能靠摁电源按钮了。幸运的话,不会有文件丢失。要保证不丢失文件的话,在 insmodrmmod 之前请执行 sync 命令。

别管什么 /proc 文件和什么设备文件了,它们只是小的细节问题。所有进程同内核打交道的根本方式是系统调用。当一个进程需要内核提供某项服务时(像打开一个文件,生成一个新进程,或要求更多的内存),就会发生系统调用。如果你想你的系统运作方式看起来有意思点,这就是你动手的地方。顺便说一句,如果你想知道没个程序使用了哪些系统调用,运行 strace <arguments>

总的来说,一个用户进程是不应该也不能够直接访问内核的。它不能访问内核的内存,也不能调用内核的函数。这是CPU的硬件保护机制决定的(这也是为什么叫做“保护模式”的原因)。

系统调用是这条规则的例外。所发生的事是一个进程用合适的值填充寄存器,然后调用一条跳转到已被定义过的内核中的位置的指令(当然,这些定义过的位置是对于用户进程可读的,但是显然是不可写的)。在Intel架构中,这是通过 0x80 中断完成的。硬件明白一旦你跳转到这个位置,你就不再是在处处受限的用户态中运行了,而是在无所不能的内核态中。

内核中的进程可以跳转过去的位置叫做系统调用。那儿将检查系统调用的序号,这些序号将告诉内核用户进程需要什么样的服务。然后,通过查找系统调用表 (sys_call_table) 找到内核函数的地址,调用该函数。当函数返回时,再做一些系统检查,接着就返回用户进程(或是另一个进程,如果该进程的时间用完了)。如果你想阅读一下这方面的源代码,它们就在文件 arch/$<$architecture$>$/kernel/entry.S ENTRY(system_call)行的下面。

所以,如果我们想改变某个系统调用的运作方式,我们只需要用我们自己的函数去实现它(通常只是加一点我们自己的代码,然后调用原函数)然后改变系统调用表中的指针值使它指向我们的函数。因为这些模块将在以后卸载,我们不想系统因此而不稳定,所以在 cleanup_module 中恢复系统调用表是非常重要的。

这就是这样的一个模块。我们可以“监视”一个特定的用户,然后使用 printk() 输出该用户打开的每个文件的消息。在结束前,我们用自己的 our_sys_open 函数替换了打开文件的系统调用。该函数检查当前进程的用户序号(uid,user's id),如果匹配我们监视的用户的序号,它调用 printk() 输出将要打开的文件的名字。要不然,就用同样的参数调用原始的 sys_open 函数,真正的打开文件。

函数 init_module 改变了系统调用表中的恰当位置的值然后用一个变量保存下来。函数 cleanup_module 则使用该变量将所有东西还原。这种处理方法其实是很危险的。想象一下,如果我们有两个这样的模块,A和B。A用A_open替换了系统的sys_open函数,而B用B_open。现在,我们先把模块A加载,那么原先的系统调用被A_open替代了,A_open在完成工作后自身又会调用原始的sys_open函数 。接着,我们加载B模块,它用B_open更改了现在的已更改为A_open(显然它认为是原始的sys_open系统调用)的系统调用。

现在,如果B先卸载,一切正常——系统调用会还原到A_open,而A_open又会调用原始的sys_open。但是,一旦A先卸载,系统就会崩溃。A的卸载会将系统调用还原到原始的sys_open,把B从链中切断。此时再卸载B,B会将系统调用恢复到它认为的初始状态,也就是A_open,但A_open已经不在内存中了。乍一看来,我们似乎可以通过检测系统调用是否与我们的open函数相同,如果不相同则什么都不做(这样B就不会尝试在卸载时恢复系统调用表)。但其实这样更糟。当A先被卸载时,它将检测到系统调用已被更改为B_open,所以A将不会在卸载时恢复系统调用表中相应的项。此时不幸的事发生了,B_open将仍然调用已经不存在的A_open,这样即使你不卸载B模块,系统也崩溃了。

我可以想到两种方法去解决这个问题。第一种方法是将函数调用恢复到原始的 sys_open 。但是很不幸, sys_open 不是内核系统调用表 /proc/ksyms 的一部分,我们无法找到它。第二种方法是使用计数器,这样就可以防止root使用 rmmod 卸载模块。这在成熟的实用模块中是一个好办法,但却不适合教学演示——所以我并没在我的例子中使用它。

Example 8-1. procfs.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;
}