免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 6517 | 回复: 12
打印 上一主题 下一主题

[NetBSD] NetBSD LKM简单分析 [复制链接]

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-09-11 17:12 |只看该作者 |倒序浏览
NetBSD LKM简单分析
gvim: chinaunix.net

由于需要内核模块的相关知识,打算在Linux和BSD之中选取一个系统作为分析之用。翻了翻<Linux内核分析及编程>,觉得代码有点混乱(当然,我也不知道这本书是用的哪个Linux内核版本作为例子)。于是昨天花了一下午把NetBSD的LKM结构梳理了一下,发现LKM这个东西其实很简单。下面我把我整理的东西开放出来,供大家参考。

我不清楚NetBSD的LKM机制和FreeBSD、Linux之间的实现差异,希望有熟悉FreeBSD、Linux的朋友可以做出比较。

首先以一个实例说明LKM怎样使用,接着讲述LKM的实现机理,最后结合代码看看具体实现。

1 lkm的简单示例
我们首先以一个简单示例说明LKM怎样使用,所以这个例子越简单越好。因此我用sys/lkm/syscall/example中的例子作为讲解之用。(为了方便起见,本文把这个路径简称为$exam)

为了生成可供操作的二进制代码,可以在$exam目录中直接执行make(需要管理员权限,下面除特殊说明外,都是以管理员权限操作)。该目录中有example_syscall.c 和lkminit_syscall.c 两个文件。Lkminit_syscall.c是LKM的入口,而example_syscall.c当然是我们期望的系统调用本身。先把lkminit_syscall的实现细节放在一边,看看example_syscall.c中的内容:

  1. Int example_syscall(struct lwp *l, void *uap, register_t retval[])
  2. {
  3.          printf( "I am a loaded system call using the kernel printf!\n");
  4.          printf( "I will print this message each time I am called!\n");
  5.          return (0);     /* success (or error code from errno.h)*/
  6. }
复制代码


简单可以认为,只要执行了我们期望的该系统调用,它就会在控制台上打印出两行话。系统调用本身又可以另开一篇讲解,因此这里不打算深入系统调用的实质。

Ok,看看LKM是否如我们所预期的那样工作:

gvim# cd $exam
gvim# make   # make之后会将连接出一个名为syscall_example.o的目标文件
gvim# modload syscall_example.o # 注意最后的.o要一起提供

Sample Loaded system call
Copyright (c) 1993 Terrence R. Lambert
All rights reserved
Module loaded as ID 0


这个时候,模块就已经被插入内核,并且告诉你模块号是0。以上输出很有意思,如果你的控制台有彩色的话,可以看见下面的颜色。可见,前三句是内核的输出(因此用彩色表示,该色彩可以在conf/GENERIC中调节),最后一句是输出在用户层,所以是常色显示。如果你现在不能猜到原因,我会在后面解释。


gvim# cd $exam/test
gvim# cat Makefile

load:
……
modstat -n syscall_example
@./testsyscall
……  # 原来make load的作用是:用模块名作为参数,取得模块在内核中的详细参数
      # 然后直接调用已经生成的用户层例子来检验我们的系统调用。

gvim# modstat -n syscall_example #我把Makefile的内容手动实现,便于讲解
                                 #modstat当然猜名字也知道是取得模块状态
Type    Id   Offset Loadaddr Size Info     Rev Module Name
SYSCALL   0     210 c6caf000 0004 c6caf1c0   2 syscall_example


modstat返回了syscall_example模块的内部表示:Type类型字段表明该模块是一个系统调用,Id号是0,系统调用号偏移是210(下一段有简述),加载地址c6caf000(大于C000000,因此可以知道是内核地址,这里,你应该知道为什么前面的printf会产生颜色的原因了吧:因为printf是内核的printf,输出在内核控制台,而不是我们常用的lib中的printf),大小4K(一页),Info和Rev我没有去了解,Module Name就是我们插入模块的名称。

gvim# ./testsyscall

Table offset as reported by modstat: 210


