添加一个Linux内核系统调用

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本篇基于如下上下文

Linux 4.14
glibc-2.31
ubuntu-base-16.04-core-armhf.tar.gz
QEMU+Arm vexpress-a9

进行分析。
在阅读本篇之前,建议先阅读下我的另一篇博文:Linux系统调用实现简析 ,了解一下系统调用是怎么工作的。

3. 添加一个系统调用

在给出如何添加一个系统调用的步骤之前,先了解一下内核编译系统是如何处理系统调用的。

3.1 系统调用相关文件的生成过程

ARM32 架构下,Linux 内核使用目录 arch/arm/tools 下的 Makefile 、 脚本、数据文件,来自动生成系统调用表和系统调用编号文件。
先看一下 arch/arm/tools/Makefile

gen := arch/$(ARCH)/include/generated
kapi := $(gen)/asm
uapi := $(gen)/uapi/asm
syshdr := $(srctree)/$(src)/syscallhdr.sh
sysnr := $(srctree)/$(src)/syscallnr.sh
systbl := $(srctree)/$(src)/syscalltbl.sh
# arch/arm/tools/syscall.tbl 定义了系统调用列表
syscall := $(srctree)/$(src)/syscall.tbl

gen-y := $(gen)/calls-oabi.S # OABI 规范系统调用表项生成文件
gen-y += $(gen)/calls-eabi.S # EABI 规范系统调用表项生成文件
kapi-hdrs-y := $(kapi)/unistd-nr.h # 系统调用数目定义(数目对齐到 4)生成文件
kapi-hdrs-y += $(kapi)/mach-types.h
uapi-hdrs-y := $(uapi)/unistd-common.h # OABI 和 EABI 公用系统调用的编号定义生成文件
uapi-hdrs-y += $(uapi)/unistd-oabi.h # OABI 系统调用编号定义生成文件
uapi-hdrs-y += $(uapi)/unistd-eabi.h # EABI 系统调用编号定义生成文件

targets += $(addprefix ../../../,$(gen-y) $(kapi-hdrs-y) $(uapi-hdrs-y))

PHONY += kapi uapi

kapi:	$(kapi-hdrs-y) $(gen-y)

uapi:	$(uapi-hdrs-y)

...

# 用来生成文件 arch/arm/include/generated/uapi/asm/unistd-common.h 的规则
syshdr_abi_unistd-common := common
$(uapi)/unistd-common.h: $(syscall) $(syshdr) FORCE
	$(call if_changed,syshdr)

# 用来生成文件 arch/arm/include/generated/uapi/asm/unistd-oabi.h 的规则
syshdr_abi_unistd-oabi := oabi
$(uapi)/unistd-oabi.h: $(syscall) $(syshdr) FORCE
	$(call if_changed,syshdr)

# 用来生成文件 arch/arm/include/generated/uapi/asm/unistd-eabi.h 的规则
syshdr_abi_unistd-eabi := eabi
$(uapi)/unistd-eabi.h: $(syscall) $(syshdr) FORCE
	$(call if_changed,syshdr)

# 用来生成文件 arch/arm/include/generated/asm/unistd-nr.h 的规则
sysnr_abi_unistd-nr := common,oabi,eabi,compat
$(kapi)/unistd-nr.h: $(syscall) $(sysnr) FORCE
	$(call if_changed,sysnr)

# 用来生成文件 arch/arm/include/generated/calls-oabi.S 的规则
systbl_abi_calls-oabi := common,oabi
$(gen)/calls-oabi.S: $(syscall) $(systbl) FORCE
	$(call if_changed,systbl)

# 用来生成文件 arch/arm/include/generated/calls-eabi.S 的规则
systbl_abi_calls-eabi := common,eabi
$(gen)/calls-eabi.S: $(syscall) $(systbl) FORCE
	$(call if_changed,systbl)

上述 Makefile 描述了 ARM32 架构下系统调用编译的过程。
ARM32 架构用文件 arch/arm/tools/syscall.tbl 定义系统调用表:

