[转] 添加新的系统调用 _syscall0(int, mysyscall)
实验目的
阅读 Linux 内核源代码,通过添加一个简单的系统调用实验,进一步理解
Linux操作系统处理系统调用的统一流程。通过用kernel module的方法来实现一
个系统调用实验,进一步理解Linux的内核模块和Linux系统调用机制,对通过
module方法添加一个系统调用的步骤有所了解。
实验内容
1、在现有的系统中添加一个不用传递参数的系统调用。调用这个系统调
用,使用户的uid变成0。
2、用kernel module的方法来实现一个系统调用
实验提示 1
怎么样?觉得困难还是觉得太简单?我们首先承认,每个人接触Linux的时间长短不一
样,因此基础也会有一点差别。那么对于觉得太简单的人呢,请你迅速地合上书本,然后跑
到电脑前面,开始实现这个题目。来吧,不要眼高手低,做完之后,你就可以跳过这一节,
直接进入下一节的学习了。对于觉得有点困难的人呢,不用着急,这一节就是专门为你准备
的。我们会列出详细的实现步骤,你一定也没有问题的。
如果你前面对整个系统调用的过程有一个清晰的理解的话,我们就顺着系统调用的流程
思路,给出一个添加新的系统调用的步骤:
一、决定你的系统调用的名字
这个名字就是你编写用户程序想使用的名字,比如我们取一个简单的名字:mysyscall。
一旦这个名字确定下来了,那么在系统调用中的几个相关名字也就确定下来了。
l 系统调用的编号名字:__NR_mysyscall;
l 内核中系统调用的实现程序的名字:sys_mysyscall;
现在在你的用户程序中出现了:
#include <linux/unistd.h>
int main()
{
mysyscall();
}
流程转到标准C库。
二、利用标准C 库进行包装吗
编译器怎么知道这个mysyscall 是怎么来的呢?在前面我们分析的时候,我们知道那时
标准C 库给系统调用作了一层包装,给所有的系统调用做出了定义。但是显然,我们可能
不愿意去改变标准C库,也没有必要去改变。那么我们在自己的程序中来做:
#include <linux/unistd.h>
_syscall0(int,mysyscall) /* 注意这里没有分号*/
int main()
{
mysyscall();
}
好,由于有了_syscall0 这个宏,mysyscall 将得到定义。但是现在系统会去找系统调用号,
以放入eax。所以,接下来我们定义系统调用号。
三、添加系统调用号
系统调用号在文件unistd.h里面定义。这个文件可能在你的系统上会有两个版本:一个
是C库文件版本,出现的地方是在/usr/include/unistd.h和/usr/include/asm/unistd.h;另外还有
一个版本是内核自己的unistd.h,出现的地方是在你解压出来的2.4.18 内核代码的对应位置
(比如/usr/src/linux/include/linux/unistd.h和/usr/include/asm-i386/unistd.h)。当然,也有可能
这个C 库文件只是一个到对应内核文件的连接。至于为什么会存在两个版本的头文件,这
个问题将会在5-6较高级主题一节进行说明。现在,你要做的就是在文件unistd.h中添加我
们的系统调用号:__NR_mysyscall,如下所示:
include/asm-i386/unistd.h
/usr/include/asm/unistd.h
240 #define __NR_llistxattr 233
241 #define __NR_flistxattr 234
242 #define __NR_removexattr 235
243 #define __NR_lremovexattr 236
244 #define __NR_fremovexattr 237
245 #define __NR_mysyscall 238 /* mysyscall adds here */
添加系统调用号之后,系统才能根据这个号,作为索引,去找syscall_table中的相应表项。
所以说,我们接下来的一步就是:
四、在系统调用表中添加相应表项
我们前面讲过,系统调用处理程序(system_call)会根据eax 中的索引到系统调用表
(sys_call_table)中去寻找相应的表项。所以,我们必须在那里添加我们自己的一个值。
arch/i386/kernel/entry.S
398 ENTRY(sys_call_table)
399 .long SYMBOL_NAME(sys_ni_syscall)
400 .long SYMBOL_NAME(sys_exit)
401 .long SYMBOL_NAME(sys_fork)
402 .long SYMBOL_NAME(sys_read)
403 .long SYMBOL_NAME(sys_write)
……
……
634 .long SYMBOL_NAME(sys_ni_syscall)
635 .long SYMBOL_NAME(sys_ni_syscall)
636 .long SYMBOL_NAME(sys_ni_syscall)
637 .long SYMBOL_NAME(sys_mysyscall)
638
639 .rept NR_syscalls-(.-sys_call_table)/4
640 .long SYMBOL_NAME(sys_ni_syscall)
641 .endr
到现在为止,系统已经能够正确地找到并且调用sys_mysyscall。剩下的就只有一件事情,那
就是sys_mysyscall的实现。
五、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 内核结构的层次性还是非常清楚的,这就使得每一个开发者可以
把精力放在怎么样实现具体的功能上,而不用在一些接口函数上伤脑筋。
现在所有的代码添加已经结束,那么要使得这个系统调用真正在内核中运行起来,我们
就需要对内核进行重新编译。这个问题我们在第一章的时候就讨论到了,应该没有问题,因
此我们在这里略过。
六、编写用户态程序
要测试我们新添加的系统调用,我们可以编写一个用户程序调用我们自己的系统调用。
我们对自己的系统调用的功能已经很清楚了:使得自己的uid变成0。那么我们看看是不是
如此:
用户态程序
#include <linux/unistd.h>
_syscall0(int,mysyscall) /* 注意这里没有分号*/
int main()
{
mysyscall(); /* 这个系统调用的作用是使得自己的uid为0 */
printf(“em…, this is my uid: %d. \n”, getuid());
}
实验提示2:
在这里,我们把系统调用的知识和Kernel Module的知识结合起来,用kernel module的
方法来实现一个系统调用。这个系统调用是gettimeofday的简化版本。实验的目的只是为了
使你对通过module方法添加一个系统调用的步骤有所了解。
具体代码示例如下:
/* pedagogictime.c */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
/* 在这个头文件里面包含了所有的系统调用号__NR_... */
#include <linux/unistd.h>
/* for struct time* */
#include <linux/time.h>
/* for copy_to_user() */
#include <asm/uaccess.h>
/* for current macro */
#include <linux/sched.h>
#define __NR_pedagogictime 238
MODULE_DESCRIPTION("My sys_pedagogictime()");
MODULE_AUTHOR("Your Name :), (C) 2002, GPLv2 or later");
/* 用来保存旧系统调用的地址*/
static int (*anything_saved)(void);
/* 这个是我们自己的系统调用函数sys_pedagogictime(). */
static int sys_pedagogictime(struct timeval *tv)
{
struct timeval ktv;
/* 这里我们需要增加模块使用计数。*/
MOD_INC_USE_COUNT;
do_gettimeofday(&ktv);
if (copy_to_user(tv, &ktv, sizeof(ktv))){
MOD_DEC_USE_COUNT;
return -EFAULT;
}
printk(KERN_ALERT"Pid %ld called sys_gettimeofday().\n",(long)current->pid);
MOD_DEC_USE_COUNT;
return 0;
}
/* 这里是初始化函数。__init标志表明这个函数使用后就可以丢弃了。*/
int __init init_addsyscall(void)
{
extern long sys_call_table[];
/* 保存原来系统调用表中此位置的系统调用*/
anything_saved = (int(*)(void))(sys_call_table[__NR_pedagogictime]);
/* 把我们自己的系统调用放入系统调用表,注意进行类型转换*/
sys_call_table[__NR_pedagogictime] = (unsigned long)sys_pedagogictime;
return 0;
}
/* 这里是退出函数。__exit标志表明如果我们不是以模块方式编译这段程序,则这个标志后的
* 函数可以丢弃。也就是说,模块被编译进内核,只要内核还在运行,就不会被卸载。
*/
void __exit exit_addsyscall(void)
{
extern long sys_call_table[];
/* 恢复原先的系统调用*/
sys_call_table[__NR_pedagogictime] = (unsigned long)anything_saved;
}
/* 这两个宏告诉系统我们真正的初始化和退出函数*/
module_init(init_addsyscall);
module_exit(exit_addsyscall);
使用kernel module的方法来实现的这个系统调用,我们需要做的只是用这条命令:
gcc -Wall -O2 -DMODULE -D__KERNEL__ -DLINUX -c pedagogictime.c.
编译成.o文件,然后使用insmod pedagogictime.o把它动态地加载到正在运行的内核中。显
然,这样的做法比起我们先前的那种要重新编译内核的办法更加灵活,更加方便。这也正是
Linux kernel module program如此受欢迎的原因。
测试用这个使用kernel module编写的系统调用的用户程序:
测试用的用户程序
/* for struct timeval */
#include <linux/time.h>
/* for _syscall1 */
#include <linux/unistd.h>
#define __NR_pedagogictime 238
_syscall1(int, pedagogictime, struct timeval *, thetime)
int main()
{
struct timeval tv;
pedagogictime(&tv);
printf("tv_sec : %ld\n", tv.tv_sec);
printf("tv_nsec: %ld\n", tv.tv_usec);
printf("em..., let me sleep for 2 second.:)\n");
sleep(2);
pedagogictime(&tv);
printf("tv_sec : %ld\n", tv.tv_sec);
printf("tv_nsec: %ld\n", tv.tv_usec);
}
假设这个程序是test.c,那么使用gcc -o test test.c得到test可执行文件,然后你可以执
行这个test看看结果。