Linux内核中增加一个系统调用
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
选题要求:在Linux内核中增加一个系统调用,并编写对应的linux应用程序。利用该系统调用能够遍历系统当前所有进程的任务描述符,并按进程父子关系将这些描述符所对应的进程id(PID)组织成树形结构显示。
目录
一.程序的主要设计思路,实现方式 (1)
1.1 添加系统调用的两种方法 (1)
1.1.1编译内核法 (1)
1.1.2内核模块法 (1)
1.2 程序的主要设计思路 (1)
1.3 环境 (2)
二.程序的模块划分,及对每个模块的说明 (2)
2.1 通过内核模块实现添加系统调用 (2)
2.1.1修改系统调用的模块 (2)
2.1.2获取sys_call_table的地址 (2)
2.1.3清除内存区域的写保护 (3)
2.2 编写系统调用指定自己的系统调用 (4)
2.2.1内核的初始化函数 (4)
2.2.2自己的系统调用服务例程 (4)
2.2.3移除内核模块时,将原有的系统调用进行还原 (6)
2.2.4模块注册相关 (6)
2.3 编写用户态的测试程序 (6)
2.4 编写Makefile文件 (7)
三.所遇到的问题及解决的方法 (8)
3.1 进程个数确定 (8)
3.2 被更改的系统调用号的选择 (8)
3.3 获取系统调用表的地址 (8)
3.4 内核和用户态数据交换 (8)
四.程序运行结果及使用说明 (8)
4.1 将编译出来的内核模块hello.ko加载到内核中 (8)
4.2通过dmesg查看输出信息是否正确 (9)
4.3运行测试程序,输出树状打印结果(部分结果截图) (9)
4.4卸载自定义模块 (10)
五.附录 (11)
5.1 内核模块程序hello.c (11)
5.2 测试程序hello_test.c (14)
5.3 Makefile文件 (14)
一.程序的主要设计思路,实现方式
1.1 添加系统调用的两种方法
1.1.1编译内核法
编写好源码之后
以上准备工作做完之后,然后就要进行编译内核了,以下是编译内核的一个过程
1.1.2内核模块法
内核模块可以作为独立程序来编译的函数和数据类型的集合。之所以提供模块机制,是因为Linux本身是一个单内核。单内核由于所有内容都集成在一起,效率很高,但可扩展性和可维护性相对较差,模块机制可以弥补这一缺陷。
Linux模块可以通过静态或动态的方法加载到内核空间,静态加载是指在内核启动过程中加载;动态加载是指在内核运行的过程中随时加载。
一个模块被加载到内核中时,就成为内核代码的一部分。模块加载入系统时,系统修改内核中的符号表,将新加载的模块提供的资源和符号添加到内核符号表中,以便模块间通信。
这种方法是采用系统调用拦截的一种方式,改变某一个系统调用号对应的服务程序为我们自己的编写的程序,从而相当于添加了我们自己的系统调用。
下面的内容,会详述用内核模块法实现目标的过程。
1.2 程序的主要设计思路
程序分三部分,一部分是通过内核模块实现添加系统调用,二是编写系统调用指定自己的系统调用,最后是编写用户态的测试程序。
1.3 环境
Ubuntu14.04 + 3.13.0内核版本
内核版本:
二.程序的模块划分,及对每个模块的说明
2.1 通过内核模块实现添加系统调用
这种方法其实是系统调用拦截的实现。系统调用服务程序的地址是放在sys_call_table 中通过系统调用号定位到具体的系统调用地址,那么我们通过编写内核模块来修改sys_call_table中的系统调用的地址为我们自己定义的函数的地址,就可以实现系统调用的拦截。
通过模块加载时,将系统调用表里面的那个系统调用号的那个系统调用号对应的系统调用服务例程改为我们自己实现的系统历程函数地址。
2.1.1修改系统调用的模块
在/usr/include/i386-linux-gnu/asm/unistd_32.h文件中查看系统调用序号:
找到结果(部分截图):
可以看到,222号和223号系统调用是空的,因此选取223作为新的系统调用号。
2.1.2获取sys_call_table的地址
在/boot/System.map-3.16.0-30-generic查看系统调用表的内存地址:
找到结果:
为0xc165e140
2.1.3清除内存区域的写保护
得到了sys_call_table的地址,该符号对应的内存区域是只读的。所以我们要修改它,必须对它进行清除写保护,这里介绍两种方法:
第一种方法:我们知道控制寄存器cr0的第16位是写保护位。cr0的第16位置为了禁止超级权限,若清零了则允许超级权限往内核中写入数据,这样我们可以再写入之前,将那一位清零,使我们可以写入。然后写完后,又将那一位复原就行了。
//使cr0寄存器的第17位设置为0(即是内核空间可写)
unsigned int clear_and_return_cr0(void)
{
unsigned int cr0 = 0;
unsigned int ret;
asm("movl %%cr0, %%eax":"=a"(cr0));
//将cr0寄存器的值移动到eax寄存器中,同时输出到cr0变量中
ret = cr0;
cr0 &= 0xfffeffff;//将cr0变量的第17位清0
asm("movl %%eax, %%cr0"::"a"(cr0));
//将cr0变量的值放入寄存器eax中,并且放入cr0寄存器中
return ret;
}
//读取val的值到eax寄存器,再将eax寄存器的值放入cr0寄存器中---改变内核地址空间参数
void setback_cr0(unsigned int val)
{
asm volatile("movl %%eax, %%cr0"::"a"(val));
}
第二种方法:通过设置虚拟地址对应的也表项的读写属性来设置。