当前位置:Linux教程 - Linux - 在Linux可加载内核模块中探秘(5)

在Linux可加载内核模块中探秘(5)



         第一部分:基础知识
    第五章:在内核空间建立自己的函数
    作者:CoolBoy

    【ChinaByte 教程】上一章我们讲到采用宏的方法来建立自己的系统函数。但也许有的同学对这个“宏”还没弄得很清楚,今天我们就来详细讲解一下。
      还记得上次我们讲到的这一行程序吗?

      static inline _syscall1(int, brk, void *, end_data_segment);

      是不是感觉很难以理解?这里我们用宏构造了一个功能类似于用户空间brk()的brk函数,这个函数负责增加数据段的长度,可是这个宏是如何具体实现的呢?下面是它的详细代码:

    #define _syscall1(type,name,type1,arg1) \\
    type name(type1 arg1) \\
    { \\
    long __res; \\
    __asm__ volatile (\"int $0x80\" \\
    : \"=a\" (__res) \\
    : \"0\" (__NR_##name),\"b\" ((long)(arg1))); \\
    if (__res >= 0) \\
    return (type) __res; \\
    errno = -__res; \\
    return -1; \\
    }
    /*注一:这个宏仅适于在内核空间中使用*/
    /*注二:这是个inline宏,所以我们使用了“\\”续行符*/
    /*注三:参数name代表着我们所需要呼叫的系统调用,在asm/unistd.h文件中,name又被扩展定义为__NR_##name*/

      其实这段代码就是从asm/unistd.h文件中截取的,从代码中看出,利用这个宏,我们能用0x80中断来呼叫名字为“name”的任何系统调用,从而实现操作系统的各种功能。说穿了,用户空间中的大多数库函数都是由类似于_syscall的宏来构造的,例如fork,brk,open,read,write等等,它们都采用“宏+中断”模式来呼叫系统调用。所以,虽然在内核空间不能直接调用brk()函数,但通过调用类似的宏,我们就能很轻松地建立起自己的brk()函数了。
      不过,除了用宏之外,我个人还喜欢用另外一种更为简单的办法。:)

    int (*open)(char *, int, int); /*先定义一个函数原型*/
    open = sys_call_table[SYS_open]; /*这里也可以用另一个常量 __NR_open*/

      在这里我们先定义了一个函数open的原型,然后我们将系统调用表里面相应函数指针的值赋给它,这样我们也能达到相同的目的,并且不需要用到任何宏。无独有偶,我在网上发现著名的LKM病毒SVAT也采用了这个机制,说明这种方法还是很有效的,不过效果究竟如何还是要大家自己用了才知道。

      另外,这两种方法还有一点容易被人忽略:因为它们实际上都是呼叫系统调用,而系统调用只接受从用户空间来的参数,所以我们还必须把内核空间的参数转换到用户空间去。

      这里又说到转换,想起前天cy网友问的内核空间和用户空间编程的区别问题,好象很不一样——其实也没什么很不同的地方,因为内核和用户空间是两个不同的内存空间,互相通不了信息,在用户空间能访问到的函数,在内核空间是访问不到的,仅有少数几个可以用,并且参数也不能共享,这就是两种空间在编程上的区别。而谈到其中的机制问题,简单来说,linux采用一系列寄存器(register)来区别用户和核心空间,比如刚才的例子,系统调用为什么只接受从用户空间来的参数?其实这是因为它只能通过DS寄存器访问到DS覆盖的那一段内存,若能把“段选择器”FS的值改变一下,比如改为用户空间的数据段寄存器DS的值(DS中存放有调用参数),那么,系统调用则可直接去访问内核空间中的参数。

      要改变FS的值并不困难,我们可以先用函数get_ds()检索到DS的值,然后用函数set_fs(get_ds())来做到这一点,只不过要注意保留FS的值以便用完后恢复。我们用下例来说明:

    unsigned long old_fs_value=get_fs();
    set_fs(get_ds); /*这样我们就能直接去访问用户空间的数据了*/
    open(filename, O_CREAT|O_RDWR|O_EXCL, 0640); /*filename实际上存储于内核空间*/
    set_fs(old_fs_value); /*恢复fs*/

      就我个人来看,直接改变寄存器的值是最容易,最高效的一种方法。不过当然,最终还得要由你自己来判断。

      还记得我们提到过的函数吗?brk,open等等,它们都有各自的系统调用相对应。不过也有很多用户空间的函数到头来都集中到同一个系统调用上,例如系统调用sys_socketcall,几乎所有的关于socket的函数create,close,send,revcieving等等,全都是在调用它。所以在建立你自己的内核函数之前,多看看核心的源代码也许会学到很多有用的方法哦。

      当然,内核中本来就有的函数,我们就不必再费劲自己去建立了,我们已经见识过了printk()核心函数,这些函数是通常是通过库函数的形式提供给内核开发者的,以满足内核开发中的一些简单的需要。下面的列表中包含了我们平时所要用到的一些内核函数:

    将数据打包成字符流的形式
    int sprintf (char *buf, const char *fmt, ...);
    int vsprintf (char *buf, const char *fmt, va_list args); 将数据于核心空间打印
    printk (...) 内存函数
    void *memset (void *s, char c, size_t count);
    void *memcpy (void *dest, const void *src, size_t count);
    char *bcopy (const char *src, char *dest, int count);
    void *memmove (void *dest, const void *src, size_t count);
    int memcmp (const void *cs, const void *ct, size_t count);
    void *memscan (void *addr, unsigned char c, size_t size); 字符比较函数
    int register_symtab (struct symbol_table *intab); see I.1
    char *strcpy (char *dest, const char *src);
    char *strncpy (char *dest, const char *src, size_t count);
    char *strcat (char *dest, const char *src);
    char *strncat (char *dest, const char *src, size_t count);
    int strcmp (const char *cs, const char *ct);
    int strncmp (const char *cs,const char *ct, size_t count);
    char *strchr (const char *s, char c);
    size_t strlen (const char *s);
    size_t strnlen (const char *s, size_t count);
    size_t strspn (const char *s, const char *accept);
    char *strpbrk (const char *cs, const char *ct);
    char *strtok (char *s, const char *ct); 可存取用户空间的函数
    unsigned long simple_strtoul (const char *cp, char **endp, unsigned int base); /*字符转换为数字*/
    get_user_byte (addr);
    put_user_byte (x, addr);
    get_user_word (addr);
    put_user_word (x, addr);
    get_user_long (addr);
    put_user_long (x, addr); 检查高级用户的权限
    suser();
    fsuser();
    注册设备驱动的函数
    (..._chrdev -> 字符设备 ; ..._blkdev -> 块设备)
    int register_chrdev (unsigned int major, const char *name, struct file_o perations *fops);
    int unregister_chrdev (unsigned int major, const char *name);
    int register_blkdev (unsigned int major, const char *name, struct file_o perations *fops);
    int unregister_blkdev (unsigned int major, const char *name);

      今天主要讨论了几种建立核心函数的方法,以及用寄存器实现空间的切换,如果对C语言不太熟悉,今天学习的东西也许就有些困难,不过程序只要看懂了就可以了。用户空间和内核空间的转换,以及核心函数的建立,都是为我们以后能自己动手写一个有用的LKM打下基础。下节我们会深入了解内核守护进程是怎样的一个概念,请大家继续关注哦!



    发布人:Crystal 来自:Linux专区