内核符号与模块链接

1.       内核符号表

内核符号表是linux内核提供给内核代码的一部分引用,所指的“内核代码”包括内核以及驱动程序等运行在内核内存空间的程序,“引用”包括对变量的引用,对函数的引用等。最常见的内核符号引用如printk打印输出,通常内核符号是由一部分内核代码提供给其他内核代码访问其内部数据的接口。

外部模块只能使用内核或其他外部模块导出的符号。内核或外部模块可通过EXPORT_SYMBOL宏导出符号。

如下例所示,在add.c中导出add符号,在use_add.c中使用add符号。

/* file: add.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

int add(int a, int b)
{
    return a + b;
}


static int add_init(void)
{
        return 0;
}

static void add_exit(void)
{
    return;
}

EXPORT_SYMBOL(add);
MODULE_LICENSE("GPL");
module_init(add_init);
module_exit(add_exit);


编译该模块生成add.ko, 并执行insmod add.ko;


/* file: use_add.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

extern int add(int, int);
static int add_init(void)
{
    int c = add(1, 2);
    printk("c = %d\n", c);
    return 0;
}

static void add_exit(void)
{
    return;
}

MODULE_LICENSE("GPL");
module_init(add_init);
module_exit(add_exit);


编译该模块生成use_add.ko, 并执行insmod use_add.ko;

use_add模块能正常加载,说明它能正确使用add模块中导出的符号。

2 获取内核符号的地址

 Kprobes是一个轻量级的内核调试工具,利用Kprobes技术可以在运行的内核中动态的插入探测点,在探测点处执行用户预定义的操作。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kprobes.h>

#define dbgprint(format,args...) \
        printk("intercept: function:%s-L%d: "format, __FUNCTION__, __LINE__, ##args);

struct kprobe kp_exec;
char *object = "vfs_read";

unsigned int get_symbol_addr(void)
{
    int ret = 0;
    kp_exec.symbol_name = object;
    ret = register_kprobe(&kp_exec);
    if (ret != 0 ) {
        dbgprint("cannot find %s by kprobe.\n", object);
        return -1;
    }
    dbgprint("%s at %p\n", object, (void *)kp_exec.addr);
    unregister_kprobe(&kp_exec);
    return 0;
}

static int probe_init(void)
{
    get_symbol_addr();
    return 0;
}

static void probe_exit(void)
{
    return;
}

MODULE_LICENSE("GPL");
module_init(probe_init);
module_exit(probe_exit);


但以上代码不能获取sys_call_tableaudit_socketcall等的地址,不解,另外符号的地址还可通过/proc/kallsyms中获取或调用kallsyms_lookup_name()例程获取(必须启用CONFIG_KALLSYMS 编译内核)。

3 模块的链接与卸载

模块是内核功能的扩展,用户态的进程是调用libc库,然后实现系统调用的。在编译用户态进程时可以使用静态库,也可以使用动态库,而模块是内核的一部分,无法调用用户态的库,只能调用内核自己实现的一些调用。

模块所需要调用的函数声明包含在内核的一些列头文件中,在加载模块时,内核会根据符号表来修正模块中的一些符号的地址,内核符号表在内存中,所在的位置可以通过输出内核符号表来看各个函数,变量所在的位置。

用户可通过执行insmod把一个模块链接到正在运行的内核中,该程序执行以下操作:

1.  从命令行读取要链接模块的模块名称;

2.  确定模块文件在目录树中的位置;

3.  从磁盘读入模块的目标代码文件;

4.  调用init_module系统调用,传入参数:存有模块目标代码的用户态缓冲区地址,目标代码长度和存有insmod程序所需参数的用户态内存区;

 

sys_init_module主要执行以下操作:

1.  检查用户是否有权限链接模块,当前用户必须具有CAP_SYS_MODULE权能;

if (!capable(CAP_SYS_MODULE))

         return -EPERM;

2.  为模块目标代码分配一个临时的内存区,并将用户空间的目标代码拷贝到临时内存区;

                  if (len > 64 * 1024 * 1024 || (hdr = vmalloc(len)) == NULL)

return ERR_PTR(-ENOMEM);

if (copy_from_user(hdr, umod, len) != 0) {

err = -EFAULT;

goto free_hdr;

}

3.  验证内存区中的数据是否有效表示模块的ELF对象,如果不能,返回错误码;

if (memcmp(hdr->e_ident, ELFMAG, 4) != 0 || hdr->e_type != ET_REL || !elf_check_arch(hdr) || hdr->e_shentsize != sizeof(*sechdrs)) {

err = -ENOEXEC;

goto free_hdr;

}

4.  为传给insmod的参数分配一个内存区,并存入用户态缓冲区的数据,该缓冲区地址是系统调用传入的第三个参数;

args = strndup_user(uargs, ~0UL >> 1);

         if (IS_ERR(args)) {

                   err = PTR_ERR(args);

                   goto free_hdr;

         }

5.  查找modules链表,以验证模块未被链接,通过比较模块名(module结构的name字段)进行这一检查;

if (find_module(mod->name)) {

                   err = -EEXIST;

                   goto free_mod;

         }

6.  为模块核心可执行代码分配一个内存区,并存入模块相应节的内容;

7.  为模块初始化代码分配一个内存区,并存入模块相应节的内容;

8.  为新模块确定模块对象地址,将第6,7步分配的内存地址存入模块的module_coremodule_init字段;

ptr = module_alloc(mod->core_size);

         if (!ptr) {

                   err = -ENOMEM;

                   goto free_percpu;

         }

         memset(ptr, 0, mod->core_size);

         mod->module_core = ptr;

 

         ptr = module_alloc(mod->init_size);

         if (!ptr && mod->init_size) {

                   err = -ENOMEM;

                   goto free_core;

         }

         memset(ptr, 0, mod->init_size);

         mod->module_init = ptr;

9.  初始化模块对象的modules_which_use_me链表。当前执行CPU的计数器为1,其余所有的CPU的模块引用计数为0

10. 根据模块对象许可证类型设置模块对象的license_gplok标志;

set_license(mod, get_modinfo(sechdrs, infoindex, "license"));

11. 使用内核符号表与模块符号表,重置模块目标码,即用相应的逻辑地址偏移量替换  所有外部与全局符号的实例;

/* Fix up syms, so that st_value is a pointer to location. */

         err = simplify_symbols(sechdrs, symindex, strtab, versindex, pcpuindex,mod);

