限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
本篇基于如下上下文
Linux 4.14
glibc-2.31
ubuntu-base-16.04-core-armhf.tar.gz
QEMU+Arm vexpress-a9
进行分析。
在阅读本篇之前,建议先阅读下我的另一篇博文:Linux系统调用实现简析 ,了解一下系统调用是怎么工作的。
在给出如何添加一个系统调用的步骤之前,先了解一下内核编译系统是如何处理系统调用的。
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
有了上面的基础,添加一个内核系统调用就变得简单起来了。只要两步:
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
...
上面已经完成了添加系统调用内核侧的工作。接下来,我们看一下用户空间的工作,以及系统调用的使用示范。
通常我们使用系统调用,是通过 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 指令发起系统调用。