一.实验目的
(1)学习怎样重新编译 Linux 内核
(2)理解、掌握 Linux 标准内核和发行版本内核的区别
二.实验内容
1、通过重新编译Linux来实现系统调用
2、通过增加模块来实现系统调用
三.实验步骤和结果
(一)通过重新编译内核进行系统调用
1.获取root权限(即管理员权限)
在终端输入下列指令:sudo su
注意:获取root权限需要用户输入密码(登陆密码),密码是看不见的,用户只需正确的输入密码即可,回车键结束
示意图如下:
2.准备工作
(1)安装相关编译程序
sudo apt-get install build-essential kernel-package libncurses5-dev
选择第一项:
(2)从外面将linux-5.5.8.tar拖入到了虚拟机桌面,将该压缩文件移入到/usr/src/文件夹中
3.进入/usr/src解压文件
cd /usr/src
sudo tar -xvf /usr/src/linux-5.5.8.tar.xz
解压完成!
4.进入解压出的文件目录
cd /usr/src/linux-5.5.8/kernel
5.安装vim
sudo apt-get install vim
6.打开sys.c加入函数
vim sys.c
在vim中, i进入编辑, esc退出编辑状态. G跳到末尾, gg进入开头。 :wq保存退出, :q不保存退出
在末尾加入函数
asmlinkage long sys_helloworld(void){
printk( "helloworld!");
return 1;
}
7.添加声明
cd /usr/src/linux-5.5.8/arch/x86/include/asm/
vim syscalls.h
(插入asmlinkage long sys_helloworld(void);)
8.加一个系统调用的id
a、使用命令cd /usr/src/linux-5.5.8/arch/x86/entry/syscalls,进入/usr/src/linux-5.5.8/arch/x86/syscalls目录
b、使用命令vim syscall_64.tbl,打开文件syscall_64.tbl(该文件有一个系统调用列表,最前面的属性是id)
c、在里面添加自己的系统调用号(335 64 helloworld sys_helloworld)
d、使用esc +:wq命令保存退出
9.配置内核
cd /usr/src/linux-5.5.8
编译内核和安装内核。
依次输入这四条语句
sudo make mrproper
sudo make clean
sudo make menuconfig (并且在make menuconfig时,将那个General setup内的localversion修改成新的名称,比如我这里的myKernel)
根据自己处理器的最大线程数目来编译。
sudo make -j4 (我的电脑是4核4线程),线程越多编译越快!
配置内核界面:
开始编译,过程耗时两小时左右。
编译成功!
10、安装内核
编译后安装内核到系统中
sudo make modules_install //安装模块
sudo make install // 安装内核
模块安装完成!
内核安装完成!
11、重启虚拟机
将之前的工作保存后直接重启,重启后点击鼠标进入ubuntu并且迅速按住shift,长按!选择新内核。
12、验证系统调用是否成功
a、登陆虚拟机
b、用文本编辑器编辑一段代码hello.c,并保存至根目录下。
c、输入下列指令:
(1)gcc hello.c
(2)./a.out
返回值为1,系统调用实现成功!
(二)通过模块修改系统调用
1、在/usr/include/x86_64-linux-gnu/asm/unistd_64.h文件中查看系统调用序号
发现333号系统调用是空的,因此选取333作为新的系统调用号。
2、查看系统调用表的内存地址:为0xffffffffa6c002a0
3、编写模块文件:
module5.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/sched.h>
MODULE_LICENSE("Dual BSD/GPL");
#define SYS_CALL_TABLE_ADDRESS 0xffffffffb18002a0 //sys_call_table对应的地址
#define NUM 333 //系统调用号为333
int orig_cr0; //用来存储cr0寄存器原来的值
unsigned long *sys_call_table_my=0;
static int(*anything_saved)(void); //定义一个函数指针,用来保存一个系统调用
static int clear_cr0(void) //使cr0寄存器的第17位设置为0(内核空间可写)
{
unsigned int cr0=0;
unsigned int ret;
asm volatile("movq %%cr0,%%eax":"=a"(cr0));//将cr0寄存器的值移动到eax寄存器中,同时输出到cr0变量中
ret=cr0;
cr0&=0xfffeffff;//将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器
asm volatile("movq %%eax,%%cr0"::"a"(cr0));//将cr0变量的值作为输入,输入到寄存器eax中,同时移动到寄存器cr0中
return ret;
}
static void setback_cr0(int val) //使cr0寄存器设置为内核不可写
{
asm volatile("movq %%eax,%%cr0"::"a"(val));
}
asmlinkage long sys_mycall(void) //定义自己的系统调用
{
printk("模块系统调用-当前pid:%d,当前comm:%s\n",current->pid,current->comm);
return current->pid;
}
static int __init call_init(void)
{
sys_call_table_my=(unsigned long*)(SYS_CALL_TABLE_ADDRESS);
printk("call_init......\n");
anything_saved=(int(*)(void))(sys_call_table_my[NUM]);//保存系统调用表中的NUM位置上的系统调用
orig_cr0=clear_cr0();//使内核地址空间可写
sys_call_table_my[NUM]=(unsigned long) &sys_mycall;//用自己的系统调用替换NUM位置上的系统调用
setback_cr0(orig_cr0);//使内核地址空间不可写
return 0;
}
static void __exit call_exit(void)
{
printk("call_exit......\n");
orig_cr0=clear_cr0();
sys_call_table_my[NUM]=(unsigned long)anything_saved;//将系统调用恢复
setback_cr0(orig_cr0);
}
module_init(call_init);
module_exit(call_exit);
MODULE_AUTHOR("25");
MODULE_VERSION("BETA 1.0");
MODULE_DESCRIPTION("a module for replace a syscall");
Makefile
4、执行make命令
5、载入模块,并用lsmod命令查看模块是否被加载
6、编写测试程序:
test.c
7、编译test.c,执行a.out,输出Hello,3011,系统调用成功!
四.实验问题分析
1、第一次编译就报了下面的错误,后来我想了想,可能是在添加系统调用id的时候出了错。因为当时添加系统调用的id的时候,我看到syscall_64.tbl里面的函数名都是__x64_sys_开头,所以误以为所有的函数名都要以这个格式来命名,现在弄明白了,不用管系统里的其他函数是什么样的,这里的函数名必须与sys.c里添加的函数以及syscall.h里的函数声明保持严格的一致,否则会报错。
2、第二次重新编译,前面的过程很顺利,最后一步进行系统调用的时候又出现了问题,返回值为-1,我通过上网查阅相关资料推测应该是系统调用找不到的原因。我又重新检查了一遍,才发现自己犯了粗心大意的错误,原来,最后的hello.c程序我在写的时候误将syscall(335)写成了syscall(333),系统调用的id错了,自然就找不到了。改过来之后,返回值为1,系统调用成功。
3、第二个小实验其实也并没有多难,但是做的过程并非一帆风顺,前前后后重做了十几次,因为每次加载模块的时候,都显示被杀死,而我想卸载的时候,又显示模块正在被使用,卸载不了,我通过lsmod查看时发现系统中有module5的存在,因此我推测,这个模块应该可以理解为被杀死的模块的尸体。想要卸载掉这个死模块,只能重启。
4、其中第一个问题是,查看系统调用序号的时候,我一开始是在unistd_32.h里查看的,然后找了个223来用,但后来发现,应该在unistd_64.h里找。
5、第二个问题也是实验成功之前一直困扰我一个小细节。就是每次重启之后,我一直都用的第一次查出来的系统调用表的内存地址,以为这个地址是恒定不变的。后来通过上网查资料,问同学才最终发现内存地址每次重启都会更新,这个才是导致我的模块一直被杀死的重要原因。
五.实验总结
本次实验,我学会了通过重新编译内核和修改模块两种方法来实现Linux系统调用,通过修改模块的方法来修改系统调用,更省时,也更能体现Linux内核的模块作用。在实验过程中我遇到了很多困难,但是在一步步的解决这些问题的同时,也在进一步的加深我对Linux系统调用的理解。