0%

如何编译操作系统内核,并新增系统调用💡

实验内容

已经获得了linux内核源代码。通过修改源代码,创建一个新的编号为320的系统调用,具体功能由自己指定。接着生成内核配置文件,并编译安装内核。最终使得开机时能够选择启动自己编译的内核,并成功调用新增的系统调用。

实验目的

通过编写内核代码的方式,加深自己对操作系统内核的理解,使其概念变得不那么抽象。同时感受内核中系统调用的运作方式。

设计思路以及流程图

新增系统调用

打开虚拟机,在桌面发现linux内核压缩文件,首先将其解压:

1
2
3
$ cd Desktop
$ tar zxvf linux-2.6.21.tar.gz
$ cd linux-2.6.21

接着可以看到解压好的内核。

我们开始尝试新增一个320号系统调用。

打开终端,cd到内核目录,使用vi编辑arch/i386/kernel/syscall_table.S,在尾部加上psta系统调用。

然后我们在include/linux目录下添加psta.h头文件:

其中nice是标志符,当其为0时代表没有调用,当成功调用该syscall时将其置为1。

接着,修改include/linux目录下的Kbuild文件,将psta.h添加进去:

在kernel目录下新建文件psta.c,在该文件中实现sys_psta函数:

这是我自己写的psta.c,其内容将在后文分析。

修改文件kernel/Makefile,使得psta.c在编译时可见:

在include/asm-i386/unistd.h里加上系统调用号的宏定义:

修改include/linux/syscalls.h,加上函数sys_psta的声明。

声明pinfo结构体:

添加psta.h头文件:

末尾添加函数声明:

重新编译内核

生成内核配置文件:

1
2
$ make mrproper
$ cp /boot/config-`uname -r` ./config

make mrproper保证内核树是干净的,这会删除.config文件,然后通过第二条命令将当前运行的内核config文件复制到自己编写的内核目录下。其中``符号代表bash执行返回结果,也就是说实际上返回的版本号拼接上config-就是config文件名。

Linux采用了双树系统。一个树是稳定树(stable tree),另一个树是非稳定树(unstable tree)或者开发树(development tree)。一些新特性、实验性改进等都将首先在开发树中进行。如果在开发树中所做的改进也可以应用于稳定树,那么在开发树中经过测试以后,在稳定树中将进行相同的改进。一旦开发树经过了足够的发展,开发树就会成为新的稳定树。

参考链接:https://blog.csdn.net/zhenguo26/article/details/79641322

接着更新config文件:

1
$ make oldconfig

更改Makefile文件,定义自己的内核版本号,我将其设置为-seu。

1
EXTRAVERSION = -seu

最后编译安装内核:

1
2
3
4
5
$ make all
$ su
$ make modules_install
$ make install
$ make headers_install

重启系统:

1
$ reboot

可以发现启动页面以及有了我们编译的新内核。

打开终端,执行uname -r命令,可以验证我们的内核版本确实已经切换。

这时候开始验证我们的代码是否起能成功调用我们写的320号system call。

源程序以及注释

以下是psta.c的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/kernel.h>
#include <linux/linkage.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <linux/psta.h>

asmlinkage int sys_psta(struct pinfo *buf) {

//nice=1 represents system call is successfully invoked
buf->nice = 1;
printk("This system call is successfully running!\n");

//print the pid into system log
printk("The pid of the process that invoked this system call is %d\n",buf->pid);

//print the uid into system log
printk("The uid of the user that invoked this system call is %d\n",buf->uid);
return 0;
}

可以看到在320号system call中,将传入的pinfo结构体的nice变量赋值为1,代表成功调用了。并在内核日志中打印调用该system call进程的pid以及用户的uid。下面继续来看测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<sys/syscall.h>
#include<unistd.h>
#include<stdio.h>
#include<linux/psta.h>

int main()
{
printf("The pid of current process is %d\n",getpid());
printf("The current uid is %d\n",getuid());
struct pinfo info;

//initialize variable nice with 0
info.nice = 0;

//obtain the pid and uid
info.pid = getpid();
info.uid = getuid();

//if the system call is successfully invoked, the return value of ret will be 0
int ret = syscall(320,&info);
printf("0 represents this system call is successfully invoked,ret:%d\n",ret);
printf("The system call change the value of nice into:%d\n",info.nice);
return 0;
}

也就是说,我们的预期结果是:

  • nice变量的值通过系统调用变成了1
  • ret变量返回0,代表成功调用320号system call,否则返回-1
  • 内核日志中打印出的pid与uid与程序输出一致

程序运行结果

下面是运行结果:

可以看出:

  • 当权限为用户时,当前uid是500,程序的进程号为3951
  • 当权限为管理员时,当前uid是0,程序的进程号是3971
  • 两次的nice值均改变为1
  • 两次的ret值均返回0

再来看内核日志:

内核日志与程序输出结果相符,实验成功。

实验体会

这次操作系统实验是我第一次在内核态编程,过程很繁琐复杂,但同时也受益匪浅。在实验的过程中,我遇到了很多的bug,以及各种奇怪的编译错误,甚至虚拟机无法启动得推倒重来。即便如此,最终还是通过百度谷歌各种方式一一解决了bug。下面是我的个人的一些体悟。

关于内核

我觉得内核是连接用户程序与硬件的一个桥梁,在用户态写代码时,我们只能调用内核给定好的system call进行操作。而各种system call的功能是相对安全的,用户不能直接对底层硬件进行操作,因此内核一个很重要的功能就是让用户的操作更加规范且安全,这是一个保护机制。

linux是开源的,我们可以在操作规范的前提下编写自己的内核,打开内核目录,可以看到各式各样的.h以及.c文件,这些都是最底层的系统api,因此编写的时候必须十分谨慎,稍有不慎就可能导致系统崩溃。

同时,编译内核也是一个繁琐的过程,每一次更改自己编写的系统调用,make all都需要等待相当长的时间,因为编译器需要对整一个内核进行重新编译、链接。

内核态和用户态

在编写system call的过程中,我有许多想实现的功能没法实现,经过百度,我发现这是因为内核态和用户态的不同而导致的。

例如,在psta.c中,开始我希望能够获取当前进程的pid,并将其存放在pinfo的pid变量中。我尝试使用getpid(),但编译提示说找不到头文件。原来,linux下的C库在内核态下是无法使用的,这些C库包含的是已经封装好system call的函数,只能在用户态下使用。而内核态显然是更底层的,因此在内核态无法使用用户态的C库和各种函数。而如果想要在内核态中使用用户态的某些功能,就必须找到内核中对应的头文件和函数。在linux内核中有一个宏current,current->pid即指向当前的pid。为此我尝试了使用current,但是失败了,最后返回的结果是一个负数,但我始终没能找出原因。

再例如,在内核程序中我们没法通过include<stdio.h>来使用printf函数,取而代之的是printk,这个函数可以将消息打印在内核日志中。

写在最后

Linux内核是一个复杂而精细的东西,在使用修改的过程中必须要十分小心,但这也是Linux内核的魅力所在。有次在编译的过程中,由于没有重新生成内核配置文件就编译,出了很多莫名其妙的错误,到最后才发现。Linux的维护者在编写内核代码时想必也是十分小心的,我想,如果底层api中出现了漏洞,那么黑客就很可能会通过编写恶意的漏洞利用程序来进行攻击。因此,阅读内核代码,可以体会到Linux维护者是如何编写高效而安全的程序的。自己编写系统调用,在确保安全的前提下,可以提升系统中某些功能的运行效率,若想实现某一功能,只需调用我们自己编写的system call,而不用通过用户态函数。这样效率将极大提升。