#
# Linux system call numbers and entry vectors
#
# The format is:
# <num>	<abi>	<name>			[<entry point>			[<oabi compat entry point>]]
#
# Where abi is:
#  common - for system calls shared between oabi and eabi (may have compat)
#  oabi   - for oabi-only system calls (may have compat)
#  eabi   - for eabi-only system calls
#
# For each syscall number, "common" is mutually exclusive with oabi and eabi
#
0	common	restart_syscall		sys_restart_syscall
1	common	exit			sys_exit
2	common	fork			sys_fork
3	common	read			sys_read
4	common	write			sys_write
5	common	open			sys_open
6	common	close			sys_close
# 7 was sys_waitpid
...
13	oabi	time			sys_time
...
397	common	statx			sys_statx
...

文件的开头已经说明了每一项各个域的含义,在此不做赘述。另外:

1. 标记为 `common` 的系统调用的 编号 和 函数入口,会同时输出到文
   件 `arch/arm/include/generated/calls-oabi.S``arch/arm/include/generated/calls-eabi.S`2. 而标记为 `oabi``eabi` 的系统调用的 编号 和 函数入口,分别
   出现在文件 `arch/arm/include/generated/calls-oabi.S``arch/arm/include/generated/calls-eabi.S`3. 另外,系统调用的编号不一定是连续的。

总结起来,系统调用表编译生成过程如下:

                         --> unistd-common.h, unistd-{eabi,oabi}.h
             Makefile  /
syscall.tbl ----------> ---> unistd-nr.h
                       \
                         --> calls-{eabi,oabi}.S

3.2 系统调用的实现和使用

3.2.1 添加系统调用内核侧的工作:实现

有了上面的基础,添加一个内核系统调用就变得简单起来了。只要两步:

1. 在文件 `arch/arm/tools/syscall.tbl` 最后面添加一项;
2. 实现添加项的系统调用调用接口。

我们添加一个系统调用号为 398、函数入口为 sys_foo 的系统调用,对系统调用表文件 arch/arm/tools/syscall.tbl 修改如下:

#
# Linux system call numbers and entry vectors
#
# The format is:
# 					[			[]]
#
# Where abi is:
#  common - for system calls shared between oabi and eabi (may have compat)
#  oabi   - for oabi-only system calls (may have compat)
#  eabi   - for eabi-only system calls
#
# For each syscall number, "common" is mutually exclusive with oabi and eabi
#
0	common	restart_syscall		sys_restart_syscall
1	common	exit			sys_exit
2	common	fork			sys_fork
3	common	read			sys_read
4	common	write			sys_write
...
398	common	foo			sys_foo

在文件 arch/arm/tools/syscall.tbl 中添加一项,仅仅是在 sys_call_table[] 中添加了一项,只完成了添加内核系统调用的步骤1。
接下来,我们实现系统调用入口 sys_foo() 的代码。我不想再建立一个新的文件,就借鸡生蛋,在 kernel/sys_ni.c 中实现了它:

...
#if 1
#include 

asmlinkage long sys_foo(void);

asmlinkage long sys_foo(void)
{
        printk(KERN_INFO "%s() called\n", __func__);
        return 0;
}
#endif
...

上面已经完成了添加系统调用内核侧的工作。接下来,我们看一下用户空间的工作,以及系统调用的使用示范。

3.2.2 添加系统调用用户侧的工作:使用

通常我们使用系统调用,是通过 glibc 间接发起的,在前文提到的我的另一篇博文里有过分析。但我们新添的系统调用 sys_foo()glibc 是不知道的,没法为我们做接口封装,但 glibc 为这种情形提供另一个接口 syscall() ,专门用来发起系统调用,我们先来看一下该函数的原型:

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include 
#include    /* For SYS_xxx definitions */

long syscall(long number, ...);

其中参数 number 为系统调用编号,剩下的是系统调用的参数列表。
当然,我们也可以采用 glibc 封装其它系统调用的方法,自己封装对系统调用的接口,但这比不上使用 syscall() 来得简洁方便,可移植性也更差。
我们来示范一下使用 syscall() 调用 sys_foo() 的代码:

/*
 * syscall_test.c
 */

#include 
#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include 
#include    /* For SYS_xxx definitions */

#define __NR_foo 398

int main(void)
{
	(void)syscall(__NR_foo);

	return 0;
}

编译上面的测试代码:

arm-linux-gnueabihf-gcc -o syscall_test syscall_test.c

将编译生成的文件 syscall_test 拷贝到根文件系统的 /usr/bin 目录下,然后用 QEMU 启动修改后的内核,系统运行起来后,运行命令:

