席金玉+ 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程
一、系统调用步骤
- 程序调用库的封装函数;
- 调用软中断int $0x80进入内核;
- 在内核中首先执行system_call()函数,接着根据系统调用号在系统调用表中查找到对应的系统调用服务例程;
- 执行该服务例程;
- 执行完毕后,转入ret_from_sys_call例程,从系统调用返回。
二、系统调用分析
系统调用(System Call):OS内核中都有一组实现系统功能的过程,系统调用就是对上述过程的调用。应用程序利用系统调用,向OS提出服务请求,OS代为完成任务。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点,只有系统调用时一个例外。
系统调用时用户态进入内核态的唯一入口,常用的系统调用有:
- 控制硬件:如read/write调用;
- 设置系统状态或读取内核数据——getpid()、getpriority()、setpriority()、sethostname();
- 进程管理:fork()、clone()、execve()、exit()等。
优点:编程容易,从硬件设备的低级编程中解脱出来;提高了系统的安全性,可以先检查请求的正确性。
分析:Linux中实现系统调用利用了i386体系结构中的软件中断。即调用了int $0x80汇编指令;这条汇编指令将产生向量为128的编程异常,CPU便被切换到内核态执行内核函数,转到了系统调用处理程序的入口:system_call();int $0x80指令将用户态的执行模式转变为内核态,并将控制权交给系统调用过程的起点system_call()处理函数。system_call()检查系统调用号,该号码告诉内核进程请求哪种服务。内核进程查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。接着调用相应的函数,在返回后做一些系统检查,最后返回到进程。
2.1 系统调用和普通函数调用
API是用于某种特定目的的函数,供应用程序调用,而系统调用供应用程序直接进入系统内核。
Linux内核提供了一些C语言的函数库,这些库对系统调用进行了一些包装盒扩展,因为这些库函数与系统调用的关系非常紧密,所以习惯上把这些函数也称为系统调用。
有的API函数在用户空间就可以完成工作,如一些用于数学计算的函数,因此不需要使用系统调用。有的API函数可能会进行多次系统调用。
不同的API函数也可能会有不同的系统调用。比如malloc()、calloc()、free()等函数都使用相同的方法分配和释放内存。
2.2 系统调用和系统命令
系统命令相对API来说,更高一层。每个系统命令都是一个执行程序,如1s命令等。这些命令的实现调用了系统调用。
系统调用时用户进入内核的接口层,它本省并非内核函数,但是它由内核函数实现。
进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用“服务例程”。如系统调用getpid实际调用的服务例程为sys_getpid(),或者说系统调用getpid()是服务例程sys_getpid()的封装例程。
2.3 封装例程
由于陷入指令是一条特殊命令,依赖操作系统实现的平台,如在i386体系结构中国,这条指令是int $0x80(陷入指令),不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。
在标准C库函数中,为每个系统调用设置了一个封装例程,当一个用户程序执行了一个系统调用时,就会调用到C函数库中的相对应的封装例程。
2.4 sys_call代码分析
push1 %eax /*将系统调用号压栈*/
SAVE_ALL
cmp1$(NR_syscalls),%eax /*检查系统调用号*/
jb nobadsys
mov1 $(-ENOSYS), 24(%esp) /*堆栈中的eax设置为-ENOSYS,作为返回值*/
jmp ret_from_sys_call
nobadsys:
call *sys_call_table(, %eax, 4) #调用系统调用表中调用号为eax的系统调用例程。
mov1 %eax,EAX(%esp) #将返回值存入堆栈中
jmp ret_from_sys_call
分析:
首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成);
对用户态进程传递过来的系统调用号进行有效检查(eax是系统调用号,它应该小于NR_syscalls),如果是合法的系统调用,再进一步检测该系统调用是否正被跟踪。根据eax中的系统调用号调用相应的服务例程。
服务例程结束后,从eax寄存器获得它的返回值,并把这个返回值存放在堆栈中,让其位于用户态eax寄存器曾存放的位置。
然后跳转到ret_from_sys_call(),终止系统调用程序的执行。
2.5 SAVE_ALL
分析:SAVE_ALL将寄存器的参数压入到核心栈中(这样内核才能使用用户传入的参数)。因为子啊不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须把参数指定到各个寄存器中。
2.6 系统调用表和调用号
系统调用处理程序一旦运行,就可以从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中得到系统调用号,然后再去系统调用表中寻找相应服务例程。
系统调用表记录了各个系统调用的服务例程的入口地址;以系统调用号为偏移量能够在该表中找到对应处理函数地址。
在linux/include/linux/sys.h中定义的NR_syscalls表示该表能容纳的最大系统调用数,一般NR_syscalls=256。
2.7 系统调用的返回
当服务例程结束时,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 #堆栈弹栈,返回用户态
所有的系统调用返回一个整数,正数或0表示系统调用成功结束,负数表示一个出错条件。
这里的返回值与封装例程返回值的约定不同,内核没有设置或使用errno变量,封装例程在系统调用返回取得返回值之后设置这个变量,当系统调用出错时,返回的那个负值将要存放在errno变量中返回给应用程序。
2.8 系统调用的参数传递
很多系统调用需要不止一个参数:普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。但因为系统是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈也不能直接使用内核态堆栈。
在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的。
系统调用使用寄存器来传递参数,要传递的参数有:系统调用号,系统调用所需的参数。
用于传递参数的寄存器有:eax(用于保存系统调用号和系统调用返回值);系统调用参数保存在:ebx,ecx,edx,esi和edi中
进入内核态后,system_call通过使用SAVE_ALL宏把这些寄存器的值保存在内核堆栈中。
用寄存器传递参数必须满足两个条件:
- 每个参数的长度不能超过寄存器的长度;
- 参数的个数不能超过6个(包括eax中传递的系统调用号);否则,需要用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区即可。返回值必须写到eax寄存器中。
三、实验过程
打开实验楼虚拟机,进入终端界面,然后执行以下步骤:
之后打开另一个终端界面,仿照上次实验的过程,进入单步执行方式,设置断点:break time_asm,运行程序:
单步执行time_asm命令,查看time_asm命令的运行过程。
四、实验总结
这个实验使我对系统调用的具体过程有了较大的了解,但是还没有达到深刻理解的目的,之后的学习我会温习这部分系统调用的知识点。
在系统调用过程给你中,操作系统为用户程序和内核程序设置了一组接口用来完成应用程序对内核程序的调用完成相应的功能,这组接口就是系统调用。
有了系统调用,使得用户程序在完成一定功能的时候也可以间接性的“越级”调用特权级指令来执行,只不过这种调用不同于普通的函数调用。I386体系结构设置不同的执行级别,使得内核在接收某个用户请求之前在接口级别上就可以检查这种请求是否是正确的合法的。
Linux系统就是通过应用程序发出的系统调用(system call)实现了用户态进程和内核态进程间的交互完成特定功能。