文章目录
  1. 系统调用简介
  2. 系统调用原理
    1. 触发中断
    2. 切换堆栈

系统调用简介

Linux系统在x86架构下的系统调用由int 0x80中断完成,各通用寄存器用于传递参数,其中eax寄存器存储系统调用的调用号,常用的调用号如下表。

 

可使用man 2 read查看read系统调用的详细说明。

但是,不同操作系统之间系统调用不兼容,为此,运行库对这些系统调用封装成统一的接口,使得相同的代码在不同操作系统下都可以被编译并产生一致的效果。

系统调用原理

以基于int的Linux经典系统调用为例。如下是调用fork系统调用的示意图。

 

触发中断

fork函数是对系统调用fork的封装,由如下宏代码定义。

1
_syscall0(pid_t, fork);

其中,_syscall0是一个宏函数,用于定义一个没有参数的系统调用的封装。i386版本的_syscall0定义如下。

1
2
3
4
5
6
7
8
9
#define _syscall0(type, name)\
type name(void)\
{\
long __res;\
__asm__ volatile ("int $0x80"\
: "=a" (__res)\
: "0" (__NR_##name));\
__syscall_return(type,__res);\
}

对于_syscall0(type, name),上述宏会展开为如下代码。

1
2
3
4
5
6
7
8
pid_t fork(void)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_fork));
__syscall_return(type,__res);
}

其中,__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的值。