本文为本人大三下学期linux实验课程所写,如有不妥之处欢迎前辈们批评指正
系统:ubuntu20.04
在开始实验之前请确保你的ubantu分区大小达到50G以上,可用空间在25G以上。
(1)查看系统内核版本
开发之前首先得确定系统的内核版本是哪一个,可以通过apt-cache search linux-source来查看。
(2)安装内核版本源码
可以使用apt-get install linux-source-5.4.0下载相应版本的内核源码,下载后被存放在/usr/src下。
在/usr/src下增加了两个文件
使用sudo tar jxvf linux-source-5.4.0.tar.bz2进行解压
(3)修改注册表
修改/user/src/linux-source-5.4.0/arch/x86/entry/syscalls路径下的syscall_64.tbl文件,添加如下系统调用编号。其中<436为增加的系统调用号,jkbcall为调用函数名,64为位数>
(4)添加系统调用头文件
修改/user/src/linux-source-5.4.0/include/linux下的syscalls.h文件,添加上自己的系统调用函数的声明。
(5)实现系统调用函数
修改/user/src/linux-source-5.4.0/kernel下的sys.c文件,添加上自己的系统调用函数的声明。
(6)编译内核文件
①安装编译内核所需要的包
②正式编译内核代码
在完成了相关编译包的安装后,接下来就可以正式开始进行编译了。首先第一步要完成的是清除旧的编译内容,即使用sudo make mrproper命令。
第二步要做的就是删除原先编译产生的.o文件,即使用sudo make clean命令,clean之后就可以进行内核配置文件的编译了,即sudo make oldconfig。
第三步要做的就是真正的编译了,使用sudo make -jn命令,其中n参数代表使用几个内核参与编译。虚拟机的内核数量可以通过在虚拟机的设置里面选择使用几个内核。内核的数目越多,编译的速度就会越快,但是内核的数目取决于电脑的主机的硬件,我给ubantu分配的是6个核。
编译过程中需要如下问题:
这个问题的解决方案为修改.config文件,使用sudo gedit .config打开文件Ctrl F 查找“canonical”找到下图位置将这两个“”中原本的debian/canoncial-certs.pem清除。注意:图中9937行的“”中的不要清除,否则后续操作会出现问题。
并且修改此文件需在 sudo make clean ;sudo make clean;sudo make oldconfig这三条指令之后执行,不然修改过的.config文件又会复原
运行了五十分钟后再次报错
原因为缺少一个软件包,所以前面一定要把软件包安装完全,如果是按照我前面步骤来的应该不会遇到这个问题因为我已经把这个包的安装添加在前面了
安装完包之后重复②中内容重新编译,很气,跑了一个来小时,把我给ubantu分配的40G给吃完了,告诉我磁盘空间不够用,梦中惊坐起,重装系统,从头来过。分配了八个核,跑了五十多分钟接近一个小时,终于完成了编译(经过亲身实践编译过程需要20G,在开始编译前最好保证可用空间超过25G)
第四步就是将编译好的模块进行安装,采用sudo make modules_install命令。
第五步就是安装内核了,采用sudo make install命令。
$ make install 该命令的作用是将.config,vmlinuz,initrd.img,System.map文件到/boot/目录、更新grub,检查/boot/grub/grub.cfg,menuentry中,5.4.189的内核版本已生成
配置启动项$ sudo gedit /etc/default/grub, 将GRUB_TIMEOUT = 0 注释掉(这个参数如果设为一个常数,则将在此时间内隐藏菜单而显示引导画面。菜单将会被隐藏),下次启动Ubuntu,bootloader会提示选择哪一版内核进行启动。
如果不行采用下面的方法(本人按照网上讲的那些修改GRUB_TIMEOUT都没有用)
在reboot虚拟机之后在启动的过程中长按shift键,进入到BootLoader选择界面,选择ubantu的高级设置,回车
之后选择自己刚刚装载的内核版本然后回车
进去之后打开终端,实验uname -a查看内核版本
(7)测试自增系统调用
自己添加的系统调用号为436号,所以只需要调用syscall函数调用自己创建的系统调用即可。
编译并运行测试文件
因为系统调用的printk不会打印在基本输出上,所以我们在sys.c中实现的printk效果不会打印出来,但是我们可以使用sudo dmesg查看内核态打印的信息。
(8)模块添加法添加系统调用
第一步在/lib/modules/5.4.189/build/arch/x86/include/generated/uapi/asm/unistd_64.h(5.4.149可以通过uname -r查得,不同内核都不一样)修改系统调用号,在最后加入__NR_hhrcall_Core,436号为内核编译法自动添加进去的,335号为本次模块添加法插入(此调用号不宜过大,可用查看/user/src/linux-source-5.4.0/arch/x86/entry/syscalls路径下的syscall_64.tbl文件,有哪些调用号是可用的)
第二步查找sys_call_tabel的系统内存地址。
方法一 使用/proc/kallsyms:kallsyms包含了kernel image和动态加载模块的符号表,包括内核中的函数符号(包括没有EXPORT_SYMBOL导出的符号)、全局变量(用EXPORT_SYMBOL导出的全局变量),函数如果被编译器内联(inline)或优化掉,则它在/proc/kallsyms有可能找不到。此方法由变量名获取虚拟地址使用如下命令:
方法二 使用System.map:System.map是一份内核符号表kernel symbol table,包含了内核中的变量名和函数名地址,在每次编译内核时,自动生成。由变量名获取虚拟地址使用如下命令
第三步创建内核链接文件core.c
需要注意到的是__NR_syscall这一个宏定义的内容必须与上述unistd_64.h中新添的系统调用号保持一致,因为其采用的是索引查表的逻辑进行的。如果不一致就无法定位到准确的系统调用。如果想要修改系统调用函数,修改sys_mucall()文件即可,在__init__addsyscall()函数中有自定义系统调用函数与代码段地址的绑定。core.c文件内容如下:
第四步编写Makefile文件
编写Makefile文件需要注意的是目标文件的名字必须与.c名字一致,这样才能指导make的自动补充依赖编译的功能。并且更改文件权限
创建完成后我将他们全都放入到桌面下的core目录里面
第五步编译并插入模块
使用make命令进行编译,起初遇到了一点问题
插入模块
第六步编写、编译并执行目标文件
编写jkb.c进行测试
编译并运行
查看内核打印信息,使用dmesg命令
至此本文已经结束。
附core.c源码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/unistd.h>
#include <linux/time.h>
#include <asm/uaccess.h>
#include <linux/sched.h>
#include <linux/kallsyms.h>
#define __NR_syscall 335 /* 系统调用号335 */
unsigned long * sys_call_table;
unsigned int clear_and_return_cr0(void);
void setback_cr0(unsigned int val);
static int sys_mycall(void);
int orig_cr0; /* 用来存储cr0寄存器原来的值 */
unsigned long *sys_call_table = 0;
static int (*anything_saved)(void); /*定义一个函数指针,用来保存一个系统调用*/
/*
* 设置cr0寄存器的第17位为0
*/
unsigned int clear_and_return_cr0(void)
{
unsigned int cr0 = 0;
unsigned int ret;
/* 前者用在32位系统。后者用在64位系统,本系统64位 */
//asm volatile ("movl %%cr0, %%eax" : "=a"(cr0));
asm volatile ("movq %%cr0, %%rax" : "=a"(cr0)); /* 将cr0寄存器的值移动到rax寄存器中,同时输出到cr0变量中 */
ret = cr0;
cr0 &= 0xfffeffff; /* 将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器 */
//asm volatile ("movl %%eax, %%cr0" :: "a"(cr0));
asm volatile ("movq %%rax, %%cr0" :: "a"(cr0)); /* 读取cr0的值到rax寄存器,再将rax寄存器的值放入cr0中 */
return ret;
}
/* 读取val的值到rax寄存器,再将rax寄存器的值放入cr0中 */
void setback_cr0(unsigned int val)
{
//asm volatile ("movl %%eax, %%cr0" :: "a"(val));
asm volatile ("movq %%rax, %%cr0" :: "a"(val));
}
/* 添加自己的系统调用函数 */
static int sys_mycall(void)
{
int ret = 12345;
printk("My syscall is successful!\n");
return ret;
}
/*模块的初始化函数,模块的入口函数,加载模块*/
static int __init init_addsyscall(void)
{
printk("My syscall is starting。。。\n");
sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); /* 获取系统调用服务首地址 */
printk("sys_call_table: 0x%p\n", sys_call_table);
anything_saved = (int(*)(void))(sys_call_table[__NR_syscall]); /* 保存原始系统调用 */
orig_cr0 = clear_and_return_cr0(); /* 设置cr0可更改 */
sys_call_table[__NR_syscall] = (unsigned long)&sys_mycall; /* 更改原始的系统调用服务地址 */
setback_cr0(orig_cr0); /* 设置为原始的只读cr0 */
return 0;
}
/*出口函数,卸载模块*/
static void __exit exit_addsyscall(void)
{
orig_cr0 = clear_and_return_cr0(); /* 设置cr0中对sys_call_table的更改权限 */
sys_call_table[__NR_syscall] = (unsigned long)anything_saved; /* 设置cr0可更改 */
setback_cr0(orig_cr0); /* 恢复原有的中断向量表中的函数指针的值 */
printk("My syscall exit....\n"); /* 恢复原有的cr0的值 */
}
module_init(init_addsyscall);
module_exit(exit_addsyscall);
MODULE_LICENSE("GPL");