- 论坛徽章:
- 2
|
3 实现细节
3.1 modload.c
现在用代码来详细说明LKM是如何运作的。内核部分代码主要分布在sys/kern/kern_lkm.c中,我们需要分析的是这份代码文件中的一部分。下面的分析需要可执行文件格式的知识,可以在google上找到不少说明,我假设大家都具备这个基本知识。一个很好用的工具是modload的-d参数,会打印不少有用信息。
我们从modload命令入手,剖析LKM。 modload命令的代码可以在/usr/src/sbin/modload中找到。依照main()的顺序分析,可以一窥究竟。解说顺序是先引用代码,然后在引用下面解释。
- /usr/src/sbin/modload/modload.c
- ……
- main()
- {
- ……
- p = strrchr(modout, '.');
- if (!p || strcmp(p, ".o"))
- errx(2, "module object must end in .o");
- ……
复制代码
上面我提醒说modload命令所插入的模块名,必须以.o结尾,这段代码就是作这个用。
- /*
- * Verify that the entry point for the module exists.
- */
- if (verify_entry(entry, modobj)) {
- /*
- * Try <modobj>_init if entry is DFLT_ENTRY.
- */
- if (strcmp(entry, DFLT_ENTRY) == 0) {
- char *nentry;
- if ((p = strrchr(modout, '/')))
- p++;
- else
- p = modout;
- asprintf(&nentry, "%s%s", p, DFLT_ENTRYEXT);
- if (!nentry)
- err(1, "malloc");
- entry = nentry;
- if (verify_entry(entry, modobj))
- errx(1, "entry point _%s not found in %s",
- 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的使用规范。
- if (kname == NULL) {
- int fd = open(_PATH_KSYMS, O_RDONLY);
- if (fd < 0) {
- warn("%s", _PATH_KSYMS);
- } else {
- close(fd);
- kname = _PATH_KSYMS;
- }
- }
复制代码
接着这段代码的作用是打开参数_PATH_KSYMS 指定的设备。在/usr/src/include/paths.h中找到#define _PATH_KSYMS "/dev/ksyms"。也就是要求打开/dev/ksyms设备,该设备的作用是提供内核中的符号名及其地址。具体参看man 4 ksyms。
- if (kname == NULL) {
- #ifdef CPU_BOOTED_KERNEL
- ……
- rc = sysctl(……);
- ……
- #endif /* CPU_BOOTED_KERNEL */
- kname = _PATH_UNIX;
- }
复制代码
上面说过,syscall_example.o的未决符号的链接是和内核进行的,因此,这段代码作用是找到引导内核(如果不是用引导内核与.o做连接,那么执行时会出现core dump,详见man 4 lkm)。启动内核的默认情况是/netbsd(也就是_PATH_UNIX 所指的情况,在src/include/paths.h中定义#define _PATH_UNIX "/netbsd"),但是在系统引导阶段可以指定内核,所以用sysctl接口取得引导内核。(本文假设默认情况,即假设/netbsd)。
- 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。
- 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查证。
- if (ioctl(devfd, LMRESERV, &resrv) == -1)
复制代码
这里操作/dev/lkm伪设备,在内核里分配一部分内存,作为存放代码只用。(后面再解释详情)。分配得到的内存,记录在参量resrv中。
- if (prelink(kname, entry, out, (void *)resrv.addr, modobj, ldscript))
- ……
- 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中有如下几行:
- dev_type_open(lkmopen);
- dev_type_close(lkmclose);
- dev_type_ioctl(lkmioctl);
- const struct cdevsw lkm_cdevsw = {
- lkmopen, lkmclose, noread, nowrite, lkmioctl,
- nostop, notty, nopoll, nommap, nokqfilter, D_OTHER,
- };
复制代码
简单说来就是设备操作的入口,lkmopen、lkmclose和lkmioctl分别对应打开,关闭,和上面提到的ioctl操作。Lkm{open,close}都比较简单,lkmopen()的功能集中在避免相同模块多次初始化,lkmclose主要是收回之前已经分配的内存。
lkmioctl()是一个比较复杂的函数,我们只拣相关片断,其余的比较容易举一反三。
- case LMRESERV:
- ……
- case LMLOADBUF:
- ……
- case LMREADY:
- ……
复制代码
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最开始处定义:
- #define LKM_SPACE_ALLOC(size, exec) \
- uvm_km_alloc(lkm_map, (size), 0, \
- 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中实现:
- MOD_SYSCALL( "syscall_example", -1, &newent)
- ……
- int syscall_example_lkmentry __P((struct lkm_table *, int, int));
- syscall_example_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
- {
- DISPATCH(lkmtp,cmd,ver,syscall_load,lkm_nofunc,lkm_nofunc)
- }
复制代码
按照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)。
- #define LKM_DISPATCH(lkmtp, cmd, envdep, load, unload, stat)\
- …
- case LKM_E_LOAD: \
- …
- case LKM_E_UNLOAD: \
- …
- case LKM_E_STAT:\
- …
复制代码
分别执行宏变量load,unload,stat传递的函数,本例中为syscall_load(),lkm_nofunc(),lkm_nofunc(),然后将cmd命令传递到lkmdispatch()中。
lkmdispatch()在kern_lwp.c中。判断模块的类型,然后执行模块的命令:
- switch(lkmtp->private.lkm_any->lkm_type) {
- case LM_SYSCALL:
- error = _lkm_syscall(lkmtp, cmd);
- break;
- …
- case LM_VFS:
- …
- }
复制代码
本例中是LM_SYSCALL,因此调用_lkm_syscall()。
- _lkm_syscall(struct lkm_table *lkmtp, int cmd)
- {
- ……
- switch(cmd) {
- case LKM_E_LOAD:
- ……
- memcpy(&args->lkm_oldent, &sysent[ i], sizeof(struct sysent));
- /* replace with new */
- memcpy(&sysent[ i], args->lkm_sysent, sizeof(struct sysent));
- /* done! */
- args->mod.lkm_offset = i; /* slot in sysent[] */
- case LKM_E_UNLOAD:
- ……
- memcpy(&sysent[i], &args->lkm_oldent, sizeof(struct sysent));
- ……
- }
复制代码
在第一次加载模块时,关键作用在这两个memcpy上。这两句的作用是替换i号系统调用的函数指针,指向我们提供的地址。在本例中,也就是将i=210的系统调用号所指地址替换成example_syscall()的地址(虽然在系统里210号是预留给LKM的)。最后返回lkm_offset,这也就是我们用modstat取得的系统调用号(即相对于系统调用表的偏移)。而在UNLOAD的时候进行恢复。
到此,基本向大家阐明了LKM机制的过程。
[ 本帖最后由 gvim 于 2006-9-11 20:18 编辑 ] |
|