系统调用的工作机制

中断处理是从用户态进入内核态主要的方式

当从用户态切换到内核态的时候,必须用户态的寄存器上下文保存起来,同时设置内核态的寄存器内容
中断/int指令会在堆栈上保存一些寄存器的值
      如:用户态栈顶地址、当时的状态字、当时的 cs:eip 的值
同时设置内核态的栈顶地址、内核态的状态字,中断处理程序的入口地址 cs:eip 的值(对于系统调用来讲,它是指向system_call函数)

中断/int指令发生后第一件事就是保护现场

保护现场就是进入中断程序保存需要用到的寄存器的数据

当进入到中断处理程序后,一开始就执行SAVE_ALL,把其它的一些寄存器的值push到内核堆栈里面去

 
SAVE_ALL

中断处理结束前最后一件事是恢复现场

恢复现场就是退出中断程序恢复寄存器的数据

当中断处理程序结束之后,它会RESTORE_ALL,把保存的用户态的寄存器再pop出来到当前的CPU里面,最后iret,iret指令与中断信号(包括int指令)发生时CPU做的动作刚好相反

 
RESTORE_ALL

中断处理的完整过程

interrupt(ex:int 0x80)
save cs:eip/ss:esp/eflags(current) to kernel stack, then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack)
SAVE_ALL
....  // 内核代码,完成中断服务,发生进程调度
RESTORE_ALL
iret
pop cs:eip/ss:esp/eflags from kernel stack

SAVE_ALL....如果发生了进程调度,那么当前的状态都会暂时保存在系统里面,当下一次发生进程调度切换回当前进程的时候,就会接着把它执行完,RESTORE_ALL....

以系统调用为例,看中断具体是怎么执行的

系统调用通过软中断向内核发出一个明确的请求,是操作系统为用户态进程与硬件设备进行交互提供的一组接口

封装例程 (wrapper routine),唯一目的就是发布系统调用,让程序员在写代码的时候不需要用汇编指令来触发一个系统调用,而是直接调用一个函数就可以触发一个系统调用

应用编程接口(application program interface, API) 只是一个函数定义。一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API。但并不是每个API都对应一个特定的系统调用,API可能直接提供用户态的服务,例如一些数学函数。一个单独的API可能调用几个系统调用,不同的API可能调用了同一个系统调用

 
应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系

User Mode 用户态      Kernel Mode 内核态

xyz()函数,是系统调用对应的API,这个应用程序编程接口里面封装了一个系统调用,这个系统调用会触发一个int 0x80的中断,0x80这个中断向量对应着system_call这个内核代码的起点,这个内核代码里面会有SAVE_ALL,然后执行到sys_xyz()中断服务程序,进入程序里面处理,在中断服务程序执行完之后会ret_from_sys_call,在return的过程中可能会发生进程调度(这是一个进程调度的时机),如果没有进程调度,就会iret,回到用户态接着执行

Summary

系统调用的三层皮:API、中断向量对应的system_call、中断服务程序sys_xyz

当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数,在Linux中是通过执行int $0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常。(Intel Pentium II中引入了sysenter指令(快速系统调用),2.6已经支持)

内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,使用eax寄存器(系统调用号将xyz()和sys_xyz()关联起来了)

 

系统调用的参数传递方法

普通函数调用的时候,可以采用把参数压栈的方式传递参数。但是从用户态到内核态,怎么传递参数呢?

system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号

一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)
这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
进入sys_call之后,立即将eax的值压入内核堆栈

寄存器传递参数具有如下限制:
1)每个参数的长度不能超过寄存器的长度,即32位
2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp)
超过6个怎么办?

如果超过6个,就把某一个寄存器作为一个指针,指向一块内存,进入到内核态之后可以访问到所有的地址空间,通过内存来传递参数

通过库函数API使用系统调用获取系统当前时间

 
通过库函数API使用系统调用获取系统当前时间

用汇编方式触发系统调用获取系统当前时间

 
用汇编方式触发系统调用获取系统当前时间

系统调用传递第一个参数使用ebx,这里是NULL
使用eax传递系统调用号,这里time是13
系统调用的返回值使用eax存储,和普通函数一样

(完)



作者:那只大象
链接:https://www.jianshu.com/p/4c8a1242082a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原文地址:https://www.cnblogs.com/feng9exe/p/12521394.html