既然需要添加系统调用,就需要准备好 Linux 的源码(一句废话:)。这里是基于 5.10.215 的内核版本修改的。
1 Linux 源码初步探索
初次(也不完全是初次)阅读 Linux 源码,目标比较明确,直接冲
sysycall
!
自己本身对 RISC-V 架构比较熟悉,直接在 arch/riscv
目录下找文件名中带 syscall
的,结果找到了一个 riscv/kernel/syscall_table.c
的代码。这份代码非常简单,(跟没写没啥区别,)代码如下:
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2009 Arnd Bergmann <arnd@arndb.de>
* Copyright (C) 2012 Regents of the University of California
*/
#include <linux/linkage.h>
#include <linux/syscalls.h>
#include <asm-generic/syscalls.h>
#include <asm/vdso.h>
#include <asm/syscall.h>
#undef __SYSCALL
#define __SYSCALL(nr, call) [nr] = (call),
void *sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include <asm/unistd.h>
};
这份代码最关键的部分就是 sys_call_table
这个函数指针数组了,再引入了一个 asm/unistd
的头文件;还有一点,就是这个 __SYSCALL
宏了。此外,还有几个头文件,那我们就从这些头文件入手。先来看看 asm/syscall.h
的代码。
目前为止,还不清楚 asm
是那个单词的缩写,但是可以说的是,asm
绝不在主目录下的 include
的文件夹里,而是在 arch/$ISA/include
的文件夹里。asm/syscall.h
定义了一些查看系统调用状态的函数,例如获取返回值 get_return_value
、获取参数 get_arguments
等系统调用。
在 asm/vdso.h
中,定义了 vdso
的数据结构体和 vdso
的标识宏。此外,还定义了一个 riscv_flush_icache
的系统调用声明。vDSO
用于一些快速的系统调用,而不需要切换到内核态。
vDSO (virtual dynamic shared object) 也是一种系统调用加速机制。 vDSO 和 vsyscall 的基本原理类似,都是通过映射到用户空间的代码和数据来模拟系统调用,来达到加速的目的。 而它们的主要区别在于: vDSO 是一个ELF 格式的动态库,拥有完整的符号表信息。
接下来的三个头文件,都是在 linux
源码的主目录下的 include
的文件夹里。先来看一下 asm-generic/syscalls.h
的代码,在这个代码里面声明了三个系统调用,分别是 mmap2
、mmap
、rt_sigreturn
系统调用。总结是一些架构相关的系统调用。
linux/syscalls.h
是最关键的一个头文件了。先是,声明了全部内核中的系统调用函数(如果改架构没有实现 syscall_wrapper
),还有与之相关的结构体。为了使这些系统调用和架构分离,定义了一个通用的系统调用的宏(SYSCALL_DEFINEx),以及相关辅助系统调用宏拆解的宏(MAP,SC_DECL 等)。更为细节的拆解宏,就在架构中的相关宏进行拆解(例如 x86 的 asm/syscall_wrapper.h
),这样就可以生成架构相关的系统调用了。了解到这个层次就知道那些代码是比较重要的的了。
最后一个头文件就是 linux/linkage.h
,一些为不同编译器提供定制的宏,可能用于汇编代码文件中。在此处理解 syscall,这部分代码不是特别重要。
2 x86 的系统调用
为了找到系统调用是怎么调用的,不妨先去找一下一个系统调用的实现函数,例如 sys_read
。通过命令行查找,但似乎并没有找到?
看了以下网上的 Linux 内核揭秘,结果发现,并不是简单地声明 sys_read
了得,而是使用了使用了 SYSCALL_DEFINEx
的宏。按照这个方式一搜,还真搜到了。这里以 chmod
系统调用为例子了,代码如下:
SYSCALL_DEFINE2(chmod, const char __user *, filename, umode_t, mode)
{
return do_fchmodat(AT_FDCWD, filename, mode);
}
为了理解代码,我们需要拆解这个 SYSCALL_DEFINE2
的宏。如 1 节当中所述,在 linux/syscall.h
中,定义了这个宏,代码为 #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
;宏里面又嵌套了一个 SYSCALL_DEFINEx
宏,这个宏就在刚才代码的附近了,但是这里并没有使用这个宏定义,应为 x86 架构有 syscall_wrap.h。再来看看 SYSCALL_DEFINEx
宏的代码:
#define __SYSCALL_DEFINEx(x, name, ...) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
__X64_SYS_STUBx(x, name, __VA_ARGS__) \
__IA32_SYS_STUBx(x, name, __VA_ARGS__) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##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 __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
使用 name
有点抽象,况且后面还有 sname
,我们把 name
替换成 _##read
,并剖出不是特别重要的部分,代码如下:
#define __SYSCALL_DEFINEx(x, name, ...) \
static long __se_sys_read(...); \
static inline long __do_sys_read(...); \
__X64_SYS_STUBx(x, name, __VA_ARGS__) \
__IA32_SYS_STUBx(x, name, __VA_ARGS__) \
static long __se_sys_read(...) { __do_sys_read(); } \
static inline long __do_sys_read(...)
最后一行的 __do_sys_read
的函数主体部分就在实现系统调用的地方了。在此处,可以明显的看出一个调用关系,__se_sys_read
调用了__do_sys_read
。接下来需要解密的是 __X64_SYS_STUBx
和 __IA32_SYS_STUBx
。这个两个分别对应的是 64 位架构和 32 位架构,有且只有一个会实现。这里选择的是 64 位架构,就只看 __X64_SYS_STUBx(x, name, __VA_ARGS__)
的宏。
#define __X64_SYS_STUBx(x, name, ...) \
__SYS_STUBx(x64, sys##name, \
SC_X86_64_REGS_TO_ARGS(x, __VA_ARGS__))
继续,套用了 __SYS_STUBx
的宏,代码如下:
#define __SYS_STUBx(abi, name, ...) \
long __##abi##_##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO); \
long __##abi##_##name(const struct pt_regs *regs) \
{ \
return __se_##name(__VA_ARGS__); \
}
化简之后,这里就是声明了一个 __x64_sys_read
函数,而且这个函数调用了 __se_sys_read
!完整的调用链条已经找到了,现在就是需要找到,什么地方调用了这个 __x64_sys_read
的函数。此时,想到了在第 1 节中讲到的 syscall_table
。
x86 的 syscall_table
是在 arch/x86/entry/syscall_64.c
文件中声明的,代码格式和 rsicv
的大同小异。不同之处在于,riscv 引用的是 asm/unistd.h
的头文件,而 x86 使用的是 asm/syscalls_64.h
的头文件。但问题是,我们并不能在 x86 的 include 下面找到这个头文件,但是可以在 generated
的文件夹中找到。可以猜测这部分是自动生成的。再看看 entry/syscall
目录,有一个 syscall_64.tbl
文件,文件内容的大概格式如下:
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
最后生成的代码文件格式如下:
__SYSCALL_COMMON(0, sys_read)
__SYSCALL_COMMON(1, sys_write)
__SYSCALL_COMMON(2, sys_open)
__SYSCALL_COMMON(3, sys_close)
__SYSCALL_COMMON
的宏,是在 syscall_64.c
中声明的,会将其转化为 __SYSCALL_64
宏,__SYSCALL_64
宏又生成 __x64_sys_read
的格式。这下,也就清楚 __x64_sys_read
是在哪里调用的了。
3 系统调用初始化
在第 2 节中,我们已经了解了系统调用的声明方式和如何解宏到架构相关的代码。那么在第 3 节,我们探索应用程序触发系统调用的时候如何进入到系统调用的处理函数的。
在 init/main.c
的代码中,有起始函数 start_kernel()
。内核进入到 start_kernel
后,会调用 trap_init()
函数对陷入进行初始化。然而 trap_init()
的声明是这样的:
void __init __weak trap_init() {};
这里的 __init
表示该函数只会在系统系统的时候短暂地被使用;而 __weak
表示若有其他同名的函数,该函数可以被忽略。
trap_init()
可能就是一个架构相关的函数。对与 x86 来说,在 arch/x86/kernel/traps.c
中声明了 trap_init()
函数,函数的内容如下:
void __init trap_init(void)
{
/* Init cpu_entry_area before IST entries are set up */
setup_cpu_entry_areas();
/* Init GHCB memory pages when running as an SEV-ES guest */
sev_es_init_vc_handling();
/* Initialize TSS before setting up traps so ISTs work */
cpu_init_exception_handling();
/* Setup traps as cpu_init() might #GP */
idt_setup_traps();
cpu_init();
}
关键之处在于在最后调用了 cpu_init()
的函数。cpu_init()
在 arch/x86/kernel/cpu/common.c
的文件中,它又在这个函数中,调用了 syscall_init()
函数。该函数也同样位于该文件中。syscall_init()
函数完成了两件很重要的事情,一个是写 MSR_STAR
寄存器,另一个是写 MSR_LSTAR
寄存器。代码如下:
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
这两个寄存器写入了系统调用的入口地址。
4 添加系统调用
两个步骤,声明加实现。
4.1 声明
x86 是脚本生成相关代码,故只需要在 arch/x86/entry/syscalls/syscall_64.tbl
仿照写一个即可。这里我选择系统调用号 441。对于其他架构的声明,例如 riscv,因为没有 wrapper,所以需要在 include/linux/syscalls.h
中声明。
4.2 实现
选择在一个文件夹中实现即可,使用 SYSCALL_DEFINE
相关宏。在系统调用中,输出一条 Hello, myself!
的信息,并且返回 441。
4.3 测试
编译内核!
编写一个用户程序,使用内联汇编触发系统调用。x86 的 64 位内核可以直接使用 syscall
命令,在 rax
寄存器中传递系统调用号 441。检查系统调用的返回结果,如果是 441,则说明调用成功。
测试代码如下:
#include <stdio.h>
int main() {
int res;
asm volatile(
"mov $441, %%rax\n"
"syscall\n"
: "=a"(res)
);
if (res == 441) {
printf("Successfully call sys_myself system call!\n");
} else {
printf("Failed to call sys_myself system call!\n");
}
return 0;
}
4.4 patch
修改的代码如下:
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index 3798192..969fdec 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -362,6 +362,7 @@
438 common pidfd_getfd sys_pidfd_getfd
439 common faccessat2 sys_faccessat2
440 common process_madvise sys_process_madvise
+441 common myself sys_myself
#
# Due to a historical design error, certain syscalls are numbered differently
diff --git a/fs/open.c b/fs/open.c
index 83f62cf..044b43f 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -140,6 +140,11 @@ long do_sys_truncate(const char __user *pathname, loff_t length)
return error;
}
+SYSCALL_DEFINE0(myself) {
+ printk("Hello, myself!\n");
+ return 441;
+}
+
SYSCALL_DEFINE2(truncate, const char __user *, path, long, length)
{
return do_sys_truncate(path, length);
4.5 运行
启动 Linux,并且在该环境中编译测试代码 gcc myprog.c -o myprog
,生成可执行文件,运行它。执行结果如下:
5 在 ARM 架构的系统调用添加
ARM 架构下添加系统调用(针对是 Linux 6.6.7)比较简单,只需要在 include/uapi/asm-generic/unistd.h
; 添加系统调用信息,如:
#define __NR_mysyscall 454 __SYSCALL(__NR_mysyscall, sys_mysyscall)
然后将 __NR_syscalls
的值改成 455。在 Linux 的其他位置上,添加合适的系统调用函数即可。