这篇文章是《读薄「Linux 内核设计与实现」》系列文章的第 III 篇,本文主要讲了以下问题:系统调用的概念、系统调用的实现原理与过程以及如何在 Linux 中增加一个系统调用。
0x00 系统调用的概念
系统调用是为了和用户空间上的进程进行交互,内核提供的一组界面。
应用程序通过这组界面访问硬件和其他操作系统资源
完成对硬件和资源的访问控制
硬件设备的抽象(提供设备的独立性)
0x01 系统调用简介
I 常用系统调用
fork(), exec(), open(), read(), write(), close(),……
目前 Linux 系统调用 300 多个
II 应用程序及系统调用的层次关系
应用程序通过在用户空间实现的 API 而不是直接通过系统调用来编程
例:调用 printf() 函数时,应用程序、C 库和内核的关系:
应用程序调用 printf() -> C 库中的 printf() -> C 库中的 write() -> 内核中的 write() 系统调用
0x02 Linux 系统调用实现原理
I 相关概念
int 80H:软中断,通知内核的机制是靠软中断实现的,第128号中断处理程序
IVT(Interrupt Vector Table):中断向量表,包括所有中断程序入口地址,它固定存放于内存中(实模式下应用)
IDT(Interrupt Descriptor Table):中断描述符表,不固定内存位置,通过 IDTR 寄存器定位该表(保护模式下应用,int 80H 占据其中一项)
syscall table:系统调用表
系统调用号: 在 Linux 中,每个系统调用被赋予一个系统调用号,表示它在表中的编号
II 系统调用的加载
操作系统在加载时做的有关系统调用的加载:
int 80H 处理程序地址的加载:start_kernel()中的 trap_init()和 set_system_gate()
各系统调用处理程序的加载(entry.s)
III 系统调用过程(以 x86 为例)
首先,通过软中断陷入到 int 80h 中断中,促使系统切换到内核态去执行异常处理程序(系统调用处理程序);之后,系统通过读取 eax 寄存器的值来获取系统调用号;之后,系统通过读取寄存器来获取传递的参数(ebx, ecx, edx, esi, edi)按照顺序存放前五个参数,如果参数为6个或以上,则将其中一个寄存器的值指向内存空间;最后,执行相应系统调用代码,完成系统调用
IV 系统调用的参数验证
系统调用必须仔细检查他们所有参数是否合法有效,如果用户将不合法的参数传递给内核,那么系统的安全和稳定将面临极大考验。
权限验证:系统调用的调用者可以使用 capable() 函数来检查是否有权能对制定的资源进行操作
指针合法性验证:在接受一个用户空间的指针之前,内核需要验证:
- 指针指向的内存区域属于用户空间
- 指针指向的内存区域在进程的地址空间里
- 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,进程决不能绕过内存访问限制
0x03 如何增加一个系统调用
增加系统调用函数(/kernel/sys.c)
把系统调用函数入口添加到 sys_call_table(entry.s)
添加系统调用号
0x04 系统调用的意义
它为用户提供了一种硬件的抽象接口
在保证系统稳定和安全的前提下提供服务,避免应用程序恣意横行
本文的版权归作者 罗远航 所有,采用 Attribution-NonCommercial 3.0 License。任何人可以进行转载、分享,但不可在未经允许的情况下用于商业用途;转载请注明出处。感谢配合!