到目前为止,我们唯一做的事情是使用定义完善的内核机制来注册/proc文件和设备处理程序。 如果你想做一些内核程序员认为你想做的事情,例如编写设备驱动程序,这很好。 但是,如果你想做一些不寻常的事情,以某种方式改变系统的行为呢? 那么,你基本上就得靠自己了。
这就是内核编程变得危险的地方。 在编写下面的示例时,我终止了open()系统调用。 这意味着我无法打开任何文件,无法运行任何程序,也无法shutdown计算机。 我不得不拔掉电源开关。 幸运的是,没有文件损坏。 为了确保你也不会丢失任何文件,请在执行 insmod 和 rmmod 之前运行 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;
}