添加系统调用
实验目的
学习Linux内核的系统调用,理解、掌握Linux系统调用的实现框架、用户界面、参数传递、进入/返回过程。
实验内容
本实验分两步走。
第一步,在系统中添加一个不用传递参数的系统调用;执行这个系统调用,使用户的uid等于0。
显然,这不是一个有实际意义的系统调用。
我们的目的并不是实用不实用,而是通过最简单的例子,帮助熟悉对系统调用的添加过程,为下面我们添加更加复杂的系统调用打好基础。
第二步,用kernel module机制,实现系统调用gettimeofday的简化版,返回调用时刻的日期和时间。
实验指导
2.1一个简单的例子
在我们开始学习系统调用这一章之前,让我们先来看一个简单的例子。
就好像哪个经典的编程书上都会使用到的例子一样:
1: int main(){
2: printf(“Hello World!\n”);
3: }
我们也准备了一个例子给你:
1: #include <linux/unistd.h> /* all system calls need this header */
2: int main(){
3: int i = getuid();
4: printf(“Hello World! This is my uid: %d\n”, i);
5: }
这就是一个最简单的系统调用的例子。
与上面那个传统的例子相比,在这个例子中多了
2行,他们的作用分别是:
第一行:包括unistd.h这个头文件。
所有用到系统调用的程序都需要包括它,因为系统调用中需要的参数(例如,本例中的“__NR_getuid”,以及_syscall0()函数)包括在unistd.h中;根据C语言的规定,include <linux/unistd.h>意味着/usr/include/linux目录下整个unistd.h都属于Hello World源程序了。
第三行:进行getuid()系统调用,并将返回值赋给变量i。
好了,这就是最简单的一个使用了系统调用的程序,现在你可以在你的机器上试一试它。
然后我们一起进入到系统调用的神秘世界中去。
2.2 简单系统调用的添加
在这一节中,我们将要实现一个简单的系统调用的添加。
我们先给出题目:
题目:在现有的系统中添加一个不用传递参数的系统调用。
功能要求:调用这个系统调用,使用户的uid等于0。
目的:显然,这不是一个有实际意义的系统调用,我们的目的并不是有用,而是一种证明,一个对系统调用的添加过程的熟悉,为下面我们添加更加复杂
的系统调用打好基础。
怎么样?觉得困难还是觉得太简单?我们首先承认,每个人接触Linux的时间长短不一样,因此基础也会有一点差别。
那么对于觉得太简单的人呢,请你迅速地合上书本,然后跑到电脑前面,开始实现这个题目。
来吧,不要眼高手低,做完之后,你就可以跳过这一节,直接进入下一节的学习了。
对于觉得有点困难的人呢,不用着急,这一节就是专门为你准备的。
我们会列出详细的实现步骤,你一定也没有问题的。
如果你前面对整个系统调用的过程有一个清晰的理解的话,我们就顺着系统调用的流程思路,给出一个添加新的系统调用的步骤:
2.2.1决定你的系统调用的名字
这个名字就是你编写用户程序想使用的名字,比如我们取一个简单的名字:mysyscall。
一旦这个名字确定下来了,那么在系统调用中的几个相关名字也就确定下来了。
●系统调用的编号名字:__NR_mysyscall;
●内核中系统调用的实现程序的名字:sys_mysyscall;
现在在你的用户程序中出现了:
#include <linux/unistd.h>
int main()
{
mysyscall();
}
流程转到标准C库。
2.2.2利用标准C库进行包装吗
编译器怎么知道这个mysyscall是怎么来的呢?在前面我们分析的时候,我们知道那时标准C库给系统调用作了一层包装,给所有的系统调用做出了定义。
但是显然,我们可能
不愿意去改变标准C库,也没有必要去改变。
那么我们在自己的程序中来做:
#include <linux/unistd.h>
_syscall0(int,mysyscall) /* 注意这里没有分号*/
int main()
{
mysyscall();
}
好,由于有了_syscall0这个宏,mysyscall将得到定义。
但是现在系统会去找系统调用号,以放入eax。
所以,接下来我们定义系统调用号。
2.2.3添加系统调用号
系统调用号在文件unistd.h里面定义。
这个文件可能在你的系统上会有两个版本:一个是C库文件版本,出现的地方是在/usr/include/unistd.h和/usr/include/asm/unistd.h;另外还有一个版本是内核自己的unistd.h,出现的地方是在你解压出来的2.6.15内核代码的对应位置(比如/usr/src/linux/include/linux/unistd.h和/usr/include/asm-i386/unistd.h)。
当然,也有可能这个C库文件只是一个到对应内核文件的连接。
现在,你要做的就是在文件unistd.h中添加我们的系统调用号:__NR_mysyscall,如下所示:
include/asm-i386/unistd.h
/usr/include/asm/unistd.h
231 #define __NR_mysyscall223 /* mysyscall adds here */
/* 注意:不同版本的内核系统调用号不一样,您可以根据内核版本不同对系统调用号进行修改*/
添加系统调用号之后,系统才能根据这个号,作为索引,去找syscall_table中的相应表项。
所以说,我们接下来的一步就是:
2.2.4在系统调用表中添加相应表项
我们前面讲过,系统调用处理程序(system_call)会根据eax中的索引到系统调用表(sys_call_table)中去寻找相应的表项。
所以,我们必须在那里添加我们自己的一个值。
arch/i386/kernel/syscall_table.S
……
233 .long sys_mysyscall
234 .long sys_gettid
235 .long sys_readahead /* 225 */
……
到现在为止,系统已经能够正确地找到并且调用sys_mysyscall。
剩下的就只有一件事情,那就是sys_mysyscall的实现。
2.2.5 sys_mysyscall的实现
我们把这一小段程序添加在kernel/sys.c里面。
在这里,我们没有在kernel目录下另外
添加自己的一个文件,这样做的目的是为了简单,而且不用修改Makefile,省去不必要的麻烦。
asmlinkage int sys_mysyscall(void)
{
current->uid = current->euid = current->suid = current->fsuid = 0;
return 0;
}
这个系统调用中,把标志进程身份的几个变量uid、euid、suid和fsuid都设为0。
到这里为止,我们所要做的添加一个新的系统调用的所有工作就完成了,是不是非常简单?的确如此。
因为Linux内核结构的层次性还是非常清楚的,这就使得每一个开发者可以把精力放在怎么样实现具体的功能上,而不用在一些接口函数上伤脑筋。
现在所有的代码添加已经结束,那么要使得这个系统调用真正在内核中运行起来,我们就需要对内核进行重新编译。
内核编译完成后,重新启动编译后的新内核。
2.2.6 编写用户态程序
要测试我们新添加的系统调用,我们可以编写一个用户程序调用我们自己的系统调用。
我们对自己的系统调用的功能已经很清楚了:使得自己的uid变成0。
那么我们看看是不是如此:
用户态程序
#include <linux/unistd.h>
_syscall0(int,mysyscall) /* 注意这里没有分号*/
int main()
{
mysyscall(); /* 这个系统调用的作用是使得自己的uid为0 */
printf(“em…, this is my uid: %d. \n”, getuid());
}
用gcc编译源程序
# gcc –o test test.c
运行程序
# ./test
输出结果
em…, this is my uid:0。