系统调用与API

一、系统调用介绍

1.什么是系统调用

在现代操作系统中,程序运行的时候,本身并没有权利访问多少系统资源,系统有限的资源有可能别多个不同的程序同时访问,为了保护系统资源,让应用程序有能力访问系统资源,每个操作系统都提供了一套接口,以供应用程序使用。这些接口往往通过系统中断来实现。比如Linux使用0x80号中断作为系统调用的入口,window采用0x2E号中断作为系统调用接口。

2.Linux 系统调用

在X86下,系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号,比如EAX=1表示退出进程(exit),EAX=2表示创建进程(fork),EAX=3表示读取IO(read);EAX=4表示写文件或IO(write)等,每个系统调用都对应于内核源代码中的一个函数,它们都是以“sys_”开头的,比如exit调用对应内核中的sys_exit函数。当系统调用返回时,EAX又作为调用结果的返回值。Linux系统有300多个系统调用,这些系统调用都可以在程序里直接使用,它的C语言形式被定义在”/user/include/unistd.h”

二、系统调用原理

1.特权级与中断

现代的CPU常常可以在多种截然不同的特权级别下执行指令,在现代操作系统中也据此有两种特权级别,分别为用户模式(User Mode)内核模式(Kernel Mode ),也被称为用户态内核态。运行在高特权级的代码将自己降低至低特权是允许的,但反过来低特权级的代码将自己提高至高特权级则不是轻易就能进行的。
操作系统一般是通过中断(interrupt)来从用户态切换到内核态。中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。
中断一般具有两个属性,一个称为中断号(从0开始),一个称为中断处理程序(Iterrupt Service Routine,ISR)。不同的中断具有不同的中断号,而同时一个中断处理程序对应一个中断号。在内核中,有一个数组称为中断向量表(Interrupt Vector Table),这个数组的第n项包含了指向第n号中断的中断处理程序的指针。当中断到来时,CPU会暂停当前行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU会继续执行之前的代码。

由于中断号有限,系统用一个或几个中断号来对应所有的系统调用。i386 下Window里绝大多数系统调用都是由int 0x2e 来触发的,而Linux则使用int 0x80来触发所有的系统调用。Linux的系统调用会将系统调用号放在某个固定的寄存器中,对应的中断代码会取得这个系统调用号,并且调用正确的函数。

2.基于int的Linux 的经典系统调用实现

这里写图片描述
1.触发中断
首先当程序在代码里调用一个系统调用时,是以一个函数的形式调用的,例如程序调用fork:

int main(){
    fork();
}

fork函数是一个对系统调用fork的封装,可以用下列宏来定义它:

_syscall0(pid_t,fork);

_syscall()是一个宏函数,用于定义一个没有参数的系统调用的封装。它的第一个参数为这个系统调用的返回值类型,这里为 pid_t,是一个Linux自定义类型,代表进程的id。_syscall()的第二个参数是系统调用的名称,_syscall()展开之后会形成一个与系统调用名称同名的函数。下面的代码是i386版本的syscall()定义:

#define _syscall0(type,name)