$ syscall_test
$ dmesg | tail -1
[   75.647642] sys_foo() called

可以看到,我们新添加的系统调用 sys_foo() 运行起来了。另外,我们还可以用 strace 追踪新添加的系统调用:

$ strace syscall_test
execve("/usr/bin/syscall_test", ["syscall_test"], [/* 13 vars */]) = 0
brk(NULL)                               = 0x21000
uname({sysname="Linux", nodename="qemu-ubuntu", ...}) = 0
...
syscall_398(0, 0x1, 0, 0, 0x76e708ab, 0x76f42000) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

我们前面实现系统调用的方式有点不太“正规”,通常是用宏 SYSCALL_DEFINEx() 宏来定义系统调用。SYSCALL_DEFINEx() 为系统调用添加了 syscall_metadata 数据,其中记录了系统调用的名字字串等信息,供 ftrace 使用。我们看一下宏 SYSCALL_DEFINEx() 的定义:

/* include/linux/syscalls.h */

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define SYSCALL_METADATA(sname, nb, ...)			\
	static const char *types_##sname[] = {			\
		__MAP(nb,__SC_STR_TDECL,__VA_ARGS__)		\
	};							\
	static const char *args_##sname[] = {			\
		__MAP(nb,__SC_STR_ADECL,__VA_ARGS__)		\
	};							\
	SYSCALL_TRACE_ENTER_EVENT(sname);			\
	SYSCALL_TRACE_EXIT_EVENT(sname);			\
	static struct syscall_metadata __used			\
	  __syscall_meta_##sname = {				\
		.name 		= "sys"#sname,			\
		.syscall_nr	= -1,	/* Filled in at boot */	\
		.nb_args 	= nb,				\
		.types		= nb ? types_##sname : NULL,	\
		.args		= nb ? args_##sname : NULL,	\
		.enter_event	= &event_enter_##sname,		\
		.exit_event	= &event_exit_##sname,		\
		.enter_fields	= LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
	};							\
	static struct syscall_metadata __used			\
	  __attribute__((section("__syscalls_metadata")))	\
	 *__p_syscall_meta_##sname = &__syscall_meta_##sname;

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...)					\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(SyS##name))));		\
	static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));	\
	asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));	\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

观察 __SYSCALL_DEFINEx() 宏,还发现了一个小秘密,原来带参数的系统调用,它们还有另外一个名字 SyS_xxx() ,如 sys_write() 的另一个名字是 SyS_write() ,这也是为什么我们在 Kernel panic 中看到的是 SyS_write(),而不是 sys_write() 的原因。
最后,我们简单的看一下 ARM32 架构的 syscall() 的实现,看它又是怎么发起系统调用的。

/* sysdeps/unix/sysv/linux/arm/syscall.S */

ENTRY (syscall)
	mov	ip, sp
	push	{r4, r5, r6, r7} # 将 r4~r7 压到堆栈 (sp 变化)
	cfi_adjust_cfa_offset (16)
	cfi_rel_offset (r4, 0)
	cfi_rel_offset (r5, 4)
	cfi_rel_offset (r6, 8)
	cfi_rel_offset (r7, 12)
	mov	r7, r0 // 从 r7 传递系统调用号
	mov	r0, r1 // 从 r0 传递参数 1#
	mov	r1, r2 // 从 r1 传递参数 2#
	mov	r2, r3 // 从 r2 传递参数 3#
	ldmfd	ip, {r3, r4, r5, r6} # 从寄存器 r3~r6 ,传递参数 4#~7#
	swi	0x0 // 通过 swi 指令发起系统调用
	pop	{r4, r5, r6, r7} # 平衡堆栈
	cfi_adjust_cfa_offset (-16)
	cfi_restore (r4)
	cfi_restore (r5)
	cfi_restore (r6)
	cfi_restore (r7)
	cmn	r0, #4096
	it	cc
	RETINSTR(cc, lr)
	b	PLTJMP(syscall_error)
PSEUDO_END (syscall)

可以看到,syscall() 的实现逻辑是:

1. 使用 r0~r6 传递系统调用参数,这暗示着系统调用的最大参数个数为 7 个;
2. 使用 r7 传递系统调用号;
3. 通过 swi 指令发起系统调用。

你可能感兴趣的:(Linux,#,内核,linux)