目录
1. Linux中的各种接口
1.1 LSB标准
1.2 Linux API
1.2.1 概述
1.2.2 Linux内核系统调用接口
1.2.3 C标准库
1.3 Linux ABI
1.4 内核API
1.5 系统调用与各种接口的关系
1.5.1 系统调用与API的关系
1.5.2 系统调用与系统命令的关系
1.5.3 系统调用与内核函数的关系
2. 中断、异常和系统调用的比较
2.1 源头不同
2.2 服务响应方式不同
2.3 处理机制不同
3. 系统调用的基本概念
3.1 系统调用号
3.2 系统调用表
3.3 系统调用封装例程和服务例程
4. 系统调用处理流程
4.1 设置异常处理函数
4.2 触发软中断进入内核态
4.3 调用系统调用服务例程
4.4 系统调用返回
5. 添加新的系统调用
5.1 添加系统调用号
5.1 添加系统调用表项
5.3 实现系统调用服务例程
5.4 重新编译内核
5.5 编写用户态程序
① LSB即Linux Standards Base,是Linux标准化领域中事实上的标准
② 由于Linux的发行版众多,为了促进Linux不同发行版之间的兼容性,LSB开发了一系列标准,使各种软件可以在兼容LSB标准的系统上运行
Linux API是Linux内核与用户空间的API,也就是让用户空间的程序能够通过这个接口访问系统资源和内核提供的服务
Linux API由两部分组成:Linux内核系统调用接口和GNU C库(glibc库)中的例程
系统调用接口是内核中所有已实现的可用系统调用的集合
GNU C库是Linux内核系统调用接口的封装,其中包括POSIX兼容的应用函数调用和Linux专用的应用函数调用
目前最新的Linux内核5.0版本中系统调用大约有380个,GNU C库大约有2000个函数
说明:POSIX标准
POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX),POISX标准是针对API而不是针对系统调用的,他定义了操作系统应该为应用程序提供的接口标准,并不涉及对应的函数如何实现
ABI是一些列约定的集合,可以说调用惯例(calling convention)就是ABI。ABI是和具体的CPU架构和OS相关的,具体而言,ABI包括以下内容,
① 一个特定的处理器指令集
② 函数调用惯例
③ 系统调用方式
④ 可执行文件的格式(e.g. ELF、PE)
说明:什么是函数调用惯例和系统调用方式
Linux提供了一个syscall函数,用来根据系统调用号直接调用系统调用,在该函数的man手册中说明了不同体系结构触发软中断的命令以及系统调用的传参方式,其实这就是所谓的ABI
① 内核API主要是内核中标记为EXPORT_SYMBOL的函数,这些函数主要是为了内核模块的编写而提供的
② 收到内核版本更迭的影响,内核API并不稳定,3.x版本内核的模块可能在4.x版本上就无法使用
由于API是对系统调用的封装,所以API和系统调用之间可能存在如下几种关系,
① API和系统调用形式一致
e.g. read函数和read系统调用
② 几个不同的API内部使用了同一个系统调用
e.g. malloc / calloc和free函数的实现都调用了brk系统调用
③ 一个API内部使用了多个系统调用
e.g. malloc函数的实现根据要分配内存的大小,可能使用brk系统调用,也可能使用mmap系统调用
④ API内部不使用系统调用
e.g. string.h中声明的各种字符串处理函数
系统命令相对API接口更高一层,每个系统命令都是一个可执行程序,使用strace命令可以查看系统命令使用的系统调用
说明:下图列出了一些系统命令与Linux各模块之间的关系
系统调用进入内核后,会找到各自对应的内核函数,这些内核函数被称为系统调用的服务例程
e.g. 系统调用getpid对应的服务例程为sys_getpid
根据中断和异常章节的学习可知,中断、异常和系统调用本质上属于一类,在处理方式上也类似,他们的差异体现在如下方面
① 中断是外设发出的请求
② 异常是应用程序意想不到的行为
③ 系统调用是应用程序请求操作系统提供服务
① 中断是异步的
② 异常是同步的
③ 系统调用既可以是异步的(e.g. 异步IO),也可以是同步的
① 中断服务程序在内核态运行,对用户是透明的
② 异常出现时,或者杀死进程,或者重新执行引起异常的指令
③ 系统调用是用户发出请求后等待操作系统的服务
① 定义在各体系结构的unistd.h中,在Linux 2.6.11 + 80386版本中为include/asm-i386/unistd.h
② 用来唯一的表示每个系统调用
③ 作为系统调用表的下标,当用户空间的进程执行一个系统调用时,该系统调用号作为参数传递,用来指明要执行的系统调用服务例程
① 在Linux 2.6.11 + 80386版本中,系统调用表sys_call_table定义在arch/i386/kernel/entry.S中
② 系统调用表是一个函数指针数组,使用系统调用号索引
说明:系统调用号和系统调用表一旦分配好就不能有改变,否则编译好的应用程序会因为调用到错误的系统调用而导致程序异常
① 系统调用服务例程就是内核态最终处理系统调用请求的函数
② 引入系统调用封装例程是因为用户空间的程序无法直接调用内核代码,因此在需要执行一个系统调用时,是通过软中断引发一个异常进入内核态,封装例程就是对这个过程的封装
在内核初始化的trap_init函数中,将系统调用异常处理函数设置为system_call
说明:将系统调用的IDT描述符设置为系统门,使得用户态可以穿过该门进入内核态
根据不同的体系结构,使用int $0x80 / syscall / svc指令即可触发系统调用对应的异常,并跳转到system_call函数运行
system_call函数在调用系统调用服务例程之前完成如下工作,
① 将系统调用号压栈(根据ABI,系统调用号通过eax寄存器传递)
② 调用SAVE_ALL将异常处理程序可以用到的所有寄存器保存到相应的栈中
此处注意3点,
a. 由于系统调用一定是从用户态切换到内核态,所以在进入异常处理时,硬件会进行栈的切换,并自动保存相关寄存器,如下图所示(系统调用中没有ERROR CODE)
b. SAVE_ALL中同时将ds和es装入内核数据段的段选择符
c. 根据之前的系统调用ABI说明,SAVE_ALL保存的寄存器中就包含了系统调用参数
③ 调用GET_THREAD_INFO,将当前进程thread_info的地址存放在ebp中
④ 判断系统调用号的合法性,如果合法则查找系统调用表并调用系统调用服务例程
① 系统调用服务例程根据系统调用号在sys_call_table中查表得到
② 所有系统调用服务例程的参数为struct pt_regs类型,该类型对应的就是寄存器在栈上的布局
这里就有一个问题了,我们知道在X86体系结构中,函数参数是优先通过寄存器传递的,那么给系统调用服务例程的参数是如何传递的呢 ? 这里的玄机就在于asmlinkage宏
__attribute__((regparm(n)))用于指定最多可以使用n个寄存器传递参数,超过n的参数将使用栈传递
对于系统调用,使用regparm(0),也就是所有参数均通过栈传递,内核中所有系统调用的实现都使用了这个修饰符
说明:由于ARM体系结构中定义了ATPCS标准,函数的前4个参数使用r0 ~ r4寄存器传递,所以asmlinkage宏实际上就是extern "C",并未使用regparm修饰
在调用实际的系统调用服务例程之前,会先将sp指针传递给r0
当服务例程执行结束时,system_call从eax获得他的返回值,并把这个返回值存放在栈中,让其位于用户态eax寄存器曾存放的位置,然后执行syscall_exit终止系统调用处理程序
当进程恢复到用户态执行时,就可以从eax中找到系统调用的返回值
说明:系统调用机制优化简介
在2.6的早期版本中,系统调用的实现使用的是int 0x80和iret命令,因为需要从用户态切换到内核态执行服务例程,然后再返回用户态,所以开销很大
为了加快系统调用的速度,随后引入了vsyscalls和vDSO机制,这两种机制都是从机制上对系统调用的速度进行了优化,但是使用软中断来进行系统调用需要进行特权级切换这一根本问题并没有解决
目前X86_64体系结构使用syscall / sysret指令实现系统调用,细节就不介绍了,因为我目前也不懂
说明2:定义系统调用服务例程
在后续的Linux内核版本中,提供了一组宏,用于定义系统调用服务例程
其中宏名中的数字表示服务例程的参数个数,下面举例说明
该宏可定义sys_write函数
说明:如上文所述,根据不同的体系结构与内核版本,要修改的文件位置会有所不同,甚至要修改的文件就不同,但是思路是一致的
修改体系结构目录中的unistd.h文件,增加系统调用号
注意同步修改NR_syscalls宏,该宏表示系统调用个数,会用于判断系统调用号的合法性
修改体系结构目录的entry.S文件,添加系统调用表项
可以新建文件,也可以在原有文件中添加系统调用服务例程。如果新建文件,注意修改Makefile
此处我们选择在kernel/sys.c中新增服务例程,
注意使用asmlinage修饰符
由于修改了内核源码,要使其生效,必须重新编译并布署内核
#include
#include
#define __NR_mysyscall 289
int main(void)
{
syscall(__NR_mysyscall);
return 0;
}
此处使用syscall + 系统调用号的方式调用系统调用,并未提供系统封装例程。在Linux 2.6.18版本之前,unistd.h中提供了一组__syscall宏用于定义系统调用封装例程,下面以定义3个参数的封装例程为例加以说明
其中type & name为封装例程的返回值与函数名,之后的type & arg对用于定义函数形参
在封装例程的实现中,就是按照ABI的约定将参数通过寄存器传输,并调用int $0x80触发软中断
这里特别说明下红框中的"0",他表示输入部分仍使用和输出部分相同的寄存器,次数就是用eax传输系统调用号(系统调用号由__NR_##name构成)
说明:增加有套路,定义需谨慎
增加一个系统调用并不难,他有一套比较规范的方法,难点是在实际应用中如何增加合适的系统调用。在绝大多数情况下,我们不会新增系统调用