给 Linux 内核添加自己定义的系统调用

March 25, 2022 - 2 minute read -
Linux 内核

系统调用是操作系统提供给应用程序的接口,是由操作系统开发者定义在内核之中。所以我们可以自己编译内核并且注册自己的系统调用。

本人采用的系统是 Ubuntu20.04,内核版本为 5.10.56,体系结构为 x86_64。

添加系统调用需要在内核源码中操作,所以需要先下载一份内核源码,然后注册好系统调用之后把自己修改好的内核加载到系统中。所有版本的内核源代码都可以在Linux内核官方网站中找到,可以去自行下载。

1. 注册系统调用号

在 Linux 中,每个系统调用都被赋予了一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称。

内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在 sys_call_table 中。所以首先我们需要注册一个系统调用号,在系统调用表中填入对应的信息就算是注册了一个系统调用号。

每一种体系结构中,都明确定义了这个表,在本系统 x86_64 中,它定义于 arch/x86/entry/syscalls/syscall_64.tbl(不同体系结构是不一样的,需要找到自己对应的系统调用表)。

sys_call_table

系统调用表中每一项都以下由四个元素组成:

<number> <abi> <name> <entry point>

其中,<num> 代表系统调用号,例如在 x86_64 架构中 open 的系统调用号就是 5;<abi> 即 x86_64 架构的 ABI,其含义 application binary interface(应用程序二进制接口);<name> 是系统调用的名字;<entry point> 代表系统调用在内核的接口函数,在 <name> 前加 sys_ 即可。

result

在系统调用表中最后一项填入这四个元素即可注册系统调用。

2. 声明系统调用函数原型

为了保证添加的系统调用能被找到并且调用,需要在 include/linux/syscalls.h 中声明该系统调用的函数原型。

func_prototype

每个系统调用都对应一个内核服务例程来实现该系统调用的具体功能,其命名格式都是以 sys_ 开头。这是 Linux 中所有系统调用都应该遵守的命名规则,例如系统调用 bar() 在内核中也实现为 sys_bar() 函数。

函数声明中的 asmlinkage 限定词是一个编译指令,用于通知编译器仅从堆栈中提取该函数的参数,而不是从寄存器中,因为在执行服务例程之前系统已经将通过寄存器传递过来的参数值压入内核堆栈了。所有的系统调用都需要这个限定词。

其次,系统调用函数返回值类型是 long。为了保证 32 位和 64 位系统的兼容性,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为 int,在内核空间为 long

3. 添加系统调用函数的定义

声明了该系统调用的原型之后,需要实现该系统调用,这只要把它放进 kernel/ 下的一个相关文件就可以了,比如 sys.c,它包含了各种各样的系统调用,我这里就在 kernel/sys.c 中添加其定义。

syscall_def

SYSCALL_DEFINEn 是用来定义系统调用的一个宏,其中 n 为该系统调用参数的个数,例如这个定义是两个参数,即为 SYSCALL_DEFINE2,宏里的参数依次为函数名以及所有参数的参数类型和参数名,展开后的代码如下:

asmlinkage long sys_compareint(int arg1, int arg2)

printk() 函数运行在内核态,是在内核中运行的向控制台输出显示的函数,终端可能看不到输出,这与自身的操作系统有关。如果终端看不到输出则可以使用 dmesg -c 命令来打印当前系统的日志信息,即可看到 printk() 函数的输出。

4. 编译并安装此内核

往此内核源码树中添加完系统调用之后需要编译才能使用,而这样就需要编译整个内核。如果此版本的内核已在系统中安装并且没有修改此内核的其他东西,只添加了系统调用,则不需要执行编译并安装一个新内核的所有步骤,只需要执行以下步骤:

(1)配置内核

在内核源码树下执行如下命令:

make menuconfig

此时会出现一个配置窗口,如果是第一次安装此则可以使用系统中原有的配置或者使用默认配置,我这里直接使用之前安装此内核时的 .config 文件的配置即可,直接退出不作修改。

(2)编译内核

内核配置完后,编译内核,生成启动映像文件 bzlmage,位于 arch/x86_64/boot/bzlmage 。执行命令如下:

make ARCH=x86_64 bzImage -j4

此过程需要较长时间,由于开启了四线程编译,大约需要二十分钟即可。

(3)安装内核

由于此版本的内核已在系统中安装过,而且未对内核做其他修改,所以亲测可以跳过安装模块,直接安装内核即可。执行命令如下:

make install

(4)重启系统

由于新版的内核在安装完之后会自动配置 grub 引导程序,所以直接重启系统 reboot 即可。

重启系统并选择此内核进入系统之后,就可以使用新添加的系统调用了。

然后可以测试一下添加的系统调用:

example

通常,系统调用靠 C 库支持。用户程序通过包含标准头文件并和 C 库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但这里只将系统调用添加到了内核中,并不能使用 glibc 库。而对此,Linux 本身提供了一个 syscall() 函数,用于直接对系统调用进行访问。将想要调用的系统调用的系统调用号和参数传入 syscall() 中即可使用此系统调用。

参考资料

[1] OS 实验一 linux 内核编译及添加系统调用 - 知乎

[2] linux添加系统调用总结(内核版本4.4.4) - CSDN博客

[3] X86_64 架构增加一个系统调用 - BiscuitOS

[4] Linux 下系统调用的三种方法- hazir - 博客园