12. 初始化模块对象的symsgpl_syms字段,使其指向模块导出的内存中的符号表;

/* Set up EXPORTed & EXPORT_GPLed symbols (section 0 is 0 length) */

         mod->num_syms = sechdrs[exportindex].sh_size / sizeof(*mod->syms);

         mod->syms = (void *)sechdrs[exportindex].sh_addr;

         if (crcindex)

                   mod->crcs = (void *)sechdrs[crcindex].sh_addr;

         mod->num_gpl_syms = sechdrs[gplindex].sh_size / sizeof(*mod->gpl_syms);

         mod->gpl_syms = (void *)sechdrs[gplindex].sh_addr;

13. 解析insmod程序的参数,并相应地设置模块变量的值;

/* Size of section 0 is 0, so this works well if no params */

          err = parse_args(mod->name, mod->args,

                             (struct kernel_param *)

                             sechdrs[setupindex].sh_addr,

                             sechdrs[setupindex].sh_size/ sizeof(struct kernel_param),

                             NULL);

14. 注册模块对象的mkobj字段中的kobject对象,这样在sysfs特殊文件系统的module目录中就有了一个新的子目录;

err = mod_sysfs_setup(mod,

                            (struct kernel_param *)

                            sechdrs[setupindex].sh_addr,

                            sechdrs[setupindex].sh_size/ sizeof(struct kernel_param));

15. 设置模块状态标志为MODULE_STATE_LIVE,执行其init方法,结束并返回0

 

用户可通过rmmod卸载外部模块,该程序执行以下操作:

1.  从命令行读取要卸载的模块的名字;

2.  打开/proc/modules文件,其中列出了所有链接到内核的模块,检查待卸载的模块是否有效链接;

3.  调用delete_module系统调用,向其传递要卸载的模块名称;

 

sys_delete_module服务例程主要执行以下操作:

1.  检查用户是否有权限卸载模块,当前用户必须具有CAP_SYS_MODULE权能;

2.  将模块名存入内核缓冲区;

3.  modules链表查找模块的module对象;

4.  检查模块的modules_which_use_me依赖链表,如果非空就返回一个错误码;

5.  检查模块状态,如果不为MODULE_STATE_LIVE,则返回错误码;

6.  如果模块有自定义的init方法,则检查是否有自定义的exit方法,如果没有,则返回错误码;

7.  为了避免竞争条件,除了运行sys_delete_module服务例程的CPU外,暂停系统中所有CPU的运行;

8.  把模块的状态设为MODULE_STATE_GOING

9.  如果所有模块引用计数器的累加值大于0,返回错误码;

10. 如果定义了exit方法,执行之;

11. modules链表中删除模块对象,从sysfs注销该模块;

12. 释放相应的内存区,包括可执行代码,module对象,符号表,异常表等,结束并返回0


参考资料:

http://hi.baidu.com/deep_pro/blog/item/03b2dde9c63d3c38b80e2d83.html

http://hi.baidu.com/deep_pro/blog/item/73587351e59eaf888c54308e.html

http://blog.csdn.net/abo8888882006/archive/2010/04/17/5495814.aspx

http://www.oldlinux.org/oldlinux/viewthread.php?tid=2981




 


posted @ 2013-04-19 14:07  ydzhang  阅读(1219)  评论(0编辑  收藏  举报