系统调 用是操作系统提供给软件开发人员的唯一接口,开发人员可利用它使用系统功能。OS核心中都有一组实现系统功能的过程(子程序),系统调用就是对上述过程的调用。因此,系统调用像一个黑箱子那样,对用户屏蔽了操作系统的具体动作而只提供有关的功能。
系统调用在os中发挥着巨大的作用,如果没有系统调用那么应用程序就是失去了内核的支持。在系统中真正被所有进程都使用的内核通信方式是系统调用。例如当进程请求内核服务时,就使用的是系统调用。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。进程使用寄存器中适当的值跳转到内核中事先定义好的代码中执行,(当然,这些代码是只读的)。在Intel结构的计算机中,这是由中断0x80实现的。
进程可以跳转到的内核中的位置叫做system_call。在此位置的过程检查系统调用号,它将告诉内核进程请求的服务是什么。然后,它再查找系统调用表sys_call_table,找到希望调用的内核函数的地址,并调用此函数,最后返回。
所以,如果希望改变一个系统调用的函数,需要做的是编写一个自己的函数,然后改变sys_call_table中的指针指向该函数,最后再使用cleanup_module将系统调用表恢复到原来的状态。我们现在向内核中添加的三个系统调用就是属于向内核中添加新的函数,且这些函数是可以直接操作系统内核的。下面是系统调用的基本处理过程:
![](https://i-blog.csdnimg.cn/blog_migrate/184a33f484fe758767dc7638c7e220be.png)
Linux的系统调用机制
在Linux系统中,系统调用是作为一种异常类型实现的。它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。这就是说,执行系统调用异常指令时,自动地将系统切换为核心态,并安排异常处理程序的执行。Linux用来实现系统调用异常的实际指令是:
Int $0x80
这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核。为达到在使用系统调用时不必用机器指令编程,在标准的C语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码段非常简短。它所要做的工作只是将送给系统调用的参数加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用,系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回用户程序。
为使系统调用的执行成为一项简单的任务,Linux提供了一组预处理宏指令。它们可以用在程序中。这些宏指令取一定的参数,然后扩展为调用指定的系统调用的函数。
这些宏指令具有类似下面的名称格式:
_syscallN(parameters)
其中N是系统调用所需的参数数目,而parameters则用一组参数代替。这些参数使宏指令完成适合于特定的系统调用的扩展。例如,为了建立调用setuid()系统调用的函数,应该使用:
_syscall1( int, setuid, uid_t, uid )
syscallN( )宏指令的第1个参数int说明产生的函数的返回值的类型是整型,第2个参数setuid说明产生的函数的名称。后面是系统调用所需要的每个参数。这一宏指令后面还有两个参数uid_t和uid分别用来指定参数的类型和名称。
另外,用作系统调用的参数的数据类型有一个限制,它们的容量不能超过四个字节。这是因为执行int $0x80指令进行系统调用时,所有的参数值都存在32位的CPU寄存器中。使用CPU寄存器传递参数带来的另一个限制是可以传送给系统调用的参数的数目。这个限制是最多可以传递5个参数。所以Linux一共定义了6个不同的_syscallN()宏指令,从_syscall0()、 _syscall1()直到_syscall5()。
一旦_syscallN()宏指令用特定系统调用的相应参数进行了扩展,得到的结果是一个与系统调用同名的函数,它可以在用户程序中执行这一系统调用。
整个系统调用的过程可以总结如下:
1, 执行用户程序;
2, 根据glibc(GNU实现的一套标准C的库函数)中的函数实现,取得系统调 用号并执行 int $0x80产生中断;
3, 进行地址空间的转换和堆栈的切换,执行SAVE_ALL。(进入内核模式)
4, 进行中断处理,根据系统调用表调用内核函数;
5, 执行内核函数;
6, 执行RESTORE_ALL并返回用户模式;
系统实现:
这里以具体的例子来说明如何向系统中添加新的系统调用。具体实现所用的文件等可能与上面所述有点不一致,但原理是相同的。
1、实验环境:
实验的环境为Ubuntu9.10系统,内核版本为2.6.31-21-generic。添加完系统调用后的内核版本命名为2.6.31-12。
2,实验步骤:
1) 下载Linux内核:在终端中输入命令$sudo apt-get install linux-source。下载后的文件默认放在目录/usr/src下。
2) 将内核代码解压缩:例如下载的内核文件为linux-source-2.6.31.tar.bz2,运行解压命令tar –jxvf linux-source-2.6.31.tar.bz2。解压出的文件夹为/usr/src/linux-source-2.6.31。如下图:
3) 修改/usr/src/linux-source-2.6.31/kernel/sys.c文件,在文件末尾增加三个系统响应函数。函数实现如下:
asmlinkage int sys_mycall(int number)
{
printk("这是我添加的第一个系统调用");
return number;
}
asmlinkage int sys_addtotal(int number)
{
int i=0,enddate=0;
printk("这是我添加的第二个系统调用");
while(i<=number)
enddate+=i++;
return enddate;
}
asmlinkage int sys_three()
{
printk("这是我添加的第三个系统调用");
return 0;
}
4) 在/usr/src/linux-source-2.6.31/arch/x86/kernel/ syscall_table_32.S 中添加:.long sys_mycall。
![](https://i-blog.csdnimg.cn/blog_migrate/15c250f5331ab604c3039c0d71783fdf.png)
5) 在/usr/src/linux-2.6.31/arch/x86/include/asm/unistd_32.h中添加:#define __NR_mycall 序号(例如337),添加系统调用的入口参数(注意:其中会顺序定义入口参数的序号,添加的序号是在原有最大值的基础上+1);实现如下:
编译内核,命令依次如下:
首先切换到解压的内核目录下。
第一步:make mrproper //清除内核中不稳定的目标文件,附属文件及内核配置文件
第二步:make clean //清除以前生成的目标文件和其他文件
第三步:make oldconfig// 采用默认的内核配置(使用make menuconfig可以自己配置编译选项)
第四步:make bzImage //编译内核
第五步:make modules //编译模块
第六步:make modules_install// 安装模块
编译完成后,设置采用新内核启动。
我编译成功的内核版本号命名为2.6.31.12
运行命令:
cp /usr/src/linux-source-2.6.31/arch/i386/boot/
bzImage /boot/vmlinuz-2.6.31.12-mykernel(注意:2.6.31.12为你编译的内核版本。)
mkinitramfs -o initrd.img-2.6.31.12 2.6.31.12
//执行目录/usr/src/linux-source-2.6.31/下
cp /usr/src/linux-source-2.6.31/initrd.img-2.6.31.12 /boot/ initrd.img-2.6.31.12
增加引导菜单项,配置启动项文件/boot/grub/grub.cfg。添加的配置如下:
完成后执行终端命令sudo update-grub2,之后重启,终端输入uname -a检查你的内核版本是否是你编译的版本2.6.31.12 。
编写测试函数:我的测试函数如下:
/*~~~~~~~~~~~~~~~test1.c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
#include<stdio.h>
int main()
{
int tmp;
tmp=syscall(337,1);
printf("\n");
if(tmp==1)
{
printf("第1次系统调用成功!\n");
}
tmp=syscall(338,5);
printf("\n");
if(tmp==15)
{
printf("第2次系统调用成功!\n");
}
tmp=syscall(339);
printf("\n");
if(tmp==0)
{
printf("第3次系统调用成功!\n");
}
}
编译,运行。在终端输入dmesg -c可显示函数的输出内容。
总结:
由于使用了系统调用,编译和执行程序时,用户都应该获得超级用户权限。而且grub.cfg默认是没有写权限的,需要修改是有写权限,为了系统安全,在配置完文件后不要忘了再将权限改回来。
编译的时间会很长,我成功的一次编译用了接近2.5个小时。需要点耐心。
如果用虚拟机安装的话,需要注意磁盘空间的大小,我第一次用了个5G个虚拟系统,结果编译到中途就没空间了。10G左右的系统大小估计差不多。
最后的测试函数可能看不到预想的输出系统,因为printk不会直接打印出来,而是需要命令:dmesg,直接执行会打印出很多东西,但它有一个参数:-c,清除缓存中的系统信息。于是每次用dmesg时,都加上这个参数,结果就只打印我们需要的信息。