Linux内核模块编程--系统调用
系统调用迄今为止,我们做的唯一的事就是用好已定义的内核机制去登记 /proc 文件和设备驱动处理程序。如果你想做内核程序员认为你想做的,例如写设备驱动程序,这就对了。但是如果你想做一些不平常的事,在某些地方改变系统的行为呢?那么这大多取决你自己。
这是内核编程中变得危险的地方。当写下下面的例子,我杀死了 open 系统调用。这意味着我不能打开任何文件,不能运行任何程序,甚至不能 shutdown 计算机。我不得不按电源开关。幸运的,没有文件消失。为了确保不失去任何文件,在你做insmod 和 rmmod之前请运行 sync 。
忘记 /proc 文件,忘记设备文件。它们只是次要的细节。 所有进程都要使用的和内核通信的真正的方法是系统调用。当一个进程请求内核的服务时(例如打开文件,分支一个新进程,请求更多内存),这是被使用的机制。如果你想改变内核的行为方式,这是你要做的地方。顺便说一下,如果你想看看一个程序使用了什么系统调用,运行 strace <命令> <参数列表>。
通常,一个进程不能访问内核。它不能访问内核的内存和调用内核的函数。CPU硬件易做图这个(那就是为什么叫‘保护模式’的原因)。系统调用是对这个通常的规则的例外。所发生的是进程用适当的值填充寄存器然后调用跳到内核中先前已定义的区域的特定的指令(当然,该区域是用户进程可读但不可写的)。在 Intel CPU下,使用中断 0x80 做这个。硬件知道一旦你跳到这个区域,你就不再是运行在严格的用户模式而是操作系统内核--因此你就可以做任何你想做的。
内核中那个进程可以跳到的区域被称为system_call。 在该区域的程序检查系统调用数,该数告诉内核进程请求什么服务。然后,它在系统调用表(sys_call_table)中查找调用的内核函数的地址。然后它调用该函数并在该函数返回后做一些系统检查,再返回那个进程(或者如果该进程的时间运行完了就返回到一个不同的进程)。如果你想读这个代码,它在源文件arch//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 的打开系统调用将是 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。咋看起来我们好象可以通过检查系统调用是否等于我们的函数及是否根本不去改变它(因此 当 B 被移除时不会改变系统调用)来解决这个问题,但那会制造更严重的问题。当 A 被移除,看似系统调用变为 B_open ,因此它不再指向 A_open,因而在它被从内存移除前它不会将它恢复成 sys_open。不幸的, B_open 将仍然试图调用不再存在的 A_open ,因此即使不移除 B 系统也会崩溃。 (译者认为无论是否进行检查,系统都会在A被先移除的情况下在B还未移除时使系统崩溃,因为从作者假设的情况看,B会调用“它”认为的原始的系统调用来完成其功能,在没有检查的情况下,B一样在其存储原系统调用的变量中存储A的函数A_OPEN并进行调用而使系统崩溃。即使B不调用“它”认为的原始的系统调用来完成其功能,系统也会崩溃,因为它无法恢复系统调用。)
我可以想到两个预防的办法。第一个是恢复调用为原始值 sys_open。不幸的, sys_open 不是内核系统表 /proc/ksyms 中的一部分,因此不能访问它。另一个就是使用引用计数来防止一旦模块被加载就可以随便被 rmmod。这对产品模块是一个好办法,但不适合做教育性的范例--这也是我为什么不在这做的原因。
ex syscall.c
/* syscall.c** 系统调用“偷窃”范例*//* Copyright (C) 1998-99 by Ori Pomerantz *//* 必要头文件 *//* 标准头文件 */#include /* 内核工作 */#include /* 明确指定是模块 *//* 处理 CONFIG_MODVERSIONS */#if CONFIG_MODVERSIONS==1#define MODVERSIONS#include#endif#include /* 系统调用列表 *//* 为了当前进程结构,我们需要这个知道当前用户是谁 */#include/* 在 2.2.3 版/usr/include/linux/version.h 包含该宏但 2.0.35 不包含* 加入以备需要 */#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#endif/* 系统调用表(函数表)。我们只将定义为外部的即可,当我们insmod的时候内核会为我们填充它 */extern void *sys_call_table[];/* 我们想侦察的UID - 将从命令行填充 */int uid;#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)MODULE_PARM(uid, "i");#endif/* 原始的系统调用的指针。我们保存它而不是调用原始函数是因为其他某人可能在我们之前可能已经代替了* 原始的系统调用(sys_open)。注意这并不是100%安全的,因为如果另一个模块在我们之前代替了* sys_open,然后当我们易做图入内核,我们将调用那个模块里的函数-它可能在我们之前又被移除了。** 另一个原因是我们不能得到 sys_open。 它是静态变量,因此是不可导出的。 */asmlinkage int (*original_call)(const char *, int, int);/* 因为某些原因,在 2.2.3 版中current->uid 为0而非真正的用户 ID。我试图找出什么地方错了,但* 不能在短时间内完成,而且我很懒-因此我仅仅使用系统调用得到UID,这是进程使用的方法。** 因为某些原因,在我重新编译内核后这个问题没有了。*/asmlinkage int (*getuid_call)();/* 我们将用来代替 sys_open 的函数 (当你调用 open 系统调用时这个函数被调用)。为了得到精确的原型* 连同参数的数目和类型,我们首先找到了原始的函数(在 fs/open.c 中)。** 理论上,这意味着我们将局限于内核的当前版本。实际上,系统调用几乎从来没有变化* (这将制造一场浩劫并且需要将所有的程序重新编译,因为系统调用是内核和进程的接口)。*/asmlinkage int our_sys_open(const char *filename,int flags,int mode){int i = 0;char ch;/* 检查是否是我们要侦察的用户 */if (uid == getuid_call()) {/* getuid_call 是 getuid 系统调用,它给出调用我们的系统调用的进程的用户的UID。 *//* 如果相关则报告文件 */printk("Opened file by %d: ", uid);do {#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)get_user(ch, filename+i);#elsech = get_user(filename+i);#endifi++;printk("%c", ch);} while (ch != 0);printk("
");}/* 调用原始的 sys_open - 否则,我们将失去打开文件的能力 */return original_call(filename, flags, mode);}/* 初始化模块 - 代替系统调用 */int init_module(){/* 警告 - 现在可能太迟了,但可能对下次... */printk("I'm dangerous. I hope you did a ");printk("sync before you insmod'ed me.
");printk("My counterpart, cleanup_module(), is even");printk("more dangerous. If
");printk("you value your file system, it will ");printk("be "sync; rmmod"
");printk("when you remove this module.
");/* 将原始的函数指针保存在 original_call,然后用我们的our_sys_open代替系统调用表中的相应系统调用*/original_call = sys_call_table[__NR_open];sys_call_table[__NR_open] = our_sys_open;/* 为了得到系统调用foo的函数地址,使用 sys_call_table[__NR_foo] 。 */printk("Spying on UID:%d
", uid);/* 得到 getuid 系统调用*/getuid_call = sys_call_table[__NR_getuid];return 0;}/* 清除 - 从/proc中注销相关的文件 */void cleanup_module(){/* 将系统调用恢复原状 */if (sys_call_table[__NR_open] != our_sys_open) {printk("Somebody else also played with the ");printk("open system call
");printk("The system may be left in ");printk("an unstable state.
");}sys_call_table[__NR_open] = original_call;}