体系调用是应用程序与操作体系内核之间的接口,它决议了程序怎么与内核打交道的。不管程序是直接进行体系调用,仍是经过运转库,终究仍是会抵达体系调用这个层面上。x86体系下,Linux体系运用0x80号中止作为体系调用的进口。EAX寄存器用于标明体系调用的接标语,比方EAX=1标明退出进程,EAX=2标明创立进程,EAX=3标明读取文件,EAX=4标明写文件等。每一个体系调用都对应于内核代码中的一个函数,它们都是以“sys_”最初的,当体系调用回来时,EAX又作为调用成果的回来值。
包含Linux,大部分操作体系的体系调用都有两个特色:运用不方便、各个操作体系之间体系调用不兼容。为了处理这些问题,运转库挺身而出,它作为体系调用与程序之间的笼统层能够坚持这样的特色:
运用简洁
方式一致;运转库有它的规范,但凡一切遵从这个规范的运转库理论上都是彼此兼容的,与操作体系和编译库无关
CPU常常能够在多种天壤之别的特权等级下履行指令,在现代操作体系中,一般也据此有两种特权等级,别离为用户形式和内核形式,也被称为用户态和内核态。体系调用是运转在内核态,而应用程序根本都是运转在用户态。操作体系一般是经过中止来从用户态切换到内核态;中止是一个硬件或软件宣布的恳求,要求CPU暂停当时的作业易手去处理愈加重要的工作。
中止一般具有两个特点,一个称为中止号,一个称为中止处理程序。不同的中止具有不同的中止号,而一起一个中止处理程序一一对应一个中止号。在内核中,有个数组称为中止向量表,这个数组的第n项包含了指向第n号中止的中止处理程序的指针。当中止到来时,CPU会暂停当时履行的代码,依据中止的中止号,在中止向量表中找到对应的中止处理程序,并调用它。中止处理程序履行完结之后,CPU会持续履行之前的代码。
一般意义上,中止有两种类型,一种称为硬件中止,这种中止来自硬件的反常或其他事情的产生。另一种称为软件中止,软件中止一般是一条指令,带有一个参数记载中止号,运用这条指令用户能够手动触发某个中止并履行其中止处理程序。因为中止号是很有限的,操作体系不会舍得用一个中止号来对应一个体系调用,Linux运用int 0x80来触发一切的体系调用。和中止相同,体系调用都有一个体系调用号,一般便是体系调用在体系调用表中的方位。linux体系中,体系调用号一般由eax传入,用户将体系调用号放入eax中,然后运用int 0x80调用中止,中止服务程序就能够从eax里获得体系调用号,从而调用对应的函数。
下图是以fork为例的Linux体系调用的履行流程:
fork函数是一个对体系调用fork的封装,能够用下列宏来界说它:
_syscall0(pid_t, fork);
_syscall0是一个宏函数,用于界说一个没有参数的体系调用的封装。它的第一个参数为这个体系调用的回来值类型,这儿为pid_t,是一个Linux自界说类型,代表进程的id。 _syscall0的第二个参数是体系调用的称号,_syscall打开之后会构成一个与体系调用称号同名的函数。该宏界说打开之后如下:
[cpp] view plain copy
// fork
pid_t fork(void)
{
long __res;
$eax = __NR_fork
__res = $eax
__syscall_return(pid_t, __res);
}
__NR_fork是一个宏,标明fork体系调用的调用号,关于x86体系结构,该宏的界说能够在Linux/include/asm-x86/unistd_32.h里找到;而__syscall_turn是另一个宏,这个宏用于查看体系调用的回来值,并把它转换为C言语的error错误码。在Linux里,体系调用运用回来值传递错误码,假如回来值为负数,那么标明该调用失利,回来值的绝对值便是错误码。而在C言语里,大多数函数都以回来-1标明调用失利,而将犯错信息存储在一个名为errno的全局变量里。__syscall_return就担任将体系调用的回来信息存储在errno中。
假如体系调用自身有参数,那么参数经过ebx来传入。x86下Linux支撑的体系调用参数至多有6个,别离运用6个寄存器来传递,它们别离是EBX、ECX、EDX、ESI、EDI 和 EBP。
当用户调用某个体系调用的时分,实践是履行了一段汇编代码。CPU履行到int $0x80时,会保存现场以便康复,接着会将特权状况切换到内核态。然后CPU便会查找中止向量表中的第0x80号元素。
在实践履行中止向量表中0x80号元素之前,CPU首要还要进行栈的切换。在Linux中,用户态和内核态运用的是不同的栈,两者各自担任各自的函数调用,互不搅扰。但在应用程序进行体系调用时,程序的履行流程从用户态切换到内核态,这是程序的当时栈有必要也相应地从用户栈切换到内核栈。从中止处理函数中回来时,程序的当时栈还要从内核栈切换回用户栈。
所谓的“当时栈”,指的是ESP地点的栈空间。假如ESP的值坐落用户栈的范围内,那么程序的当时栈便是用户栈;此外,寄存器SS的值还应该指向当时栈地点的页。所以,将当时栈有用户栈切换为内核栈的实践行为便是:
保存当时ESP、SS的值
将ESP、SS的值设置为内核栈的相应值
反过来,将当时栈由内核栈切换为用户栈的实践行为则是:康复本来的ESP、SS的值。用户态的ESP和SS保存在内核栈上,这一行为由中止指令主动地由硬件完结。当0x80号中止产生的时分,CPU除了切入内核态之外,还会主动完结下面几件事:
找到当时进程的内核栈
在内核栈中顺次压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP。
而当内核从体系调用中回来时,需求调用iret指令来切回用户态,iret指令则会从内核栈里弹出寄存器EIP、CS、EFLAGS、ESP、SS的值,使得栈康复到用户态的状况。
这样,体系调用fork经过int 0x80调用system_call,依据当时eax的值在体系调用表中挑选适宜的函数持续履行。咱们看下system_call片段:
[cpp] view plain copy
ENTRY(system_call)
…
SAVE_ALL
…
cmpl $(nr_syscalls), x
jae syscall_badsys
……
在这儿一开始运用宏SAVE_ALL将各种寄存器压入栈中,避免它们的值被后续履行的代码所掩盖。然后接下来运用cmpl指令比较eax和nr_syscalls(比最大的有用体系调用号大一的值),因而,假如不在有用体系调用号的范围内,就会跳转到syscall_badsys履行。假如体系调用号是有用的,则:
[cpp] view plain copy
system_call:
call *sys_call_table(0, x, 4)
……
RESTORE_REGS
……
iret
确认体系调用号有用而且保存寄存器之后,接下来要履行的便是调用 *sys_call_table(0,x,4)来查找中止服务程序并履行。履行完毕只要运用宏RESTORE_REGS来康复之前被SAVE_ALL保存的寄存器。最终经过iret从中止处理程序中回来。