系统调用
系统调用的概念
系统调用是计算机程序请求操作系统内核服务的方式,包括硬件相关的服务(例如访问硬盘驱动器)、创建和执行新的进程和进程调度等等。系统调用提供了进程和操作系统间的必要接口。
在大多数操作系统中,系统调用只能被用户空间进程使用。而在某些操作系统中,比如在OS/360及其后续的一些操作系统中,有特权的系统代码也会触发系统调用。
系统调用的分类
系统调用大体上可分为5类:
- 进程控制
- 加载
- 执行
- 结束,中止
- 创建进程
- 结束进程
- 得到/设置进程属性
- 等待(时间、时间、信号)
- 内存的分配和去配
- 文件管理
- 文件的创建和删除
- 打开和关闭
- 读、写和重定位
- 得到/设置文件属性
- 设备管理
- 设备的请求和释放
- 读、写和重定位
- 得到/设置设备属性
- 设备的逻辑关联或去关联
- 信息维护
- 得到/设置时间或日期
- 得到/设置系统数据
- 得到/设置进程、文件或设备属性
- 通信
- 通信连接的创建和删除
- 发送、接收信息
- 转换状态信息
- 远程设备的关联或去关联
Linux系统调用:使用 int 0x80
Linux提供了200多个系统调用,通过汇编指令 int 0x80
实现,用系统调用号来区分入口函数。
Linux实现系统调用的基本过程是:
- 应用程序准备参数,发出调用请求;
- C库封装函数引导。该函数在Linux提供的标准C库,即
glibc
中。对应的封装函数由下列汇编指令实现(以读函数调用为例):
; NASM
; read(int fd, void *buffer, size_t nbytes)
mov eax, 3 ; read系统调用号为3
mov ebx, fd
mov ecx, buffer
mov edx, nbytes
int 0x80 ; 触发系统调用
- 执行系统调用。前两步在用户态工作,陷入后在内核态工作。系统调用处理程序根据系统调用号,按系统调用表中的偏移地址跳转,调用对应的内核函数;
- 系统调用完成相应功能,将返回值存入
eax
,返回到中断处理函数; - 系统调用返回。内核函数处理完毕后,库函数读寄存器(
eax
)返回值,并返回给应用程序。恢复现场。
应用程序调用系统调用的过程是:
- 把系统调用号存入
eax
; - 把函数参数存入其它通用寄存器(约定顺序为
ebx
、ecx
、edx
、esi
、edi
,更多的参数(通常不会出现这种情况)使用堆栈传递,也可以通过寄存器存放指向参数在用户空间的地址指针来传递); - 触发
0x80
号中断(int 0x80
)。 - 示例:
; NASM
; 向显示器输出hello, world
; write(int fd, const void *buffer, size_t nbytes)
; exit(int status)
global _start
section .text
_start:
mov eax, 4 ; write系统调用号为4
mov ebx, 1 ; 文件描述符1:标准输出stdout
mov ecx, message ; 要输出的信息
mov edx, message.len ; 要输出的长度
int 0x80
mov eax, 1 ; exit系统调用号为1
mov ebx, 0 ; 状态码0:正常退出
int 0x80
section .data
message:
db "hello, world", 10
.len equ $ - message
Linux系统调用实现机制
系统调用初始化
系统调用处理程序 system_call()
的入口地址放在系统的中断表述符表IDT(Interrupt Descriptor Table)中,Linux系统初始化时,由 trap_init()
将其填写完整,其设置系统调用处理程序的语句为:
set_system_gate(0x80, &system_call)
经过初始化以后,每当执行 int 0x80
指令时,产生一个异常使系统陷入内核空间并执行128号异常处理程序,即系统调用处理程序 system_call()
。
系统调用公共入口
system_call()
是所有系统调用的公共入口,其功能是保护现场,进行正确性检查,根据系统调用号跳转到具体的内核函数。内核函数执行完毕时需调用 ret_from_sys_call()
,这时完成返回用户空间前的最后检查,用 RESTORE_ALL
宏恢复现场并执行 iret
指令返回用户断点。
保护现场
- 硬件(CPU)保护:
ss
、esp
、eflags
、cs
、eip
,压入核心栈; - 软件(操作系统)保护
- 使用
SAVE_ALL
宏将寄存器压入堆栈,加载内核的ds
和es
,往edx
中放入$(_KERNEL_DS)
以指明使用内核数据段,把内核数据段选择符装入ds
和es
。注意:该宏压入寄存器的顺序不是随意的,而是和系统调用的参数传递密切相关; es
、ds
、eax
、ebp
、edi
、esi
、edx
、ecx
、ebx
,压入核心栈。
- 使用
系统调用处理时的核心栈内容:
硬件完成 |
---|
ss |
esp |
eflags |
cs |
eip |
软件完成 |
es |
ds |
eax |
ebp |
edi |
esi |
edx |
ecx |
ebx |
返回值传递
当内核函数返回到 system_call()
时, eax
中存放着内核函数的返回值。要将这个返回值传递给应用程序,内核先将 eax
放入原先 SAVE_ALL
宏保存 eax
的位置,这样当 system_call()
调用 RESTORE_ALL
恢复寄存器时, eax
便被恢复成系统调用的返回值,完成了返回值从内核空间到用户空间的传递。
系统调用号和系统调用表
系统调用的数量由 NR_syscalls
宏给定,每个系统调用所对应的编号已预先在系统文件中定义,且都用一个宏表示,其定义有如下形式:
#define _NR_exit 1
#define _NR_fork 2
#define _NR_read 3
...
Linux的系统调用号和内核函数映射关系的系统调用表也被预先定义在系统文件中,具有如下形式:
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 空项 */
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
...
内核函数入口地址为: eax * 4 + sys_call_table 。
参考
[1] 维基百科(英文) - 系统调用:Wikipedia - System call
[2] 维基百科(中文) - 系统调用:维基百科 - 系统调用
[3] 《操作系统教程(第五版)》(费翔林、骆斌编著,高等教育出版社):1.3.4 Linux系统调用及其实现机制
更多资料
[1] Linux系统调用表:Linux系统调用表
[2] NASM入门教程:NASM Tutorial
[3] NASM官方文档:NASM官方文档