通过编译linux内核增加具有拷贝功能的系统调用

摘要:

通过编译linux内核的方式,增加一个新的系统调用。另外编写一个应用程序来测试和使用新增加的系统调用。其中,新增加的系统调用具有文件拷贝功能。

引入

我们都知道,很多我们习以为常的C语言标准函数,在linux平台的实现都是通过系统调用完成的,例如我们在用户程序中使用open()或者write()函数时,函数执行过程中会通过中断进入到系统内核中,在Intel CPU中,这个由中断INT0x80实现。跳转到的内核位置叫做sysem_call。检查系统调用号,这个号码代表进程请求哪种服务。然后,它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。接着,就调用内核函数来实现open()或者write()等函数的功能。我们要做的,就是在内核中增加一个内核函数,相应地也在系统调用表中增加一个表项,使得我们增加的功能永远的留存在系统中。

系统说明

我使用的是电脑是macbook pro,做这个实验用的是vmware中的ubuntu18.04.3虚拟机。

正式开始

首先我们要进行一些准备工作,安装相关依赖。打开终端输入下列命令。

1
2
3
4
5
6
7
8
9
10
sudo apt-get update
sudo apt-get install libncurses5-dev libssl-dev
sudo apt-get install build-essential openssl
sudo apt-get install zlibc minizip
sudo apt-get install libidn11-dev libidn11
sudo apt-get install bison
sudo apt-get install flex
————————————————
版权声明:本文为CSDN博主「Snoopy是个小机灵」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/snoopy19981210/article/details/89957254

之后,我们并不会直接编译我们现在使用的ubuntu的linux内核,因为有可能会把系统搞崩代价太大,所以采取的方法是单独下载一个linux内核,我是从清华的镜像网站下载的,下载的linux内核版本为4.18.3。

下载之后会得到一个压缩包,解压。

之后用sudo mv linux-4.18.3 /usr/src命令,将该文件夹移动至/usr/src。

然后找到/usr/src/linux-4.18.3/arch/x86/entry/syscalls/syscall_64.tbl,我的理解是,syscall_64.tbl就是系统调用表,程序运行时,内核会通过这个表找到系统调用程序的位置。我们打开这个文件,可以看到每行第一列都有一个编号,第三列是系统调用的名字。我们在编号为334的一行后面新加一行,编号为335,第二列为common,第三列为新增系统调用的名字,我命名为mycall,第四列为__x64_sys_mycall。

之后找到/usr/src/linux-4.18.3/include/linux/syscalls.h,从文件名字就能看出来,这里面的都是函数的声明,我们在文件的最后,#endif之前,添加我们自定义的函数的声明.

1
asmlinkage long sys_mycall(char *src, char *dest);

最前面的asmlinkage表示函数参数将存放在局部栈中,系统调用大多都采取这种方式。然后因为我们要实现的是文件拷贝功能,所以参数列表中是源文件的文件名和目标文件的文件名。

之后,找到/usr/src/linux-4.18.3/kernel/sys.c,我们将新增加的函数的定义添加在文件的末尾,#endif之前。

接下来需要重点说一下函数定义该怎么写,即拷贝系统调用如何实现。

首先需要注意的是,在内核中,我们只能使用内核函数,比如open()、write()函数都不能用了,需要用ksys_open()和ksys_write()。

ksys_open()函数的原型为

1
asmlinkage long ksys_open(const char __user *filename, int flags, int mode);

flags定义文件打开方式,O_RDONLY表示以只读方式打开,O_WRONLY表示可读可写方式打开,O_CREAT表示要打开的文件不存在时自动创建文件。

mode定义权限,S_IRUSR表示允许用户读文件,S_IWUSR表示允许用户写文件。

下面的函数名和参数列表肯定会让你疑惑,往下看,代码后面会解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SYSCALL_DEFINE2(mycall,char *,src,char *,dest){
int fs,fd,len;
char buffer[50];
mm_segment_t fss;
fss = get_fs();
set_fs(KERNEL_DS);
fs=ksys_open(src,O_RDONLY,0);
if(fs==-1){
printk("source file error,maybe it doesn't exist.\n");
return -1;
}
fd=ksys_open(dest,O_CREAT|O_WRONLY|O_TRUNC,S_IRUSR|S_IWUSR);
if(fd==-1){
printk("fail to create destination.\n");
return -1;
}
while((len=ksys_read(fs,buffer,50))>0)
ksys_write(fd,buffer,len);
ksys_close(fs);
ksys_close(fd);
set_fs(fss);
return 0;
}

需要注意,函数名我们用的是SYSCALL_DEFINE2,并且本来应该是参数应该是char * sourceFile,却被我写成了char *, sourceFile,打开sys.c文件我们会发现,许多函数都是这样写的,如果不想去深究,我们只需要知道2代表着参数列表中参数的个数,参数列表中变量类型与变量名之间要加逗号,如果想仔细探究下,可以参考这篇文章

接下来进行编译内核前的准备工作,终端输入下列指令。其中第二条指令用来清除编译过程中所有的中间文件,第三条指令用于清除上一次编译的中间文件,第三条指令用于生成.config文件。

1
2
3
4
cd /usr/src/linux-4.18.3
sudo make mrproper
sudo make clean
sudo make menuconfig

在出现的界面中不需要任何操作直接exit。

接下来开始编译内核,输入指令

1
sudo make

输入指令后,内核就开始编译了,这个过程会比较久。如果出了错,重新编译前应该重新输入一次上面的sudo make mrproper、sudo make clean和sudo make menuconfig命令。

编译完成后,生成了新的内核,我们安装新生成的内核。输入指令

1
2
3
sudo make modules_install
sudo make install
sudo update-grub

完成之后,我们的新内核就算是编译好了,接下来进行测试。

重启虚拟机,启动时同时按住esc和shift,在弹出的界面选择ubuntu高级选项,选择新编译的那个内核来启动系统。

启动后,执行下面的测试程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <linux/unistd.h>
#include <asm/unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
int i;
i = syscall(335,argv[1],argv[2]);
if(i == 0)
printf("Success!\n");
else
printf("i = %d", i);
return 0;
}

如果程序输出“Success!”而且源文件内容被成功复制到目标文件,那么任务就完成了。

参考资料

  1. https://blog.csdn.net/snoopy19981210/article/details/89957254
  2. https://blog.csdn.net/qq84395064/article/details/86593469
  3. https://blog.csdn.net/hxmhyp/article/details/22699669