内核模块化编程.doc
如有侵权,请联系网站删除,仅供学习与交流内核模块化编程【精品文档】第 7 页一、实验目的学习Linux内核的系统调用,理解、掌握Linux系统调用的实现框架、用户界面、参数传递、进入/返回过程。阅读Linux内核源代码,通过添加一个简单的系统调用实验,进一步理解Linux操作系统处理系统调用的统一流程。二、设备环境Linux环境:utuntu14.04,内核linux 3.19.0-49-generic三、实验内容利用内核模块在现有的系统中添加一个不用传递参数的系统调用。这个系统调用的功能是实现遍历进程并打印进程树。1、模块模块是内核的一部分,但是并没有被编译到内核里面去。它们被分别编译并连接成一组目标文件,这些文件能被插入到正在运行的内核,或者从正在运行的内核中移走。内核模块至少必须有2个函数:int_module和cleanup_module。第一个函数是在把模块插入内核时调用的;第二个函数则在删除该模块时调用。由于内核模块是内核的一部分,所以能访问所有内核资源。根据对linux系统调用机制的分析,如果要增加系统调用,可以编写自己的函数来实现,然后在sys_call_table表中增加一项,使该项中的指针指向自己编写的函数,就可以实现系统调用。2、系统调用相关的数据结构函数名以“sys_”开头,后跟该系统调用的名字。例如,系统调用fork()的响应函数是sys_fork()(见Kernel/fork.c),exit()的响应函数是sys_exit()(见kernel/fork.c)。文件include/asm/unisted.h为每个系统调用规定了唯一的编号。假设用name表示系统调用的名称,那么系统调用号与系统调用响应函数的关系是:以系统调用号_NR_name作为下标,可找出系统调用表sys_call_table中对应表项的内容,它正好是该系统调用的响应函数sys_name的入口地址。系统调用表sys_call_table记录了各sys_name函数在表中的位 置,共190项。有了这张表,就很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。系统调用表共256项,余下的项是可供用户自己添加的系统调用空间。3、task_struct数据结构task_struct是linux进程描述符的数据结构,其定义位置在include/linux/sched.h,其信息组成包括:(1)进程状态信息(state, flags,ptrace)(2)调度信息(static_prio,normal_proi, run_list, array, policy)(3)内存管理(mm, active_mm)(4)进程状态位信息(binfmt,exit_state, exit_code, exit_signal)(5)身份信息(pid, tgid, uid,suid, fsuid, gid, egid, sgid, fsgid)(6)家族信息(real_parent, parent,children, sibling)(7)进程耗间信息(realtime, utime,stime, starttime)(8)时钟信息(it_prof_expires,it_virt_expires, it_sched_expires)(9)文件系统信息(link_count, fs,files)(10)IPC信息(sysvsem, signal, sighand, blocked, sigmask, pending)本次实验所要用到的是其家族信息parent,children,sibling。4、list_head数据结构children,sibling都是list_head结构的变量,list_head其实是一个简单的双向循环链表结构,其结构定义为:struct list_head struct list_head *next,*prev;list_entry(ptr,type,member) : 如果type结构中member的地址是ptr,则返回type结构的地址。方式如下:(type *)(char *)(ptr)-(unsigned long)(&(type *)0)->member)5、实验主要内容 添加系统调用的名字碑并添加系统调用号 在系统调用表中添加相应表项 编写内核调用模块 使用内核模块加载模块程序 实现sys_mycall() 编写用户态程序测试四、程序模块程序一共分为三大模块,分别为初始化模块、结束模块、内核模块插入和打印进程树模块。三个模块结合可打印出系统当下的所有进程。初始化模块包括:init_syscall(void)和clear_and_return_cr0(void)。主要用途是修改sys_call_table表首地址的只读状态和现场保护。Sys_call_table首地址属性是只读,因此修改之使其可以通过系统调用号获取系统调用程序的地址,在修改完成后恢复sys_call_table首地址的只读属性。现场保护是预存被使用服务号的原来所对应的系统调用程序。结束模块包括:exit_syscall(void),setback_cr0(unsigned int val)。主要用途是设置cr0可更改并恢复原有的中断向量表中的函数指针的值和恢复原有的cr0的值。内核模块插入包括:module_init(init_syscall),module_exit(exit_syscall)。主要用途是将设计好的程序插入到正在运行的内核,或者从正在运行的内核中移走。五、具体实施步骤及结果1、查找系统调用号系统调用号在文件unistd.h里面定义。这个文件在ubuntu14.04下位于/usr/include/asm-generic/unistd.h。现在我们在unistd.h中查找我们需要的系统调用号。系统调用号集中在1-278和1000以后,所以本次实验选择了300作为调用号。如图1。图12、在系统调用表中添加或修改相应表项添加系统调用号之后,系统才能根据这个号,作为索引,去找sys_call_table中的相应表项。这是2.6内核以前使用的方式,但是2.6以后linux隐藏了sys_call_table的地址,所以查看了System.map文件(文件位置在/boot/中,查看方式为cat /boot/System.map-$(uname -r) | grep sys_call_table),得到sys_call_table表的首地址,首地址为0xc16ce14。如图2。图23、实现sys_mycall()使用递归的方式,通过list数据结构,由父进程到子进程进行层次遍历。进程树第一层为linux的初始进程init,init加入list表中,再遍历init的子进程,接着遍历子进程的子进程,如此往复,可以完整遍历进程树。主要代码如下。asmlinkage void pstreeMy(struct task_struct *p,int b)for(l=p->children.next; l!=&(p->children);l=l->next)struct task_struct *t=list_entry(l,struct task_struct,sibling);pstreeMy(t,b+1);list_entry(ptr, type, member)宏定义主要作用是从一个结构的成员指针找到其容器的指针。在这里用来获取子进程task_struct结构的基地址。 4、系统调用程序插入内核模块源代码写好后,编辑Makefile文件,通过make编译后,产生test.ko文件。使用insmod命令将test.ko插入到内核模块中。查看是否加入成功,可使用lsmod命令。删除模块可使用rmmod命令。如图3。图35、打印进程树首先编写用户程序testMain.c,用来测试加载在内核中的test模块。使用gcc编译文件产生运行文件testMain,在运行testMain查看进程树。如图4。图4使用printk无法在终端打印出进程树,printk是将信息存在linux中的ringbuffer缓存中,可以通过dmesg查看,或者查看var/log/kern.log文件。详见附件。六、所遇到的问题及解决的方法1、对list_entry()的疑问由于对遍历进程树的不熟悉,造成遍历进程树数次错误,在查找了多次资料后,看到一种使用递归且很简洁的方式,其中使用了一个宏定义list_entry()。虽然看文档知道其可以通过成员查询成员所对应数据结构的基地址,但是对其内部实现还是不清楚。终于在程序完成后,查找资料,进一步加深了解list_entry()宏定义。#define list_entry(ptr, type, member) (type *)(char *)(ptr)-(unsigned long)(&(type *)0)->member)从一个结构的成员指针找到其容器的指针。ptr是找容器的那个变量的指针,把它减去自己在容器中的偏移量的值就应该 得到容器的指针。(容器就是包含自己的那个结构)。指针的加减要注意类型,用(char*)ptr是为了计算字节偏移。(type *)0)->member是一个小技巧。自己理解吧。前面的(type *)再转回容器的类型。把“0”强制转化为指针类型,则该指针一定指向“0”(数据段基址)。因为指针是“type *”型的,所以可取到以“0”为基地址的一个type型变量member域的地址。那么这个地址也就等于member域到结构体基地址的偏移字节数。(char *)(ptr)使得指针的加减操作步长为一字节,(unsigned long)(&(type *)0)->member)等于ptr指向的member到该member所在结构体基地址的偏移字节数。二者一减便得出该结构体的地址。转换为 (type *)型的指针,大功告成。2、sys_call_table表首地址无法获得由于linux因为安全的原因在2.6后隐藏了sys_call_table表的首地址,致使我所使用的linux3.16无法查看表的首地址。最初我使用的方式是:cat /proc/kallsyms | grep sys_call_tables但是并终端并没有显示任何内容,通过查找资料,最终找到查看/boot/system.map文件获得首地址。cat /boot/System.map-$(uname -r) | grep sys_call_table3、sys_call_table表首地址属性只读在获得地址后,并代入程序后,程序在insmod时,依然报错。通过对地址分析,发现地址是只读的,所以在程序更改地址时,程序出错。最后通过在linux论坛查找资料,发现可以修改内存中的权限,这时程序不再出问题。代码如下:int set_page_rw(long unsigned int _addr) struct page *pg; pgprot_t prot; pg = virt_to_page(_addr); prot.pgprot = VM_READ | VM_WRITE; return change_page_attr(pg, 1, prot);int set_page_ro(long unsigned int _addr) struct page *pg; pgprot_t prot; pg = virt_to_page(_addr); prot.pgprot = VM_READ; return change_page_attr(pg, 1, prot);最终我并没有采取这种方法,因为这种方法并不安全。最终采用的方式见附件代码。附件1、源码#include<linux/init.h>#include<linux/module.h>#include<linux/kernel.h>#include<linux/unistd.h>#include<asm-generic/uaccess.h>#include<linux/sched.h>#include<linux/list.h>#define my_syscall 300/要查啊啊,/boot/中的System.map-$(uname -r)#define sys_call_table_adress 0xc16ce140int b=0;int orig_cr0;unsigned long *sys_call_table = 0;static int (*anything_saved)(void);static struct task_struct *pParent;asmlinkage long sys_mycall(void);asmlinkage void pstreeMy(struct task_struct *p,int b);unsigned int clear_and_return_cr0(void)unsigned int cr0 = 0;unsigned int ret;asm("movl %cr0, %eax":"=a"(cr0);ret = cr0;cr0 &= 0xfffeffff;asm("movl %eax, %cr0":"a"(cr0);return ret;void setback_cr0(unsigned int val)asm volatile("movl %eax, %cr0":"a"(val);static int _init init_syscall(void)printk("hello, kerneln");/获取系统调用服务首地址sys_call_table = (unsigned long *)sys_call_table_adress;/保存原始系统调用的地址anything_saved = (int(*)(void) (sys_call_tablemy_syscall);/设置cr0可更改orig_cr0 = clear_and_return_cr0();/更改原始的系统调用服务地址sys_call_tablemy_syscall = (unsigned long)&sys_mycall;setback_cr0(orig_cr0);/设置为原始的只读cr0/回溯到初始父进程for(pParent=current;pParent!=&init_task;pParent=pParent->parent)return 0;static void _exit exit_syscall(void)/设置cr0中对sys_call_table的更改权限。orig_cr0 = clear_and_return_cr0();/设置cr0可更改/恢复原有的中断向量表中的函数指针的值。 sys_call_tablemy_syscall = (unsigned long)anything_saved;/恢复原有的cr0的值setback_cr0(orig_cr0);printk("call exit n");asmlinkage long sys_mycall(void) printk("This is my_syscall!n");pstreeMy(pParent,b); return current->pid;asmlinkage void pstreeMy(struct task_struct *p,int b)int i;struct list_head *l;for(i=0;i<b;i+)printk(" ");printk("|-%sn",p->comm);for(l=p->children.next; l!=&(p->children);l=l->next)struct task_struct *t=list_entry(l,struct task_struct,sibling);pstreeMy(t,b+1);module_init(init_syscall);module_exit(exit_syscall);MODULE_LICENSE("GPL");2、进程树