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