Linux系统调用全过程详解

系统调用(SYSTEM CALL)

OS内核中都有一组实现系统功能的过程,系统调用就是对上述过程的调用。编程人员利用系统调用,向OS提出服务请求,由OS代为完成。

一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。

统调用是用户态进入内核态的唯一入口:一夫当关,万夫莫开。常用系统调用:

  • 控制硬件:如write/read调用。
  • 设置系统状态或读取内核数据——getpid()、getpriority()、setpriority()、sethostname()
  • 进程管理:如 fork()、clone()、execve()、exit()等

优点 编程容易,从硬件设备的低级编程中解脱出来 提高了系统的安全性,可以先检查请求的正确性

Linux系统调用全过程详解

Int 0x80指令

Linux中实现系统调用利用了i386体系结构中的软件中断。即调用了int  $0x80汇编指令。

这条汇编指令将产生向量为128的编程异常,CPU便被切换到内核态执行内核函数,转到了系统调用处理程序的入口:system_call()。

int $0x80指令将用户态的执行模式转变为内核态,并将控制权交给系统调用过程的起点system_call()处理函数。

system_cal()检查系统调用号,该号码告诉内核进程请求哪种服务。

内核进程查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。

接着调用相应的函数,在返回后做一些系统检查,最后返回到进程。

system_call()函数

Linux系统调用全过程详解

系统调用和普通函数调用

API是用于某种特定目的的函数,供应用程序调用,而系统调用供应用程序直接进入系统内核。

Linux内核提供了一些C语言函数库,这些库对系统调用进行了一些包装和扩展,因为这些库函数与系统调用的关系非常紧密,所以习惯上把这些函数也称为系统调用。

有的API函数在用户空间就可以完成工作,如一些用于数学计算的函数,因此不需要使用系统调用。

有的API函数可能会进行多次系统调用。

不同的API 函数也可能会有相同的系统调用。比如malloc(),calloc(),free()等函数都使用相同的方法分配和释放内存。

系统命令、内核函数

系统调用与系统命令

  • 系统命令相对API来说,更高一层。每个系统命令都是一个执行程序,如ls命令等。这些命令的实现调用了系统调用。

系统调用与内核函数

  • 系统调用是用户进入内核的接口层,它本身并非内核函数,但是它由内核函数实现。
  • 进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程”。如系统调用getpid实际调用的服务例程为sys_getpid(),或者说系统调用getpid()是服务例程sys_getpid()的封装例程。

封装例程(wrapper routine)

由于陷入指令是一条特殊指令,依赖操作系统实现的平台,如在i386体系结构中,这条指令是int $0x80(陷入指令),不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。

在标准C库函数中,为每个系统调用设置了一个封装例程,当一个用户程序执行了一个系统调用时,就会调用到C函数库中的相对应的封装例程。

系统调用过程

Linux系统调用全过程详解

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;  
  • 将寄存器中的参数压入到核心栈中(这样内核才能使用用户传入的参数。)
  • 因为在不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须把参数指定到各个寄存器中

Linux系统调用全过程详解Linux系统调用全过程详解

系统调用表与调用号

这样系统调用处理程序一旦运行,就可以从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)
  • 系统调用表记录了各个系统调用的服务例程的入口地址。
  • 以系统调用号为偏移量能够在该表中找到对应处理函数地址。
  • 在linux/include/linux/sys.h中定义的NR_syscalls表示该表能容纳的最大系统调用数,一般NR_syscalls = 256。

Linux系统调用全过程详解

系统调用的返回

当服务例程结束时,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     #堆栈弹栈,返回用户态

系统调用的返回值

所有的系统调用返回一个整数值。

  1. 正数或0表示系统调用成功结束
  2. 负数表示一个出错条件

这里的返回值与封装例程返回值的约定不同

  1. 内核没有设置或使用errno变量
  2. 封装例程在系统调用返回取得返回值之后设置这个变量
  3. 当系统调用出错时,返回的那个负值将要存放在errno变量中返回给应用程序

系统调用-实例分析

假设源文件名为getpid.c,内容是:

#include <syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
    long ID;
    ID = getpid();
    printf ("getpid()=%ld\n", ID);
    return(0);
}
  1. 该程序调用封装例程getpid()。该封装例程将系统调用号_NR_getpid(第20个)压入EAX寄存器
  2. CPU通过int $0x80 进入内核,找到system_call(),并调用它     (以下进入内核态)
  3. 在内核中首先执行system_call(),接着执行根据系统调用号在调用表中查找到的对应的系统调用服务例程sys_getpid()。
  4. 执行sys_getpid()服务例程。
  5. 执行完毕后,转入ret_from_sys_call()例程,系统调用返回到用户态。

系统调用的参数传递

很多系统调用需要不止一个参数

普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈

Linux系统调用全过程详解

在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的

Linux系统调用全过程详解

系统调用使用寄存器来传递参数,要传递的参数有:

  • 系统调用号
  • 系统调用所需的参数

用于传递参数的寄存器有:

  • eax:用于保存系统调用号和系统调用返回值
  • 系统调用参数保存在:ebx,ecx,edx,esi和edi中

进入内核态后,system_call通过使用SAVE_ALL宏把这些寄存器的值保存在内核态堆栈中。

用寄存器传递参数必须满足3个条件:  

  1. 每个参数的长度不能超过寄存器的长度  
  2. 参数的个数不能超过6个(包括eax中传递的系统调用号);否则,需要用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区即可
  3. 返回值必须写到eax寄存器中

参数传递举例

处理write系统调用的sys_write服务例程声明如下

Linux系统调用全过程详解

该函数期望在栈顶找到fd,buf和count参数     在封装sys_write()的封装例程中,将会在ebx、ecx和edx寄存器中分别填入这些参数的值,然后在进入system_call时,SAVE_ALL会把这些寄存器保存在堆栈中,进入sys_write服务例程后,就可以在相应的位置找到这些参数

asmlinkage使得编译器不通过寄存器(x=0)而 使用堆栈传递参数

SAVE_ALL

Linux系统调用全过程详解

设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. 添加系统调用号
  2. 在系统调用表中添加相应的表项
  3. 实现系统调用服务例程
  4. 重新编译内核,启动新内核
  5. 编写一段测试程序检验实验结果

(1)添加系统调用号:它位于unistd.h,每个系统调用号都以“_NR_开头”,

  • 系统调用的编号命名为  __NR_mysyscall
  • 改写/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

(2)在系统调用表中添加相应的表项

  • 内核中实现该系统调用的例程的名字    sys_mysyscall
  • 改写arch/i386/kernel/entry.S
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 <linux/unistd.h>
_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为入口参数名。

Linux系统调用全过程详解

 

 

 

 

 

 

 

 

 

 

上一篇:PADOVAN VALERIO 070413 6.50BAR


下一篇:NR 5G 承载网