询问我们的系统调用号,也就是在内核系统调用表中的偏移号,我们将上面得到的210给它。于是在内核控制台,得到内核消息(也就是带颜色的输出):

I am a loaded system call using the kernel printf!
I will print this message each time I am called!


通过上面的步骤,我们确定syscall_example系统调用确实可以运行。那么testsyscall怎么用的这个系统调用?testsyscall.c中的代码syscall(atoi(buf))告诉我们,它其实就是直接用syscall(210)来调用我们的系统调用。

说到系统调用号,简单看看系统调用号的分布。在sys/kern/syscalls.c的syscallnames数组中记载,找到209,210,211三个号,可以看出210到219一共10个号是预留给LKM的。而我们的例子中,使用的是210。

2 LKM的实现机理
LKM的全称是Loadable Kernel Module,也就是说在系统运行的时候,可以在内核空间里插入我们想执行的代码,以达到不用重启内核 就扩充内核功能的目的。首先由Sun引入,具体可以参考man 4 lkm(或者assiss写的 初识NETBSD LKM )。 通过上面的示例操作,知道syscall_example.o不过就是普通的目标文件,这个目标文件和未经连接的目标文件是一样的。系统可执行文件的知识告诉我们,在目标二进制代码需要成为可执行之前,要进行链接,以决断出目标文件.o中的未决符号引用。简单说来,普通的链接过程是和用户层的库连接,如-lm,-lkvm;而LKM的连接过程是和内核链接。链接过程简述如下:

一旦链接之后,内核地址空间中的这段代码就是实际可运行代码。接下来的问题是怎样才能使用这段代码?

NetBSD的解决办法是用设备的方式访问,即对/dev/lkm伪设备的操作。由于是对设备的操作,可以预见正如man 4 lkm中所说的那样,需要通过ioctl(2)接口。(FreeBSD、Linux是怎样操作的?)

[ 本帖最后由 gvim 于 2006-9-11 17:37 编辑 ]

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
2 [报告]
发表于 2006-09-11 17:13 |只看该作者
3 实现细节
3.1 modload.c
现在用代码来详细说明LKM是如何运作的。内核部分代码主要分布在sys/kern/kern_lkm.c中,我们需要分析的是这份代码文件中的一部分。下面的分析需要可执行文件格式的知识,可以在google上找到不少说明,我假设大家都具备这个基本知识。一个很好用的工具是modload的-d参数,会打印不少有用信息。

