第十章 系统调用
系统调用简介
Linux系统在x86架构下的系统调用由int 0x80中断完成,各通用寄存器用于传递参数,其中eax寄存器存储系统调用的调用号,常用的调用号如下表。
可使用man 2 read
查看read系统调用的详细说明。
但是,不同操作系统之间系统调用不兼容,为此,运行库对这些系统调用封装成统一的接口,使得相同的代码在不同操作系统下都可以被编译并产生一致的效果。
系统调用原理
以基于int的Linux经典系统调用为例。如下是调用fork系统调用的示意图。
触发中断
fork函数是对系统调用fork的封装,由如下宏代码定义。
1 | _syscall0(pid_t, fork); |
其中,_syscall0
是一个宏函数,用于定义一个没有参数的系统调用的封装。i386版本的_syscall0
定义如下。
1 |
|
对于_syscall0(type, name)
,上述宏会展开为如下代码。
1 | pid_t fork(void) |
其中,__NR_fork
是一个宏,表示fork系统调用的调用号;0指示编译器选择与输出相同的寄存器来传递参数,而=a表示将返回值存放在eax寄存器,于是eax寄存器也用来传递参数__NR_fork
,而中断服务程序正好从eax处取得系统调用号。
而如果调用的系统调用需要传递参数(x86架构下Linux支持的系统调用参数个数最多6个),分别使用6个寄存器(ebx、ecx、edx、esi、edi、ebp)来传送。
当CPU执行到int $0x80时,会保存现场以便恢复,然后将特权状态从用户态切换到内核态,然后CPU查找中断向量表第0x80号元素。
切换堆栈
int指令执行的第一步就是切换堆栈(进程拥有内核栈与用户栈),由于中断会让进程从用户态进入内核态,因此进程的当前栈也应该从用户栈切换为内核栈。具体的切换方法为:
- 将寄存器esp、ss压入内核栈;
- 将内核栈顶地址存入esp寄存器,将内核栈所在页的地址存入ss寄存器;
切换堆栈之后,int指令执行的下一步是将eflags、cs、eip寄存器压入内核栈。与之对应的,当进程从内核态返回用户态时,使用iret指令,它会从内核栈中弹出ss、esp、eflags、cs、eip的值。