Xudifsd Rational life

给内核添加系统调用

2012-11-16
xudifsd

注意:本文所使用的系统为ubuntu12.04,使用i386体系,如果使用不同系统及体系很多地方需要修改,另外在安装新内核后请保持至少一个可用的旧内核,以防止新内核无法使用。

一直想直接给内核添加一些基本的东西作为内核开发的下手点,于是就想到了给内核添加一个新的系统调用。这样的添加非常简单而且可以直接通过应用程序看是否成功,前些天就着《Linux Kernel Development》给内核加了个系统调用,没想到一次就成功了。

系统调用

应用程序都是通过系统调用来取得操作系统提供的服务,但是除了汇编之外,这些系统调用在所有语言里都是对程序员不可见的,也就是说你不知道调用的是一个普通函数还是系统调用。

先从开始用汇编进行系统调用来了解整个过程吧。
把以下代码保存在t1.s中:

.section .data
word:
        .ascii "Hello from assembly\n"
word_end:
.equ len, word_end - word	#取得字符串长度

.section .text
.global _start	#程序起点
_start:
        movl $4, %eax	#write系统调用的调用号
        movl $1, %ebx	#stdout
        movl $word, %ecx	#输出字符串的地址
        movl $len, %edx	#字符串长度
        int $0x80	#通过中断进行系统调用

        movl $1, %eax	#exit的调用号
        movl $3, %ebx	#退出码
        int $0x80

使用如下命令编译,链接:

$ as t1.s -o t1.o && ld t1.o -o t1

使用

$ ./t1

运行后可以看到程序输出“Hello from assembly”,通过

$ echo $?

可以看到程序的退出码为3。这里调用了两个系统调用,write和exit,从上面的程序也可以看出系统调用的参数是直接通过寄存器传递的,而不是像普通函数一样使用堆栈。而且参数按照在C函数原型中的顺序放在ebx,ecx,edx里,eax中放系统调用号。
关于使用汇编在linux下进行编程请看《Programming From The Ground Up》。

添加系统调用

现在开始添加新的系统调用,首先添加系统调用代码,将下列代码加入到linux目录下的kernel/sys.c的末尾:

asmlinkage void sys_copy(long *src, long *dst)
{
        long buf;
	buf = *src;
	*dst = buf;
}

asmlinkage long sys_add(long a, long b)
{
        return a + b;
}

这里我们定义了两个系统调用,一个将用户的数据从一个地方拷贝到另一个地方,这和swap的功能一样,只是在内核态中运行,不过需要注意的是这个函数没有进行任何安全性检查,这样应用程序可以传入一个指向内核地址的指针,去读取或修改内核的数据,这是一个巨大的安全问题,在真正开发中绝对不能这么干。另一个系统调用只是简单地将两个参数相加。

之后要做的就是把系统调用的原型加到include/linux/syscalls.h头文件的末尾:

asmlinkage void sys_copy(long *src, long *dst);

asmlinkage long sys_add(long a, long b);

最后一步是最重要的,这一步也和网上的很多资料不同,以前版本的内核添加系统调用还需要修改总的系统调用数,而现在只需要在一个表中添加对应的项即可,总调用数由编译过程自动生成。由于我是x86系统,而且是32位,所以只需要在arch/x86/syscalls/syscall_32.tbl的末尾加入以下两项即可:

350     i386    copy                    sys_copy
351     i386    add                     sys_add

当然前面的350和351是按照你的表加入的,在我加入系统调用的时候最大的系统调用号还只是349,所以我用了350和351号。以上三个文件修改完后两个系统调用就加入了,重新编译及运行之前先试试在旧系统中是否可以使用,使用以下汇编程序:

.section .data
num1: .long 55	#copy的源地址

.section .bss
.lcomm num2, 4	#copy的目的地址

.section .text
.global _start
_start:
        movl $350, %eax	#copy系统调用
        movl $num1, %ebx	#源地址
        movl $num2, %ecx	#目的地址
        int $0x80

        movl $351, %eax #add系统调用
        movl num2, %ebx	#拷贝后的55
        movl $22, %ecx	#55加22
        int $0x80

        movl %eax, %ebx	#将返回值作为退出码
        movl $1, %eax
        int $0x80

一样需要注意的是程序中的350和351是我注册的系统调用号,如果你的系统调用号不同应该换成你的号码,同样使用上面的方法编译链接,运行后使用

$ echo $?

查看退出码,可以看到并非想要的77,因为在旧内核中并没有350和351这样的系统调用。

编译内核

修改源码后就要编译新内核,如果你从未编译过内核,你需要一个.config文件,直接通过make defconfig生成一个默认的,不过需要注意的是这个默认的不包括几乎所有的驱动,只是让你基本上能用而已,如果还想要驱动就通过make menuconfig自己去找吧,或者你也可以从/boot/里直接拷贝一个.config到当前目录下:

$ cp /boot/config-version .config

再编译,不过这样会几乎编译所有驱动,大部分还用不上,而且浪费很长时间编译。

有了.config文件就使用

$ make -j4

开始编译,-j4是告诉make使用4个线程编译。之后使用

# make modules_install

安装模块,注意最后显示的版本号,之后就是安装了

# cp arch/x86/boot/bzImage /boot/vmlinuz-$version
# cp System.map /boot/System.map-$version
# cp .config /boot/config-$version
# mkinitramfs -k -o /boot/initrd.img-$version $version
# update-grub

其中命令中的$version需要替换成安装模块后出现的版本号。前三个命令分别拷贝内核,System map和config文件,第四个命令创建一个initrd,最后一个更新grub。都成功后就可以重启再运行前面的汇编程序了,运行后再使用:

$ echo $?

得到77就证明系统调用已经好用了。

为什么不实现一个系统调用

从上面的步骤也可以看出在内核中加入一个新的系统调用十分简单,但是并不是因为简单就要去做,很多情况下最好别加一个新的系统调用,因为一旦加入一个新的系统调用,你就必须一直维护它,同时系统调用的语义也不能改变。而且删除一个系统调用也不是那么简单,为了保持向后兼容,删除了的系统调用的调用号也不能分配给其他系统调用。所以很多情况下是不到完不得已才添加一个新的系统调用。


Similar Posts

Comments