到目前为止,我们唯一做的事情是使用定义良好的内核机制来注册/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()函数,使用相同的参数,来实际打开文件。
Theinit_module函数替换了 中的适当位置sys_call_table并将原始指针保存在变量中。cleanup_modulecleanup_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 又会调用原始的系统调用。 但是,如果先移除 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; } |