type name(void)
{
 long __res;
 __asm__ volatile("int  $0x80"
     : "=a"(__res)
     : "0"(__NR_##name));
 __syscall_return(type,__res);

}

对于syscall(pid_t,fork),上面的宏将展开为:

pid_t fork(void)
{
    long __res;
    __asm__ volatile(" int $0x80"
    : "=a" (__res)
    : "0" (__NR_fork));
    __syscall_return(pid_t,__res);
}

上面的格式为AT&T格式的汇编:
1.__asm__是一个gcc的关键字,表示接下来将要嵌入汇编代码。
2.__asm__的第一个参数是一个字符串,代表汇编代码的文本。这里的汇编代码只有一句 int $0x80,这就要调用0x80号中断。
3.=a __res表示调用eax(a 表示eax) 输出返回数据并存储在__res里。
4.“0”__NR_##name表示__NR_##name为输入,“0”指示由编译器选择和输出相同的寄存器(即eax)来传递参数
更直观的,可以把这段汇编改写为更为可读的格式:
main->fork:

pid_t fork(void){
    long __res;
    $eax =__NR_fork
    int $0x80
    __res = $eax
    __syscall_return(pid_t,__res);
}

__NR_fork是一个宏,表示fork系统调用的调用号,对于x86体系结构,该宏的定义可以在Linux/include/asm-x86/unistd_32.h里找到:

#define __NR_restart_syscall 0
#define __NR_exit    1
#define __NR_fork    2
........

而__syscall_return 是另一个宏,定义如下:

#define __syscallz_return(type,res)

do{
    if((unsigned long)(res)>=(unsigned long)(-125)){
    errno=-(res);
    res = -1;
}
return (type)(res)
}while(0)

_syscall_return负责将系统调用的返回信息存储在errno中,将调用失败信息以-1返回。

fork:
mov eax,2
int 0x80
cmp eax,0xFFFFFF83
jb syscall_noerror
neg eax
mov errno,eax
mov eax,0xFFFFFFFF
syscall_noerror:
ret

如果系统调用本身有参数要如何实现呢?x86 Linux 下的syscall,用于带一个参数的系统调用:

#define _syscall112(type,name,type1,arg1)

type name(type1,arg1)
{
    long __res;
    __asm__ volatile(
    "int $0x80"
    :  "=a" (__res)
    :  "0" (__NR_##name),"b"((long)(arg1)));
 __syscall_return(type,__res);
}

“b”(long)(arg1),这句的意思是先把arg1强制转化为long,然后存放在EBX(b 代表EBX)里作为输入。

push ebx
eax = __NR_##name
ebx = arg1
int 0x80
__res = eax
pop ebx

系统调用有1个参数,那么参数通过EBX来传入。x86下Linux支持的系统调用参数至多有6个,分别使用6个寄存器来传递,它们分别是EBX、ECX、EDX、ESI、EDI和EBP。
当用户调用某个系统调用的时候,实际是执行了以上一段汇编代码。CPU执行到int $0x80时,会保存现场以便恢复,接着会将特权状态切换到内核状态。

2.切换堆栈
在实际执行中断向量表中的第0x80号元素所对应的函数之前,CPU还要进行相应栈的切换。在Linux中,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,互不干扰。当执行中断时,当前栈需要从用户栈切换到内核栈,返回时,需要从内核栈切换回用户栈。

所谓的"当前栈",指的是ESP的值所在的栈空间。如果ESP的值位于用户栈的范围内,那么程序的当前栈就是用户栈,反之亦然。此外,寄存器SS的值还应该指向栈所在的页。
当前栈由用户栈切换为内核栈的实际行为就是:
(1)保存当前的ESP、SS的值
(2)将ESP、SS的值设置为内核栈的相对值。
反过来,将当前栈由内核栈切换为用户栈的实际行为则是:
(1)恢复原来ESP、SS的值。
(2)用户态的ESP和SS的值保存在内核栈上。这一行为由i386的中断指令自动的由硬件完成。
当0x80号中断发生的时候,CPU除了切入内核态之外,还会自动完成下列几件事情:
(1)找到当前进程的内核栈(每一个进程都有自己的内核栈)
(2)在内核栈中依次压入用户态的寄存器SS、ESP、EFLAGS、CS、EIP。
 当内核从系统调用中返回的时候,需要调用iret指令来回到用户态,iret指令则会从内核栈里弹出寄存器SS、ESP、EFLAGS\CS、EIP的值,使得栈恢复到用户态的状态。

这里写图片描述
3.中断处理程序
在int指令合理地切换了栈之后,程序就切换到了中断向量表中记录的0x80号中断处理程序。
这里写图片描述
i386的中断向量表在Linux源代码的Linux/arch/i386/kernel/trap.c里可见一部分。文件的末尾 trap_init函数用于初始化中断向量表。在trap_init函数结尾最后一行set_system_gate(SYSCALL_VECTOR,$system_call)设置了系统调用中断号。Linux/include/asm-i386/mach-default/irq_vectors.h里可以找到SYSCALL_VECTOR的定义:

#define SYSCALL_VECTOR 0X80

用户调用 int 0x80 之后,最后执行的函数是system_call,该函数在Linux/arch/i386/kernel/entry.S里可以找到定义。
main -> fork -> int 0x80 ->system_call

ENTRY(system_call)
    ......
    SAVE_ALL//
    ......
    cmpl $(nr_syscalls),%eax
    jae syscall_badsys

上面是system_call的开头,在这里一开始使用宏将SAVE_ALL将各种寄存器压入栈中,然后比较nr_syscalls与eax的值,nr_syscalls是比最大系统调用号大1的值,小于它的话跳到syscall_badsys执行:

syscall_call:
    call *sys_call_table(0,%eax,4)
    ...
  RESTORE_REGS
    ...
   iret

sys_call_table(0,%eax,4)定义在 Linux/arch/i386/kernel/systable.S里

.data
ENTRY(sys_call_table)
    .long sys_restart_syscall
    .long sys_exit
    .long sys_fork
    .long sys_read
    .long sys_write
    ......

这就是Linux的i386系统调用表。*sys_call_table(0,%eax,4)指的是sys_call_table上偏移量为0+%eax*4上的那个元素的值指向的函数。
内核里的系统调用函数往往以sys_加上系统调用函数名来名,例如
sys_fork、sys_open等。
这里写图片描述
系统调用从用户那里获取参数的方式

用户调用系统调用时,根据参数的数量不同,依次将参数放入EBX、ECX、EDX、ESI、EDI和EBP这六个寄存器中。
而在进入系统调用的服务程序system_call的时候,system_call调用了一个宏SAVE_ALL来保存各个寄存器的值到栈中。SAVE_ALL的大致内容如下:

#define SAVE_ALL
 ......
 push %eax
 push %ebp
 push %esi
 push %edx
 push %ecx
 push %ebx
 mov $(KERNEL_DS),%edx
 mov %edx,%ds
 mov %edx,%es

入栈顺序与函数参数顺序一样
参数被放在了栈上。
这里写图片描述
另一反面,所有以sys开头的内核系统调用函数,都有一个asmlinkage的标识

asmlinkage pid_t sys_fork(void);

这个扩展关键字是让这个函数只从栈上来获取参数
这里写图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值