一般情况下,进程不能存取系统内核的,只有系统调用是一个例外,在intel结构的计算机中,是通过中断0x80实现的
进程可以可以跳转到内核中的位置是system_call。在此检查系统调用号,它告诉内核进程请求何种服务,然后查找系统调用表sys_call_table,找到希望调用的内核地址函数,调用此函数,然后返回。
1、 宏定义(unistd.h)
_syscallN(type,name,x…),N是系统调用所需参数数目,type是返回类型,name是面向用户的系统调用函数名,x…是参数,个数为N
如:#define _syscall1(type,name,type1,arg1) /
type name(type1 arg1) /
{ /
long __res; /
__asm__ volatile ("push %%ebx ; movl %2,%%ebx ; int $0x80 ; pop %%ebx" /
: "=a" (__res) /
: "0" (__NR_##name),"ri" ((long)(arg1)) : "memory"); /
__syscall_return(type,__res); /
}
分析:
ouput: _res 0% __NR##name “=a”表示存放在eax寄存器
也就是表示把变量_res存放在eax中,“0”表示把_NR##name关联到0%即_res.返回值也存放在eax中.
#define _syscall2(type,name,type1,arg1,type2,arg2) /
type name(type1 arg1,type2 arg2) /
{ /
long __res; /
__asm__ volatile ("push %%ebx ; movl %2,%%ebx ; int $0x80 ; pop %%ebx" /
: "=a" (__res) /
: "0" (__NR_##name),"ri" ((long)(arg1)),"c" ((long)(arg2)) /
: "memory"); /
__syscall_return(type,__res); /
}
同理可以知道arg1存放在eax中,arg2存放在ebx中
当我们在程序中用到系统调用时,对于linux预定义的系统调用,编译器会在预处理时自动加入宏_syscallN()并将其展开,对于自己添加的系统调用,需要在程序中显示地调用宏_syscallN.
2 系统调用入口函数
在_syscallN()中执行了int 0x80后,程序进入内核态,我们在陷阱门和系统门的初始化里做了如下设置
set_system_gate(SYSCALL_VECTOR,&system_call);
程序跳到system_call(entry.s)处
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
#ifdef __SMP__
ENTER_KERNEL
#endif
movl $-ENOSYS,EAX(%esp)
cmpl $(NR_syscalls),%eax
jae ret_from_sys_call
movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
testl %eax,%eax
je ret_from_sys_call
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%edx)
movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
movl SYMBOL_NAME(current_set),%ebx
#endif
andl $~CF_MASK,EFLAGS(%esp)
movl %db6,%edx
movl %edx,dbgreg6(%ebx)
testb $0x20,flags(%ebx)
jne 1f
call *%eax
movl %eax,EAX(%esp)
jmp ret_from_sys_call
这段代码现保存所有的寄存器值,然后检查调用号(__NR_name)是否合法(在系统调用表中查找),找到正确的函数指针后,就调用该函数(即你真正希望内核帮你运行的函数)。运行返回后,将调用ret_from_sys_call,这里就是著名的进程调度时机之一。
调用号定义如下(unistd.h)
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
........................
#define __NR_sync_file_range 314
#define __NR_tee 315
#define __NR_vmsplice 316
#define NR_syscalls 317
以调用号_NR_name为下标,找出系统调用表sys_call_table中对应的表项,正好就是系统调用响应函数sys_name的入口地址
系统调用响应函数(syscall_table.s)
ENTRY(sys_call_table)
long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
............
.long sys_tee /* 315 */
.long sys_vmsplice
3 实例
现在我们知道增加一条系统调用我们首先要添加服务例程实现代码,然后在进行对应向量的申明,最后当然还要在sys_call_table表中增加一项以指明服务例程的入口地址。
OK,有了以上简单的分析,现在我们可以开始进行源码的修改,假设我们需要添加一条系统调用计算两个整数的平方和,系统调用名为add2,我们需要修改三个文件:kernel/sys.c , arch/i386/kernel/entry.S 和 include/asm-386/unistd.h。
1、修改kernel/sys.c ,增加服务例程代码:
asmlinkage int sys_add2(int a , int b)
{
int c=0;
c=a*a+b*b;
return c;
}
2、修改include/asm-386/unistd.h ,对我们刚才增加的系统调用申明向量,以使用户或系统进程能够找到这条系统调用,修改后文件如下所示:
.... .....
#define _NR_sendfile 187
#define _NR_getpmsg 188
#define _NR_putmsg 189
#define _NR_vfork 190
#define _NR_add2 191 /* 这是我们添加的部分,191即向量 */
3、修改include/asm-386/unistd.h , 将服务函数入口地址加入 sys_call_table,首先找到这么一段:
.... .....
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */
.long SYMBOL_NAME(sys_vfork) /*190 */
.rept NR_syscalls-190
修改为如下:
.... .....
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */
.long SYMBOL_NAME(sys_vfork) /*190 */
.long SYMBOL_NAME(sys_add2) <=我们的系统调用
.rept NR_syscalls-191 <=将190改为191
OK,大功告成,现在只需要重新编译你的LINUX内核,然后你的LINUX就有了一条新的系统调用int add2(int a, int b)。
4 使用新的系统调用
在应用程序中使用新添加的系统调用add2。
#include <linux/unistd.h>
_syscall2(int,add2,int,a, int, b)
main()
{
int a = 1;
int b =2;
printf("result = %d /n",add2(a,b));
}