系统调用(SYSTEM CALL)
OS内核中都有一组实现系统功能的过程,系统调用就是对上述过程的调用。编程人员利用系统调用,向OS提出服务请求,由OS代为完成。
一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。
统调用是用户态进入内核态的唯一入口:一夫当关,万夫莫开。常用系统调用:
优点 编程容易,从硬件设备的低级编程中解脱出来 提高了系统的安全性,可以先检查请求的正确性
陷入指令
在 Intel CPU 中,这个指令由中断 0x80 实现。
在 ARM 中,这个指令是 SWI。
Int 0x80指令
Linux中实现系统调用利用了i386体系结构中的软件中断。即调用了int $0x80汇编指令。
这条汇编指令将产生向量为128的编程异常,CPU便被切换到内核态执行内核函数,转到了系统调用处理程序的入口:system_call()。
int $0x80指令将用户态的执行模式转变为内核态,并将控制权交给系统调用过程的起点system_call()处理函数。
system_cal()检查系统调用号,该号码告诉内核进程请求哪种服务。
内核进程查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。
接着调用相应的函数,在返回后做一些系统检查,最后返回到进程。
system_call()函数
系统调用和普通函数调用
API是用于某种特定目的的函数,供应用程序调用,而系统调用供应用程序直接进入系统内核。
Linux内核提供了一些C语言函数库,这些库对系统调用进行了一些包装和扩展,因为这些库函数与系统调用的关系非常紧密,所以习惯上把这些函数也称为系统调用。
有的API函数在用户空间就可以完成工作,如一些用于数学计算的函数,因此不需要使用系统调用。
有的API函数可能会进行多次系统调用。
不同的API 函数也可能会有相同的系统调用。比如malloc(),calloc(),free()等函数都使用相同的方法分配和释放内存。
系统命令、内核函数
系统调用与系统命令
系统调用与内核函数
封装例程(wrapper routine)
由于陷入指令是一条特殊指令,依赖操作系统实现的平台,如在i386体系结构中,这条指令是int $0x80(陷入指令),不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。
在标准C库函数中,为每个系统调用设置了一个封装例程,当一个用户程序执行了一个系统调用时,就会调用到C函数库中的相对应的封装例程。
系统调用过程
system_call()片段
...
pushl %eax /*将系统调用号压栈*/
SAVE_ALL
...
cmpl$(NR_syscalls), %eax /*检查系统调用号
Jb nobadsys
Movl $(-ENOSYS), 24(%esp) /*堆栈中的eax设置为-ENOSYS, 作为返回值
Jmp ret_from_sys_call
nobadsys:
…
call *sys_call_table(,%eax,4) #调用系统调用表中调用号为eax的系统调用例程
movl %eax,EAX(%esp) #将返回值存入堆栈中
Jmp ret_from_sys_call
首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成);
对用户态进程传递过来的系统调用号进行有效性检查(eax是系统调用号,它应该小于 NR_syscalls)
如果是合法的系统调用,再进一步检测该系统调用是否正被跟踪;
根据eax中的系统调用号调用相应的服务例程。
服务例程结束后,从eax寄存器获得它的返回值,并把这个返回值存放在堆栈中,让其位于用户态eax寄存器曾存放的位置。
然后跳转到ret_from_sys_call(),终止系统调用程序的执行。
SAVE_ALL宏定义
#define SAVE_ALL
cld;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
movl $(__KERNEL_DS),%edx;
movl %edx,%ds;
movl %edx,%es;
系统调用表与调用号
这样系统调用处理程序一旦运行,就可以从eax中得到系统调用号,然后再去系统调用表中寻找相应服务例程。
一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。
这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
核心中为每个系统调用定义了一个唯一的编号,这个编号的定义在linux/include/asm/unistd.h中(最大为NR_syscall)
同时在内核中保存了一张系统调用表,该表中保存了系统调用编号和其对应的服务例程地址。第n个表项包含系统调用号为n的服务例程的地址。
系统调用陷入内核前,需要把系统调用号一起传入内核。而该标号实际上是系统调用表( sys_call_table)的下标 在i386上,这个传递动作是通过在执行int $0x80前把调用号装入eax寄存器实现。 这样系统调用处理程序一旦运行,就可以从eax中得到系统调用号,然后再去系统调用表中寻找相应服务例程。
系统调用号
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
系统调用表 (arch/i386/kernel/entry.s)
data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open)
.long SYMBOL_NAME(sys_close)
.long SYMBOL_NAME(sys_waitpid)
.long SYMBOL_NAME(sys_creat)
.long SYMBOL_NAME(sys_link)
.long SYMBOL_NAME(sys_unlink)
.long SYMBOL_NAME(sys_execve)
.long SYMBOL_NAME(sys_chdir)
.long SYMBOL_NAME(sys_time)
.long SYMBOL_NAME(sys_mknod)
系统调用的返回
当服务例程结束时,system_call( ) 从eax获得系统调用的返回值,并把这个返回值存放在曾保存用户态 eax寄存器栈单元的那个位置上,然后跳转到ret_from_sys_call( ),终止系统调用处理程序的执行。
当进程恢复它在用户态的执行前,RESTORE_ALL宏会恢复用户进入内核前被保留到堆栈中的寄存器值。其中eax返回时会带回系统调用的返回码(负数说明调用错误,0或正数说明正常完成)
ret_from_sys_call
cli # 关中断
cmpl $0,need_resched(%ebx)
jne reschedule #如果进程描述符中的 need_resched位不为0,则重新调度
cmpl $0,sigpending(%ebx)
jne signal_return #若有未处理完的信号,则处理
restore_all:
RESTORE_ALL #堆栈弹栈,返回用户态
系统调用的返回值
所有的系统调用返回一个整数值。
这里的返回值与封装例程返回值的约定不同
系统调用-实例分析
假设源文件名为getpid.c,内容是:
#include
#include
#include
#include
int main(void)
{
long ID;
ID = getpid();
printf ("getpid()=%ld\n", ID);
return(0);
}
系统调用的参数传递
很多系统调用需要不止一个参数
普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈
在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的
系统调用使用寄存器来传递参数,要传递的参数有:
用于传递参数的寄存器有:
进入内核态后,system_call通过使用SAVE_ALL宏把这些寄存器的值保存在内核态堆栈中。
用寄存器传递参数必须满足3个条件:
参数传递举例
处理write系统调用的sys_write服务例程声明如下
该函数期望在栈顶找到fd,buf和count参数 在封装sys_write()的封装例程中,将会在ebx、ecx和edx寄存器中分别填入这些参数的值,然后在进入system_call时,SAVE_ALL会把这些寄存器保存在堆栈中,进入sys_write服务例程后,就可以在相应的位置找到这些参数
asmlinkage使得编译器不通过寄存器(x=0)而 使用堆栈传递参数
SAVE_ALL
设C库中封装的系统调用号为3的函数原形如下:
int sys_func(int para1, int para2)
C编译器产生的汇编伪码如:
…movl 0x8(%esp),%ecx
/*将用户态堆栈中的para2放入ecx
Movl 0x4(%esp),%ebx
/*#将用户态堆栈中的para1放入ebx
Movl $0x3,%eax
/*系统调用号保存在eax中int$0x80 #引发系统调用
…
Movl %eax,errno /*将结果存入全局变量errno中
Movl $-1,%eax /*eax置为-1,表示出错注
练习:添加一个系统调用mysyscall
功能要求
首先,自定义一个系统调用mysyscall ,它的功能是使用户的uid等于0 。然后,编写一段测试程序进行调用。
执行步骤如下
(1)添加系统调用号:它位于unistd.h,每个系统调用号都以“_NR_开头”,
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
(2)在系统调用表中添加相应的表项
398 ENTRY(sys_call_table)
399 .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
3)实现系统调用服务例程 把一小段程序添加在kernel/sys.c
asmlinkage int sys_mysyscall(void)
{
current->uid = current->euid = current->suid = current->fsuid = 0;
return 0;
}
(4)重新编译内核,启动新内核
(5)编写一段测试程序检验实验结果
#include
_syscall0(int,mysyscall)/* 注意这里没有分号 */
int main()
{
mysyscall();
printf(“This is my uid: %d. \n”, getuid());
}
_syscall1(int,print_info,int,testflag)
如果要在用户程序中使用系统调用函数,那么在主函数main前必须申明调用_syscall,其中1 表示该系统调用只有一个入口参数,第一个int 表示系统调用的返回值为整型,print_info为系统调用函数名,第二个int 表示入口参数的类型为整型,testflag为入口参数名。