一:系统调用的基本原理
系统调用其实就是函数调用,只不过调用的是内核态的函数,但是我们知道,用户态是不能随意调用内核态的函数的,所以采用软中断的方式从用户态陷入到内核态。在内核中通过软中断0X80,系统会跳转到一个预设好的内核空间地址,它指向了系统调用处理程序(不要和系统调用服务例程混淆),这里指的是在entry.S文件中的system_call函数。就是说,所有的系统调用都会统一跳转到这个地址执行system_call函数,那么system_call函数如何派发它们到各自的服务例程呢?
我们知道每个系统调用都有一个系统调用号。同时,内核中一个有一个system_call_table数组,它是个函数指针数组,每个函数指针都指向了系统调用的服务例程。这个系统调用号是system_call_table的下标,用来指明到底要执行哪个系统调用。当int ox80的软中断执行时,系统调用号会被放进eax寄存器中,system_call函数可以读取eax寄存器获得系统调用号,将其乘以4得到偏移地址,以sys_call_table为基地址,基地址加上偏移地址就是应该执行的系统调用服务例程的地址。
二:简单方式添加一个系统调用
操作环境基于之前已经搭起来的基于QEMU的linux-5.3.7内核的虚拟环境,搭建过程可以参考这里。
在5.x版本添加系统调用已经十分方便,基本上到了不需要知道原理的程度,简单流水账记一下。
2.1 添加系统调用声明
在5.x版本只需要在提供的工具框架(arch/arm/tools/syscall.tbl)写上自己想要添加的函数名,如下:436号为自己添加的调用
修改的文件:添加后执行make可以看到系统自动修改了一些文件,从编译情况可以看到
1. arch/arm/include/generated/asm/unistd-nr.h 自动 生成当前系统调用的总数: 由436变成了440,因为需要保证对齐,所以一次增加4
2. arch/arm/include/generated/uapi/asm/unistd-common.h 文件中自动生成了系统调用号:
3. 在arch/arm/include/generated目录的calls-eabi.S calls-oabi.S文件中添加函数入口:
2.2 添加系统嗲用实现
此时执行make命令工具框架会为我们添加必要的声明以使这个函数加到系统调用框架中,但是我们还没有添加实现,所以make会报错。
我们可以新建一个文件实现调用,或者在已有文件中添加函数实现。这里在已有文件中(sys.c)添加了实现以做演示。其实入参有几个并不影响,这个后面再说明。
添加完后在内核跟目录执行make重新编译内核。
2.3 添加用户侧执行系统调用
我们新建一个test.c文件调用436号系统调用
#include <stdio.h> #include <linux/kernel.h> #include <sys/syscall.h> #include <unistd.h> int main() { long ret = syscall(436); printf("return code is: %ld ", ret); return 0; }
使用与编译内核相同的交叉编译工具链进行编译,此例中为:arm-linux-gnueabi-gcc -o test test.c
将生成的可执行文件放到虚拟文件系统的opt文件夹下(此处参考搭建QEMU虚拟环境中第4节)。
2.4 启动QEMU并执行test
参考用户态的test.c和内核态的sys_my_syscall实现可知:
第一行为内核态的打印, 第二行为内核态返回后用户态的打印。
三:手动添加一个系统调用
手动添加其实就是把工具生成的自己手动添加一下,麻烦点在于位置比较混乱,而且理解为什么这么加就更麻烦,不知道来历所以就不便于记忆。
手动添加看第二章的3个文件就行,前文已经黑体标注(修改的文件:)一节。自己按照需求手动添加对应的三条记录就可以。
这几个文件在不同版本和不同处理器可能不一样,名字可能也由区别(如NATIVE在早期有个叫CALL的),但是基本思想一样:1. 增加系统调用号总数,2. 设置系统调用的symbol与调用号关联,3. 添加新增的调用到系统调用表。
四:系统调用怎么实现的
这个说来话长,需要从linux的启动设置说起。这里准备根据用户层执行一个系统调用的过程进行分析,其中调用表的生成过程涉及汇编可能较为复杂。