我们从modload命令入手,剖析LKM。 modload命令的代码可以在/usr/src/sbin/modload中找到。依照main()的顺序分析,可以一窥究竟。解说顺序是先引用代码,然后在引用下面解释。
  1. /usr/src/sbin/modload/modload.c
  2. ……
  3. main()
  4. {
  5.         ……
  6. p = strrchr(modout, '.');
  7.                 if (!p || strcmp(p, ".o"))
  8.                         errx(2, "module object must end in .o");
  9.                 ……
复制代码

上面我提醒说modload命令所插入的模块名,必须以.o结尾,这段代码就是作这个用。
  1.        /*
  2.          * Verify that the entry point for the module exists.
  3.          */
  4.         if (verify_entry(entry, modobj)) {
  5.                 /*
  6.                  * Try <modobj>_init if entry is DFLT_ENTRY.
  7.                  */
  8.                 if (strcmp(entry, DFLT_ENTRY) == 0) {
  9.                         char *nentry;
  10.                         if ((p = strrchr(modout, '/')))
  11.                                 p++;
  12.                         else
  13.                                 p = modout;
  14.                         asprintf(&nentry, "%s%s", p, DFLT_ENTRYEXT);
  15.                         if (!nentry)
  16.                                 err(1, "malloc");
  17.                         entry = nentry;
  18.                         if (verify_entry(entry, modobj))
  19.                                 errx(1, "entry point _%s not found in %s",
  20.                                     entry, modobj);
复制代码

verify_entry (const char *entry, char *filename) 是一个在modload.c中实现的函数,主要作用是在模块filename中查找entry入口。在main的声明处,可以看见entry的初始值const char *entry = DFLT_ENTRY; 而在modload.c文件中定义#define DFLT_ENTRY xxxinit ,同时,也可以看见DFLT_ENTRYEXT的定义 #define DFLT_ENTRYEXT  "_lkmentry"。因此,这段代码的作用是显而易见:在syscall_example.o文件中,查找是否存在名为xxxinit或者syscall_example_lkmentry的入口地址。该地址作为编译器的符号表而保留。那么,我们的syscall_example例子中的入口地址是什么?在$exam/lkminit_syscall.c中我们找到int syscall_example_lkmentry __P((struct lkm_table *, int, int));的声明。可见示例代码syscall_example的入口符合LKM的使用规范。

  1.         if (kname == NULL) {
  2.                 int fd = open(_PATH_KSYMS, O_RDONLY);
  3.                 if (fd < 0) {
  4.                         warn("%s", _PATH_KSYMS);
  5.                 } else {
  6.                         close(fd);
  7.                         kname = _PATH_KSYMS;
  8.                 }
  9.         }
复制代码

接着这段代码的作用是打开参数_PATH_KSYMS 指定的设备。在/usr/src/include/paths.h中找到#define _PATH_KSYMS "/dev/ksyms"。也就是要求打开/dev/ksyms设备,该设备的作用是提供内核中的符号名及其地址。具体参看man 4 ksyms。
  1.         if (kname == NULL) {
  2. #ifdef CPU_BOOTED_KERNEL
  3.                 ……
  4.                 rc = sysctl(……);
  5.                 ……
  6. #endif /* CPU_BOOTED_KERNEL */
  7.                         kname = _PATH_UNIX;
  8.         }
复制代码

上面说过,syscall_example.o的未决符号的链接是和内核进行的,因此,这段代码作用是找到引导内核(如果不是用引导内核与.o做连接,那么执行时会出现core dump,详见man 4 lkm)。启动内核的默认情况是/netbsd(也就是_PATH_UNIX 所指的情况,在src/include/paths.h中定义#define _PATH_UNIX "/netbsd"),但是在系统引导阶段可以指定内核,所以用sysctl接口取得引导内核。(本文假设默认情况,即假设/netbsd)。
  1. if (prelink(kname, entry, out, 0, modobj, ldscript))
复制代码

这段代码很有意思,下面的代码还会用到。prelink是modload.c中实现的函数,作用是生成用于命令行的ld命令。去到prelink()中看看,其实是一个linkcmd(&cmd, kernel, entry, outfile, address, object, ldscript);的wrapper。linkcmd()是一个在/usr/src/sbin/modload/elf.c中实现的函数(这里还有a.out格式,就不再讨论了),具体函数是elf_linkcmd(),一些asprintf(cmdp, LINKCMD…)函数按照LINKCMD的格式生成字符串。
#define LINKCMD         "ld -R %s -e %s -o %s -Ttext %p %s"
#define LINKCMD2        "ld -R %s -e %s -o %s -Ttext %p -Tdata %p %s"
#define LINKSCRIPTCMD   "ld -T %s -R %s -e %s -o %s -Ttext %p %s"
#define LINKSCRIPTCMD2  "ld -T %s -R %s -e %s -o %s -Ttext %p -Tdata %p %s"

是asprintf生成的命令格式。现在我们用modload的-d参数装载syscall_example.o,可以看到下面的有趣情景:
gvim# modload –d syscall_example.o
ld -R /dev/ksyms -e syscall_example_lkmentry -o syscall_example -Ttext 0x0 syscall_example.o
……

原来,这一次调用prelink之后构造出来的命令是ld -R /dev/ksyms -e syscall_example_lkmentry -o syscall_example -Ttext 0x0 syscall_example.o 。ld的详细参数请查阅man ld。
  1. if (mod_sizes(modfd, &modsize, &strtablen, &resrv, &stb) != 0)
复制代码

mod_sizes也是实现在/usr/src/sbin/modload/elf.c的函数,作用是取得.o中各段的偏移以及大小,分别放在modsize,strtablen等参数变量中。这里用的手段和((struct *str)0->offset)一样,在结构体的例子中相对起始地址是0,同样,模块中的相对起始地址也是0。这在上面ld命令中的参数-Ttext 0x0可以知道。

同样由modload 的-d参数,观察到下面的信息:
.text: addr = 0x0 size = 0xd8 align = 0x4
.shstrtab: addr = 0x0 size = 0x44 align = 0x1
.symtab: addr = 0x0 size = 0x2fec0 align = 0x4
.strtab: addr = 0x0 size = 0x2ad22 align = 0x1
.rodata: addr = 0xd8 size = 0xce align = 0x4
.data: addr = 0x11c0 size = 0x34 align = 0x20
.data section forced to offset 0x1c0 (was 0x11c0)

这些信息也可以用objdump查证。
  1. if (ioctl(devfd, LMRESERV, &resrv) == -1)
复制代码

这里操作/dev/lkm伪设备,在内核里分配一部分内存,作为存放代码只用。(后面再解释详情)。分配得到的内存,记录在参量resrv中。

  1. if (prelink(kname, entry, out, (void *)resrv.addr, modobj, ldscript))
  2. ……
  3. modentry = mod_load(modfd);
复制代码

前面已经取得了各段的大小,取得了内存空间,现在,就用ioctl取得的内存地址作为偏移,重定位.o中的未决符号。mod_load在elf.c中实现,作用是通过/dev/lkm接口,用ioctl()进行拷贝,比较简单。

用modload –d观察如下:

ld -R /dev/ksyms -e syscall_example_lkmentry -o syscall_example -Ttext 0xc6caf000 -Tdata 0xc6caf1c0 syscall_example.o
loading `.text': addr = 0xc6caf000, size = 0xd8
loading `.rodata': addr = 0xc6caf0d8, size = 0xce
loading `.data': addr = 0xc6caf1c0, size = 0x34
modentry = 0xc6caf000


由参数-Ttext 0xc6caf000我们知道,ioctl分配得到的内存起始地址是0xc6caf000。现在我们的syscall_example.o所包含的代码、数据就已经拷贝到内核空间中。由后面的printf("Module loaded as ID %d\n", resrv.slot);返回ID号,也就是”Module loaded as ID 0”这句输出。

到此为止,我们就完成了syscall_example.o在内核中的装载。

3.2 kern_lkm.c
要了解ioctl到底对/dev/lkm做了些什么,需要进入kern_lkm.c中查看。

在kern_lkm.c中有如下几行:
  1. dev_type_open(lkmopen);
  2. dev_type_close(lkmclose);
  3. dev_type_ioctl(lkmioctl);

  4. const struct cdevsw lkm_cdevsw = {
  5.         lkmopen, lkmclose, noread, nowrite, lkmioctl,
  6.         nostop, notty, nopoll, nommap, nokqfilter, D_OTHER,
  7. };
复制代码

简单说来就是设备操作的入口,lkmopen、lkmclose和lkmioctl分别对应打开,关闭,和上面提到的ioctl操作。Lkm{open,close}都比较简单,lkmopen()的功能集中在避免相同模块多次初始化,lkmclose主要是收回之前已经分配的内存。

lkmioctl()是一个比较复杂的函数,我们只拣相关片断,其余的比较容易举一反三。
  1. case LMRESERV:
  2. ……
  3. case LMLOADBUF:
  4. ……
  5. case LMREADY:
  6. ……
复制代码

lkmioctl()的LMRESERV命令在前面提及过,该命令由modload.c中的ioctl(devfd, LMRESERV, &resrv)一句发出。作用是分配内核内存空间。这条命令中主要流程是 a) curp = lkmalloc();分配一个表示lkm自身的数据结构,b) 接着curp->area = LKM_SPACE_ALLOC(curp->size, 1);分配curp->size大小的空间。c) resrvp->addr = curp->area;也就是我们上面得到的起始地址0xc6caf000。

LKM_SPACE_ALLOC在kern_lkm.c最开始处定义:

  1. #define LKM_SPACE_ALLOC(size, exec) \
  2.         uvm_km_alloc(lkm_map, (size), 0, \
  3.                 UVM_KMF_WIRED | ((exec) ? UVM_KMF_EXEC : 0))
复制代码

可以看见内存是在内核区分配的,是不可交换的(wired),并且是可执行段(UVM_KMF_EXEC)。

LMLOADBUF命令也提到过,在modload.c的loadbuf()函数中,用来将数据从用户区缓冲区加载到内核缓冲区。LMLOADBUF命令的主要作用在error = copyin(loadbufp->data, (caddr_t)curp->area + curp->offset, i);这句上。Copyin的分析可以在google中找到。

LMREADY命令在modload.c的最后,加载完之后是否继续执行则由modload 的-n参数保护(详见man modload)。LMREADY命令的主要作用在error = (*(curp->entry))(curp, LKM_E_LOAD, LKM_VERSION);这句上,调用一次load函数,本例中为lkminit_syscall.c中的syscall_load()。
lkmioctl()的其余命令都相对简单,就不再详诉。

3.3 lkminit_syscall.c
要完整的使用LKM还需要用户定义的入口,也就是xxxinit()或者modulename_lkmentry()。在本例中,syscall_example_lkmentry()在lkminit_syscall.c中实现:
  1. MOD_SYSCALL( "syscall_example", -1, &newent)
  2. ……
  3. int     syscall_example_lkmentry __P((struct lkm_table *, int, int));
  4. syscall_example_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
  5. {
  6.         DISPATCH(lkmtp,cmd,ver,syscall_load,lkm_nofunc,lkm_nofunc)
  7. }
复制代码


按照syscall_example_lkmentry()上的注释,入口函数中应当只包含DISPATCH这一个函数。

MOD_SYSCALL声明一个对应的数据结构,说明该模块是一个系统调用,模块名称是”syscall_example”,新的系统调用入口是”newent”。结构sysent的定义可以在sys/kern/init_sysent.c中找到。

LKM_DISPATCH宏(DISPATCH宏是它的一个wrapper)的定义在sys/sys/lkm.h 中 #define LKM_DISPATCH(lkmtp, cmd, envdep, load, unload, stat)。

  1. #define LKM_DISPATCH(lkmtp, cmd, envdep, load, unload, stat)\

  2. case LKM_E_LOAD:         \

  3. case LKM_E_UNLOAD:        \

  4. case LKM_E_STAT:\
复制代码

分别执行宏变量load,unload,stat传递的函数,本例中为syscall_load(),lkm_nofunc(),lkm_nofunc(),然后将cmd命令传递到lkmdispatch()中。

lkmdispatch()在kern_lwp.c中。判断模块的类型,然后执行模块的命令:
  1. switch(lkmtp->private.lkm_any->lkm_type) {
  2.         case LM_SYSCALL:
  3.                 error = _lkm_syscall(lkmtp, cmd);
  4.                 break;
  5.                 …
  6.         case LM_VFS:
  7.         …
  8. }
复制代码

本例中是LM_SYSCALL,因此调用_lkm_syscall()。
  1. _lkm_syscall(struct lkm_table *lkmtp, int cmd)
  2. {
  3. ……
  4.         switch(cmd) {
  5.         case LKM_E_LOAD:
  6.             ……
  7.             memcpy(&args->lkm_oldent, &sysent[ i], sizeof(struct sysent));
  8.             /* replace with new */
  9.             memcpy(&sysent[ i], args->lkm_sysent, sizeof(struct sysent));
  10.             /* done! */
  11.             args->mod.lkm_offset = i;       /* slot in sysent[] */
  12.         case LKM_E_UNLOAD:
  13.             ……
  14.             memcpy(&sysent[i], &args->lkm_oldent, sizeof(struct sysent));
  15.             ……
  16. }
复制代码

在第一次加载模块时,关键作用在这两个memcpy上。这两句的作用是替换i号系统调用的函数指针,指向我们提供的地址。在本例中,也就是将i=210的系统调用号所指地址替换成example_syscall()的地址(虽然在系统里210号是预留给LKM的)。最后返回lkm_offset,这也就是我们用modstat取得的系统调用号(即相对于系统调用表的偏移)。而在UNLOAD的时候进行恢复。

到此,基本向大家阐明了LKM机制的过程。

[ 本帖最后由 gvim 于 2006-9-11 20:18 编辑 ]

论坛徽章:
1
荣誉版主
日期:2011-11-23 16:44:17
3 [报告]
发表于 2006-09-11 17:19 |只看该作者
这个和FREEBSD的原理是相通的吧。

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
4 [报告]
发表于 2006-09-11 17:32 |只看该作者
原帖由 大大狗 于 2006-9-11 17:19 发表
这个和FREEBSD的原理是相通的吧。


是否相通,等待你去探索

论坛徽章:
0
5 [报告]
发表于 2006-09-11 18:28 |只看该作者
咋不用wiki来编辑和写文章?那样应该会舒服一些,可以有清晰 的版面

论坛徽章:
0
6 [报告]
发表于 2006-09-11 19:03 |只看该作者
前一阶段我也看了关于NETBSD LKM的文章,原理并不难。
可我没有任何能用到它的地方(内核都还不熟悉),只好先放弃了。

GVIM兄研究LKM是为了做什么东西吧?

论坛徽章:
2
亥猪
日期:2014-03-19 16:36:35午马
日期:2014-11-23 23:48:46
7 [报告]
发表于 2006-09-11 20:26 |只看该作者
原帖由 assiss 于 2006-9-11 19:03 发表
前一阶段我也看了关于NETBSD LKM的文章,原理并不难。
可我没有任何能用到它的地方(内核都还不熟悉),只好先放弃了。

GVIM兄研究LKM是为了做什么东西吧?


是的。原理很简单,刚开始乍一看kern_lkm.c 有1k多行,吓我一跳,结果仔细分析发现复杂度没有想象大。反正也看了,花了点时间把LKM框架理出来,也算是自己的一个总结。分析NetBSD将近1年了,也有一些乱78着的笔记,以后慢慢整理出来开放给大家。

原帖由 MichaelBibby 于 2006-9-11 18:28 发表
咋不用wiki来编辑和写文章?那样应该会舒服一些,可以有清晰 的版面


我还不会用wiki 只会修改别人已经建立好的。

[ 本帖最后由 gvim 于 2006-9-11 20:29 编辑 ]

论坛徽章:
0
8 [报告]
发表于 2006-09-11 21:01 |只看该作者

论坛徽章:
0
9 [报告]
发表于 2006-09-11 22:06 |只看该作者
原帖由 gvim 于 2006-9-11 20:26 发表


是的。原理很简单,刚开始乍一看kern_lkm.c 有1k多行,吓我一跳,结果仔细分析发现复杂度没有想象大。反正也看了,花了点时间把LKM框架理出来,也算是自己的一个总结。分析NetBSD将近1年了,也有一些乱78着的 ...

赞助PDF版本一个,以资鼓励,
希望看到兄弟更多的大作。

lkm.pdf

362.45 KB, 下载次数: 139

论坛徽章:
0
10 [报告]
发表于 2006-09-12 12:31 |只看该作者
Linux变化太快,找了2.6.17看了看,现在的实现与FreeBSD比较相似,一步sys_init_module完成,我记得从前还要create_module之类的了。他们都是在内核中连接,FreeBSD的实现是包括打开文件都是在内核完成,而Linux则是把文件读到buffer中,打开文件在用户态完成。对NetBSD不熟悉,从这里看,NetBSD没有实现单独的syscall而是用ioctl,其实本质差不多,只是,NetBSD的实现要分成几步来完成,先是申请内存,然后,用户态的连接,最后load进入内核。

我觉得Linux在连接这一步控制的更加严格一些,而FreeBSD的连接最自由,比如,Linux中的符号要分很多种,什么GPL之类的,而且对于内核中的没有EXPORT出来的符号,是无法使用的,而FreeBSD中,内核的所有符号KLD都能用(至少静态定义的变量也能使用,还没找到不能使用的符号,FIXME),NetBSD的连接在用户态,估计应该在这两者之间吧。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP