1.3什么是内核符号表(Kernel-Symbol-Table)
OK,我们现在已经理解了最基本的系统调用和模块的概念,但是还需要继续理解另一个十分关键的概念--内核符号表。
让我们看一眼/proc/ksyms.在里面的每一个表项代表着一个公共的内核符号.这些符号是可以被我们的LKM引用的.再认真的看一眼这个文件,你会发现很多有趣的事情.
这个文件真的很有趣,他可以帮助我们看到我们的LKM到底可以调用那些函数.但是这也同时带来一个问题,在我们的LKM中所存取的每一个符号(像函数名)也会被列在这个文件里面,也会被其他人看到.因此,一个经验丰富的系统管理员就会发现我们小小的LKM并且杀掉他.
有很多方法可以阻止管理员发现我们的LKM,这可以看第二章节.在第二章节中提到的方法可以被称之为'Hacks',但是当我们在看第二章节的内容时你会发现里面并没有提到"如何使你的LKM符号不列在/proc/ksyms中"这样的方法.在第二章中我们没有提到这个问题是基于以下理由的:
你并不需要什么特殊的技巧来使你的模块的符号不在/proc/ksyms中出现.LKM开发者们用如下的常用代码来声明他们模块的符号.
static struct symbol_table module_syms= {
/*定义自己的符号表!!*/
#include <linux/symtab_begin.h>
/*我们想引用的符号表*/
...
};
register_symtab(&module_syms);
/*实际的注册工作*/
正如我所说的,我们并不需要对外公开我们的符号,所以我们只要用如下的语句就可以了
register_symtab(NULL);
这条语句必须放在init_module()函数中,要记住!
1.4如何实现从用户空间到内核空间的转换
直到现在这篇文章所介绍的都是非常基本和简单的一些东西.从现在开始会有一些稍微难一点的.(但是并不是太难的).
在内核级别内编程带给我们很多好处,同时也给我们带来了不少缺点.系统调用必须从用户空间中获得调用的参数,(系统调用是通过包装在像libc这样的函数库中实现的)然而我们的LKM是运行在内核级别的.在第二章中我们会看到对于我们来说,通过检查某个系统调用的参数来使LKM正确的运行是十分重要的.但是,在内核空间的模块如何才能存取到用户空间分配的参数呢?
解决方法是:我们必须做一个传输转换.
对于那些非内核探索者来说,这听上去可能有些奇怪.但是这真的很简单.比如说下面这个系统调用:
int sys_chdir (const char *path)
设想系统正在调用这个函数,并且我们拦截了这个调用(我们会在第二章解释这个).我们想检查用户想去的路径.因此我们必须获得const char*
path.如果你企图通过下面的
方法直接获得path变量:
printk("<1>%s\n", path);
你就会遇到真正的问题了.....
要记住你现在是在内核空间中,你不能很轻松的读取用户空间的内存.好了,你可以获得一个plaguez提供的解决方法.他通过使用内核的函数(宏)从用户空间的内存中中提取一个字符串到内核空间:
#include <asm/segment.h>
get_user(pointer);
传给这个函数一个*path的指针从而把他从用户空间的内存拷贝到内核空间.让我们看看这段由plaguez写的从用户空间移动字符串到内核空间的代码:
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '\0') && (compt != n));
return dest;
}
如果我们想转换我们的*path变量,我们可以用如下的内核代码:
char *kernel_space_path;
kernel_space_path = (char *) kmalloc(100, GFP_KERNEL);
/*在内核空间中分配内存*/
(void) strncpy_fromfs(test, path, 20);
/*调用plaguez's的函数*/
printk("<1>%s\n", kernel_space_path);
/*现在我们可以使用这个变量了*/
kfree(test);
/*记得释放内存*/
上面的代码工作的很好.由于一个通用的转换程序有点复杂了,plaguez仅仅做了字符串的拷贝(这个函数只能用于字符串的拷贝).对于普通的数据,下面的函数是最简单的做法:
#include <asm/segment.h>
void memcpy_fromfs(void *to, const void *from, unsigned long count);
很显然,两个函数都是同一种类型的命令.但是第二个几乎和plaguez的自己新定义的函数一模一样.我推荐用memcpy_fromfs(.....)用于通用的数据传输而将plaguez的那个用于字符串拷贝.
现在我们知道了如何将数据从用户空间传送到内核空间.但是相反方向该怎么办呢?这就有点难了,因为我们并不能很容易的在内核为用户空间分配内存.如果我们可以那样做的话,我们就可以用:
#include <asm/segment.h>
void memcpy_tofs(void *to, const void *from, unsigned long count);
来做实际的转换工作.但是我们如何在用户空间分配内存给*to指针呢?plaguez的文章里面给出了最好的解决方案:
/*我们需要brk系统调用*/
static inline _syscall1(int, brk, void *, end_data_segment);
...
int ret, tmp;
char *truc = OLDEXEC;
char *nouveau = NEWEXEC;
unsigned long mmm;
mmm = current->mm->brk;
ret = brk((void *) (mmm + 256));
if (ret < 0)
return ret;
memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1);
在这里使用了一个非常漂亮的小技巧.current是当前进程的任务结构(task structure)指针,mm是mm_struct(用于那个进程的内存管理的)的指针.通过对current->mm->brk使用brk系统调用我们可以增加数据段没有使用的内存的大小.我们都知道分配内存只不过是和数据段打交道.因此通过增加未用区域的大小,我们实际上为当前的进程分配了一些内存.这块内存就可以用于从内核到(当前进程的)用户空间的拷贝.
你可能会对上面代码的第一行感到困惑.这行代码将帮助我们在内核空间使用用户空间的函数.每一个提供给我们的用户空间的函数(像fork,brk,open,read,write,......)都可以用一个_syscall(.....)宏来表示.因此我们可以构造某个系统调用宏来确切的表示特定用户空间的函数(用系统调用表示的);在这儿这个函数就是(brk.....)
看1.5获得更为详细的解释.
摘自《赛迪网》 pragmatic/THC,(版本1.0)/文