操作系统实验二 内核编译
实验1:Linux内核代码分析
1. 解压内核
桌面上的linux-2.6.21.tar.gz是linux-2.6.21的内核代码压缩包,解压:
$ cd Desktop
$ tar zxvf linux-2.6.21.tar.gz
$ cd linux-2.6.21
2. 生成内核配置文件
将当前正在运行的内核对应的配置文件作为模板来生成.config文件,即将/boot目录下的已有的config文件复制到linux-2.6.21目录下
$ make mrproper
$ cp /boot/config-`uname -r` ./config
第一个命令make mrproper用来保证内核树是干净的,如果内核第一次编译则可以省略。其中的uname –r命令可查看当前环境下的内核版本号。
更新config文件:
$ make oldconfig
部分新配置项会提示用户选择,都选N或者缺省即可,完成后即可生成.config文件。
3. 编译安装内核
在编译内核前,可以定义自己的内核版本号,在内核代码的根目录下有Makefile文件,例如将第4行改为:
EXTRAVERSION = -seu
这样新内核版本号就是2.6.21-seu
$ make all
$ su
# make modules_install
# make install
# make headers_install
Make all的执行过程可能比较长。
如果三个命令均成功执行,可以观察引导程序grub的配置文件/boot/grub/menu.lst的内容,在hiddenmenu之后可以看到刚刚编译安装的内核版本,将hiddenmenu那一行注释或删除,方便直接操作菜单:
#hiddenmenu或者hiddenmenu
然后重启系统:
# reboot
重启后可以看到grub菜单已经包含了新编译的内核。如果新内核启动失败,一般是由于配置或者内核代码修改的有问题,选择原先的内核启动,再进行修改、编译。
实验2:新增系统调用
1.在文件arch/i386/kernel/syscall_table.S
的尾部加上要新增的系统调用函数的名称,如下图中添加了psta系统调用,注释中的320表示它的系统调用号
2.在include/linux目录下添加头文件psta.h:
#ifndef _LINUX_PSTA_H
#define _LINUX_PSTA_H
//#include <linux/types.h>
struct pinfo {
int nice;
pid_t pid;
uid_t uid;
};
#endif
修改include/linux目录下的Kbuild文件,把psta.h文件加进去。
header-y += x25.h
header-y += psta.h
unifdef-y += acct.h
在kernel目录下新建文件psta.c,在该文件中实现sys_psta函数:
#include <linux/linkage.h>
#include <linux/types.h>
#include <linux/psta.h>
#include <linux/kernel.h>
asmlinkage int sys_psta(struct pinfo *buf) {
printk("Hello world!\n");
return 0;
}
宏asmlinkage定义在linux/linkage.h中,表示函数的参数通过栈传递,而不是寄存器,所有的系统调用都遵循这种参数传递方式。在上面的函数返回之前,可以插入打印一句话到内核日志,方便后面验证执行结果。
printk("Hello world\n");
下面头文件,实现了printk。
#include <linux/kernel.h>
3. 修改文件kernel/Makefile,使得psta.c在编译时可见:
另外,第2步中psta的实现不一定要在一个新文件中,例如文件kernel/sys.c也许是添加这个系统调用的合适位置,这样的话第3步就不需要了。
4. 在include/asm-i386/unistd.h
里加上系统调用号的宏定义:
其中NR_syscalls表示的值应该是最大的系统调用号加一。
5. 修改include/linux/syscalls.h
,加上函数sys_psta的声明。
先在头上加入结构体申明struct pinfo; 在该文件的头文件引用列表的末尾添加:
这算是pinfo结构体的实现。在函数申明列表的最后添加:
6. 重新编译内核
在清理上次编译生成的中间文件之前,最好将配置好的.config文件备份至别的目录下以防删除,否则必须再执行实验1中的第2步配置.config文件。
$ make mrproper
在实验1中就已提到该命令,用于清理编译内核代码的中间文件,它也会删除.config文件。在清理执行完成后,将备份的.config文件复制回来。然后执行实验1中的第3步就可以编译安装新内核了。在这个过程里面可以换一个内核版本名称,以示区别,比如EXTRAVERSION = -seu2。安装好的内核必须reboot后才能生效。
以上6步成功执行后系统调用psta就已被添加到系统中,下面来进行测试。
首先,可以用uname -r命令测试目前所使用的内核版本号。
然后,随便写一个test.c文件,在里面使用glic库提供的函数syscall来间接地使用新系统调用,调用syscall必须添加以下两个头文件,以及头文件#include <linux/psta.h>。
函数的原型如下图,syscall的第一个参数是系统调用号,后面的参数是该系统调用的各个参数,返回值就是系统调用的返回值。例如函数psta的系统调用号是320,且接受一个类型为struct pinfo *的参数。
具体的测试代码请大家根据自己实现的psta来编写,只要体现出调用了psta函数即可。
编译的时候可以用命令 gcc -o test test.c -I/home/seu/Desktop/linux-2.6.21/usr/include。
关于为什么这里编译用户态程序的时候还需要用 –I 参数强行指定内核头文件路径,请查考实验末尾的文章。如果有需要的话,可以自己修改kernel headers文件的存放位置。
最后,系统调用执行成功,用dmesg命令可以再内核消息列表的最后看到hello world。
实验3:Linux进程管理及其扩展
1.实现系统调用hide
1.1. 在include/linux/sched.h中修改task_struct,添加一个成员cloak,用来记录进程隐藏与否。
1.2. 在进程创建时,将task_struct的成员cloak初始化为未隐藏。fork系统调用的实现代码在kernel/fork.c中,具体实现的主要函数为do_fork,do_fork中调用copy_process函数创建子进程,建议将初始化cloak的代码添加在copy_process函数中:
1.3. 添加hide系统调用
#include <linux/types.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/proc_fs.h>
asmlinkage int sys_hide(pid_t pid, int on)
{
struct task_struct *p = NULL;
if(pid>0 && (current->uid)==0)
{
p = find_task_by_pid(pid);
p->cloak=on;
if(1==on){
printk("Process %d is hidden by root.\n", pid);
}
if(0==on){
printk("Process %d is displayed by root.\n", pid);
}
proc_flush_task(p);
}
else
printk("Permission denied.\n");
return 0;
}
1.4. 修改proc_pid_readdir函数(在fs/proc/base.c文件中)
其中使用for循环遍历进程,在遍历过程中添加判断,过滤掉被隐藏的进程:
1.5. 修改proc_pid_lookup函数
在进程查找完成前过滤掉被隐藏的进程。
效果展示
隐藏进程号4000后发现ps找不到进程号4000的进程
2.考虑权限问题,只有根用户才能隐藏进程。
第一次由seu用户执行,操作被拒绝
第二次由root用户执行,隐藏成功
3.实现系统调用hide_user_processes
#include<linux/linkage.h>
#include<linux/types.h>
#include<linux/sched.h>
#include<linux/pid.h>
#include<linux/proc_fs.h>
#include<linux/string.h>
asmlinkage int sys_hide_user_processes(uid_t uid,char *binname,int recover){
struct task_struct *p=NULL;
if(recover==0)
{
if(current->uid==0)//1.Paragram recover=0;2.root => you can hide the process
{
if(binname==NULL)//when null, hide all the processes of the corresponding user
{
for_each_process(p)
{
if((p->uid)==uid)
{
p->cloak=1;
proc_flush_task(p);
}
}
printk("All of the processes of uid %d are hidden.\n",uid);
}
else//otherwise, hide the process of the corresponding name
{
for_each_process(p)
{
char *s=p->comm;
if(strcmp(s,binname)==0 && p->uid==uid)
{
p->cloak=1;
printk("Process %s of uid %d is hidden.\n",binname,uid);
proc_flush_task(p);
}
}
}
}
else
printk("Sorry, you are not root user. Permission denied.\n");
}
else if(recover != 0 && (current->uid)==0)//display all of the processes, including which are hidden
{
for_each_process(p)
{
p->cloak=0;
}
}
return 0;
}
效果展示
运行后成功将所有由uid=500的用户创建的进程隐藏
4.hidden文件
4.1. 在/fs/proc/proc_misc.c中添加回调函数
4.2. 在/fs/proc/proc_misc.c中proc_misc_init函数的最后添加创建hidden文件的代码,并指定其回调函数。
4.3. hidden文件创建成功后,需要实现通过全局变量hidden_flag来约束隐藏进程的函数。
根据题目:当hidden_flag为1时,此前通过hide调用要求隐藏的进程才被隐藏。
这就意味着修改hidden_flag不可以修改cloak,即之前使用hide和hide_user_process隐藏的进程需要在hidden_flag置为1之后保持隐藏。
所以,在proc_pid_readdir函数和proc_pid_lookup函数中判断cloak值之前添加对hidden_flag的判断就可以实现约束。
效果展示
重新编译后hidden默认为1(开启隐藏功能)
将hidden改为0,此时之前被隐藏的进程都可以重新看到了
- hidden_process文件
方法和创建hidden文件一样,hidden_process文件只需要设置读的回调函数即可。