RSS Feed

November, 2012

  1. 给内核添加系统调用

    November 16, 2012 by 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就证明系统调用已经好用了。

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

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