1 min read

系统调用

1 .简介

系统调用就是操作系统提供给用户态应用与硬件交互的一组接口。在用户空间和硬件之间添加一个中间层(系统调用)主要的作用有:

  1. 为用户空间提供抽象接口,用户不需要关心硬件种类介质等。
  2. 保障系统的稳定和安全,避免用户错误的使用硬件危害系统或者窃取其他进程的资源。
  3. 由于Linux进程都是运行在虚拟系统中,如果操作系统对用户访问硬件一无所知,就几乎无法实现多任务和虚拟内存。

unix_arch

图片来源《Advanced Programming in the UNIX Environment, 3rd Edition》

2. 三种使用系统调用方式

syscall_method

2.1 软件中断

interrupt

没有外界打扰的情况下处理器会一直执行给定指令,中断就是打断处理器的执行并且告诉他先执行另一段指令,执行完毕再接着执行中断前的指令。从中断指令发出的对象可以分为硬件中断和软件中断。

  • 硬件中断就是硬件通过传输电信号到中断控制器的输入引脚,中断控制器收到电信号之后会给处理器发送一个电信号,处理器一经检测到电信号之后就中断当前工作转而处理中断。之后会通知操作系统已经产生中断,进而操作系统可以处理这个中断了。
  • 软件中断就是处理器执行特定指令时触发的中断,之后也会通知操作系统。
  • 除了系统调用,还有中断下半部tasklet也是用软件中断实现的。

在x86的机器上可以使用$INT$指令触发软件中断,Linux早期的时候就是使用软件中断来处理系统调用,中断号为128。 软件中断执行系统调用的流程为:

  1. 用户将中断号放入$eax$寄存器,前六个参数按顺序放入$ebx$、$ecx$、$edx$、$esi$、$edi$、$ebp$寄存器,六个以上的情况,需要把所有参数放在用户空间的一段连续内存中(类似用struct传参),然后将指向该内存区域的指针放入$ebx$中。
  2. 执行$int$ 0x80指令,处理器在中断向量表(IDT)中查找对应的中端处理程序,执行中断处理程序(操作系统由ring3进入ring0)entry_INT80_32:

interrupt_call
下面就是一个通过软件中断调用write系统调用的例子:

global _start

section .text
_start:
    mov eax,4      ; system call number
    mov ebx,1      ; args 1: fd=1(STDOUT)
    mov ecx,msg    ; args 2: "Hello World!"
    mov edx,msglen ; args 3: len("Hello World!")
    int 0x80       ; soft interrupt
    mov eax,1      ; syscall exit number
    mov ebx,0      ; args 1: 0 (exit(0))
    int 0x80       ; soft interrupt

section .rodata
    msg: db "Hello, World!", 10
    msglen: equ $ - msg 
nasm -g -f elf64 -o sys_call.o sys_call.s
ld -o sys_call sys_call.o

2.2 汇编指令

由于中断实现的系统调用在个别处理器上表现非常差,Linux在较新版本上使用了intel和amd上实现的快速系统调用指令syscall/sysret(64)和sysenter/sysexit(32)。这也是目前最常用的系统调用方式。

asm

具体流程为:

  1. cpu初始化cpu_init 调用 syscall_init
  2. syscall_init 将系统调用处理函数entry_SYSCALL_64entry_SYSENTER_32注册到MSR寄存器,MSR寄存器是用于控制CPU运行、功能开关、调试、跟踪程序执行、监测CPU性能等方面的寄存器。
  3. 触发系统调用后,它会在MSR中读取需要执行的函数然后执行。
  4. 剩余流程和软件中断方式差不多。

asm_call

2.3 vDOS

Linux平台的用户使用系统调用的方式大多是通过libc, 由于libc库要兼容BSD,SysV Windows等平台,所以每当Linux新增系统调用时,libc库都要间隔一段时间才会支持。并且有的用户升级Linux时并不会顺带升级libc,导致双方都带有沉重历史包袱。

后来Linux实现了快速系统调用vsyscall,内核提供.so通过动态链接直接map到进程空间里供用户使用,但是vsyscall有一个风险点——map 的起始地址固定(0xffffffffff600000),有潜在的安全风险。

为了改善vsyscall的局限性,设计了vDSO。但为了兼容vsyscall现在还保留着。

vDSO利用ASLR增强里安全性,随机地址。

可以看到Linux中的进程大多包含vDSO的动态库

➜  ~ ldd /bin/cat 
	linux-vdso.so.1 =>  (0x00007ffc03be0000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f52236dd000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f5223aa7000)

但是并不存在实际的.so文件,vsyscall以及vDOS将系统调用变为函数调用,并把他们映射到用户空间,明显的提高了系统调用的性能。 Linux 2.6时,vsyscall就支持了clock_gettime, gettimeofday, time。vdso.lds.S

vdso

看到这里的时候突然想到,刚参加工作的时候一位前辈和我说获取时间可以不使用系统调用,当时一脸懵逼,现在想想确实知道的太少。一些知识不是需要多高的智商才能学到,而是就摆在那,看了就不知道,不看就不知道。

3. 总结

系统调用是用户和硬件交互的媒介

  1. 软中断实现是最初Linux实现系统调用方式,但现在还有使用的方式,例如golang在一些架构上系统调用还是使用软中断的方式,因为golang团队在做基准测试的时候发现,软中断方式和快速指令方式效率差不多。runtime, syscall: use int $0x80 to invoke syscalls on android/386
  2. 快速汇编指令,intel和amd专门用于系统调用的指令。
  3. vsyscall和vDSO通过动态库的方式。