8.1. 系统调用

到目前为止,我们唯一做的事情是使用定义良好的内核机制来注册/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()函数,以实际打开文件。

Theinit_module函数替换sys_call_tableThecleanup_module函数使用该变量将一切恢复正常。 这种方法是危险的,因为可能有两个内核模块更改同一个系统调用。 想象一下,我们有两个内核模块 A 和 B。A 的 open 系统调用将是 A_open,B 的将是 B_open。 现在,当 A 插入内核时,系统调用被替换为 A_open,它在完成时将调用原始的 sys_open。 接下来,B 插入内核,它将系统调用替换为 B_open,它在完成时将调用它认为的原始系统调用 A_open。

现在,如果先移除 B,一切都会很好——它只会将系统调用恢复为 A_open,而 A_open 又会调用原始的 sys_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_call_table 不再导出。 这意味着,如果你想做一些比仅仅运行此示例更多的操作,你将必须修补当前的内核才能导出 sys_call_table。 在示例目录中,你将找到 README 和补丁。 正如你可以想象的那样,这样的修改不应掉以轻心。 不要在贵重的系统(即你不拥有或无法轻松恢复的系统)上尝试此操作。 你需要获取本指南的完整源代码作为 tarball 才能获得补丁和 README。 根据你的内核版本,你甚至可能需要手动应用补丁。 还在看吗? 很好,本章也是。 如果 Wile E. Coyote 是内核黑客,这将是他尝试的第一件事。 ;)

示例 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, */
#include <linux/moduleparam.h>	/* which will have params */
#include <linux/unistd.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>
#include <asm/uaccess.h>

/* 
 * 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
 *
 * sys_call_table is no longer exported in 2.6.x kernels.
 * If you really want to try this DANGEROUS module you will
 * have to apply the supplied patch against your current kernel
 * and recompile it.
 */
extern void *sys_call_table[];

/* 
 * UID we want to spy on - will be filled from the
 * command line 
 */
static int uid;
module_param(uid, int, 0644);

/* 
 * 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);

/* 
 * 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 == current->uid) {
		/* 
		 * Report the file, if relevant 
		 */
		printk("Opened file by %d: ", uid);
		do {
			get_user(ch, filename + i);
			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(KERN_ALERT "I'm dangerous. I hope you did a ");
	printk(KERN_ALERT "sync before you insmod'ed me.\n");
	printk(KERN_ALERT "My counterpart, cleanup_module(), is even");
	printk(KERN_ALERT "more dangerous. If\n");
	printk(KERN_ALERT "you value your file system, it will ");
	printk(KERN_ALERT "be \"sync; rmmod\" \n");
	printk(KERN_ALERT "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(KERN_INFO "Spying on UID:%d\n", uid);

	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(KERN_ALERT "Somebody else also played with the ");
		printk(KERN_ALERT "open system call\n");
		printk(KERN_ALERT "The system may be left in ");
		printk(KERN_ALERT "an unstable state.\n");
	}

	sys_call_table[__NR_open] = original_call;
}