高级操作系统实验报告
发布时间:2017-05-03 10:41:56
发布时间:2017-05-03 10:41:56
组号:第五组 同组同学姓名:纪天啸 宋雅迪 印佳明 于洋 湛健 日期: 2017年4月7日
实验环境:
内核版本:替换前4.8.0 替换后4.10.8
编译器:gcc 5.4.0
学习linux系统调用的过程,通过向系统内核中增加一个自己编写的新函数,实现对内核空间的读写。函数功能主要为向用户返回目前时间(书中要求返回xtime的值,在新版本中时间以由timekeeper结构体替代)。
由于编译一次内核时间较长,修改不易。所以主要实现方式为先通过编写内核模块,修改系统内存中的系统调用表,插入自己编写的内核函数,对功能进行验证。无误后,修改linux内核源代码,加入自己编写的系统调用函数,重新编译内核,对现有内核进行替换。并在用户空间对函数进行调用验证。
插入内核模块方式:
重新编译内核方式:
系统调用函数功能的实现:
附上程序源码
要求每个函数都有文件头
/*******************************************************
函数名:
参数表说明:
返回值:
函数功能描述:
********************************************************/
每个关键步骤都有注释,最好英文
插入内核模块方式:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define my_syscall_num 134//占用的原表中的系统调用号
//模块信息说明
MODULE_LICENSE("GPL");
MODULE_AUTHOR("JTX");
MODULE_DESCRIPTION("my_syscall");
MODULE_VERSION("1.0");
void disable_write_protection(void);//清除写保护
void enable_write_protection(void);//恢复写保护
asmlinkage long sys_my_syscall(int flag, struct timeval __user *thetime);//自己实现的系统调用函数
unsigned long *sys_call_table = 0;//存储系统调用表的地址
static int (*anything_saved)(void);//存储原表项的值
/*******************************************************
函数名:unsigned long **get_sys_call_table(void)
参数表说明:无
返回值:系统调用表地址
函数功能描述:获得内存中系统调用表的首地址。赋予entry内核空间首地址,对每块地址进行遍历,当entry[__NR_close]==sys_close,时即可认为找到系统调用表的地址,__NR_close为close的系统调用号。
********************************************************/
unsigned long **get_sys_call_table(void)
{
unsigned long **entry = (unsigned long **)PAGE_OFFSET;//赋予entry内核空间首地址
//对内核空间中每块地址进行遍历查找
for (;(unsigned long)entry < ULONG_MAX; entry += 1)
{
if (entry[__NR_close] == (unsigned long *)sys_close)
{
return entry;
}
}
return NULL;
}
/*******************************************************
函数名:void disable_write_protection(void)
参数表说明:无
返回值:无
函数功能描述:修改控制寄存器cr0,从而关闭写保护
********************************************************/
void disable_write_protection(void)
{
unsigned long cr0 = read_cr0();//读取cr0
clear_bit(16, &cr0);//清除第16位
write_cr0(cr0);//写回控制寄存器
}
/*******************************************************
函数名:void enable_write_protection(void)
参数表说明:无
返回值:无
函数功能描述:修改控制寄存器cr0,恢复写保护
********************************************************/
void enable_write_protection(void)
{
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
write_cr0(cr0);
}
/*******************************************************
函数名:static int __init init_addsyscall(void)
参数表说明:无
返回值:整型,状态
函数功能描述:初始化内核模块。获取系统调用表地址,并修改插入自己编写的系统调用函数
********************************************************/
static int __init init_addsyscall(void)
{
printk(KERN_EMERG"insmod!\n");
sys_call_table = (unsigned long *)get_sys_call_table();//获取系统调用表首地址
anything_saved = (int(*)(void)) (sys_call_table[my_syscall_num]);//保存原始系统调用的地址
disable_write_protection();//关闭写保护
sys_call_table[my_syscall_num] = (unsigned long)&sys_my_syscall;//更改原始的系统调用地址
enable_write_protection();//恢复写保护
return 0;
}
/*******************************************************
函数名:asmlinkage long sys_my_syscall(int flag, struct timeval __user *thetime)
参数表说明:flag为标志位,thetime返回当前时间
返回值:长整型
函数功能描述:从内核变量中读取当前时间并返回给用户,当标志位为真是,打印时间
********************************************************/
asmlinkage long sys_my_syscall(int flag, struct timeval __user *thetime)
{
if (likely(thetime != NULL))
{
struct timeval ktv;
struct timespec64 now;
getnstimeofday64(&now);
ktv.tv_sec = now.tv_sec;
ktv.tv_usec = now.tv_nsec/1000;
if (copy_to_user(thetime, &ktv, sizeof(ktv)))
return -EFAULT;
if (flag)
printk(KERN_EMERG"flag is true!\ntv.tv_sec: %ld\ntv.tv_usec: %ld\n", ktv.tv_sec, ktv.tv_usec);
}
printk(KERN_EMERG"This is my_syscall!\n");
return 0;
}
/*******************************************************
函数名:static void __exit exit_addsyscall(void)
参数表说明:无
返回值:无
函数功能描述:恢复系统调用表,退出模块
********************************************************/
static void __exit exit_addsyscall(void)
{
disable_write_protection();//关闭写保护
//恢复原有的sys_call_table的函数指针的值。
sys_call_table[my_syscall_num] = (unsigned long)anything_saved;
//恢复写保护
enable_write_protection();
printk(KERN_EMERG"rmmod!\n");
}
module_init(init_addsyscall);
module_exit(exit_addsyscall);
重新编译内核方式:
1.在include/linux/syscalls.h中加入函数声明:
asmlinkage long sys_my_syscall(int flag, struct timeval __user *thetime);
2.在kernel/sys_ni.c加入系统调用,处理失败的情况:
cond_syscall(sys_my_syscall);
3.在arch/x86/entry/syscalls/syscall_64.tbl(针对x86架构)中添加系统调用表项:
332 common my_syscall sys_my_syscall
4.在time.c中添加函数定义
插入内核模块进行测试:
编译内核前内核版本:
编译替换后:
在用户空间对调用进行测试:
问题1:编译一次内核所需时间太长,修改调试不方便
问题1解决方法:采取先在内核模块中完成测试功能后,再写入内核代码。
问题2:在编译内核时提示无配置文件,无法编译
问题2解决办法:把系统中原有的内核配置文件.config拷贝到新内核的目录下即可
问题3:查找系统调用表时,网上的例子是使用命令查看/proc文件来获取,再硬编码到程序中,这种方法不可靠,当重新开机后地址会改变
问题3解决方法:遍历内核空间每个地址即可,具体说明可见代码中函数get_sys_call_table的说明
问题4:网上例子中采取内联汇编的方式修改寄存器,在32位中有效,在64位系统中报错
问题4解决方法:查阅资料可知,内核中提供了修改寄存器的接口
问题5:在内核代码kernel文件夹下系统调用表无效
问题5解决方法:查阅文档可知,x86架构有其指定的系统调用表,在arch/x86下,kernel文件夹中为其他架构通用的系统调用表
本次实验涉及的是linux的系统调用。所谓系统调用,即操作系统为应用程序提供的与内核进行交互的一组接口。通过此接口,用户态下的应用程序可以转化为内核态,同时调用相应的内核函数运行,从而实现应用程序与内核的交互。
通过本次实验我们学会了如何向linux系统内核中添加自己编写的函数进行调用,明白了linux系统调用的过程,体会到了系统内核的重要性,计算机本身不具备文件写入和读出功能,这些功能由操作系统提供。操作系统是功能扩展机(虚拟机),它提供了许多系统调用,供程序员使用。要做好实验,需要按照实验教程上面的内容一步步落实,要边做边领悟相关原理及运行结果的出现的原因,这样我们才能在试验中学到更多、掌握更多。其次,也遇到问题我们自然是要先自己思考,通过不同的尝试来解决,之后不能解决的我们要多向老师同学请教,通过互相交流得来的知识也是会让我们难忘的。