Linux(07)之内核系统调用
Author:Once Day Date:2023年1月7日
漫漫长路,才刚刚开始…
文章目录
- Linux(07)之内核系统调用
- 1.概述
- 2. 系统调用
- 2.1 无参数的系统调用定义
- 2.2 带参数的系统调用定义
- 2.2.1 `__diag`诊断宏
- 2.2.2 `__MAP`参数映射处理宏
- 2.2.3 `protect`编译保护宏
- 2.2.4 `socket`示例展开
- 2.2.5 寄存器值传递`arm64/power/s390`
- 2.2.6 桩函数+寄存器`x86`
- 2.2.7 COMPAT_SYSCALL 32位兼容系统调用
- 3. 新增系统调用和用户空间使用
- 3.1 定义系统调用函数
- 3.2 定义系统调用表(syscall tables)的调用号
- 3.3 系统调用的执行
- 3.4 用户空间直接调用系统调用
- 3.5 常见系统调用总结
1.概述
系统调用是指内核提供给用户线程,用于和内核自身进行交互的一组接口。这些接口可以让应用程序受限的访问硬件设备,创建新线程并于已有进程进行通信的机制,以及申请操作系统其他资源的能力。
系统调用主要在用户空间进程和硬件设备之间添加了一个中间层,该层作用主要有三个:
- 为用户空间提供一种硬件的抽象接口。
- 系统调用保证了系统的稳定和安全。
- 进程运行在虚拟的系统中,由系统提供访问内核的手段。
系统调用是除了异常和陷入外,内核唯一的合法入口。Linux的系统调用比大部分操作系统都少得多,重点强调Linux系统调用的规则和实现方法。
2. 系统调用
在实际的用户空间编程中,应用程序通过应用编程接口API
而不是直接通过系统调用来编程。这表明应用程序使用的编程接口并不需要和内核提供的系统调用对应。
一个API接口定义了一组应用程序使用的编程接口,可以有如下实现:
- 实现成一个系统调用。
- 通过调用多个系统调用来实现。
- 完全不实现任何系统调用。
Unix环境下,最流行的应用编程接口是基于POSIX标准的,其由IEEE的一组标准组成。
C库提供Unix系统的主要API,包括标准C库函数和系统调用接口,并且提供了POSIX的大部分API。
Unix的接口设计有一句格言:“提供机制而不是策略”。Unix的系统调用抽象出了用于完成某种确定的目的的函数,至于这些函数怎么用完全不需要内核去关心。
2.1 无参数的系统调用定义
系统调用(syscall)一般通过C库定义的函数调用来执行,通常需要定义零个、一个或几个参数,并且产生副作用(改变内核的状态或数据)。
系统调用会通过一个long类型返回值来表示成功或错误。出现错误时,C库会把错误码写入errno
全局变量,通过调用perror()
库函数,可以将该变量翻译成用户可以理解的错误字符串。
下面是一个非常简单的系统调用实例:
//kernel/sys.c
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
对应的用户空间API接口是getpid
,返回当前进程的PID。但是在系统中,系统调用统一命名前缀为sys_getpid
,例如socket
,其系统调用函数名称为sys_socket
。
关键在于SYSCALL_DEFINE0()
宏,它定义了一个无参数的系统调用(数字0代表无参数),展开代码如下:
asmlinkage long sys_getpid(void) {
return task_tgid_vnr(current);
}
asmlinkage是一个限定宏,有如下几种形式:
#define CPP_ASMLINKAGE extern "C"
// IA64架构
#define asmlinkage CPP_ASMLINKAGE __attribute__((syscall_linkage))
// X86架构
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
// 其他架构
#define asmlinkage CPP_ASMLINKAGE
用户空间调用的系统调用函数,其参数不能存放在寄存器里,因为内核寄存器和用户寄存器需要区分开。所以需要完全依靠栈来传递参数值(系统调用号通过寄存器传递,如x86架构的eax
)。
关于__attribute__((regparm(0)))
详细解释参考以下文档:
- attribute regparm_lyingson的博客-CSDN博客_vs编译器 attribute((regparm(n)))
简单来说,regparm(register parameter)
表示指定多少个函数参数使用寄存器传递,这里表明0个,即所有参数使用栈来传递。
SYSCALL_DEFINE0()
宏的形式一般如下:
// include/linux/syscalls.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void); \
ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \
asmlinkage long sys_##sname(void)
SYSCALL_METADATA()
宏可以忽略,它是用来追踪系统调用的,和实际系统调用过程无关。
可以明显看到系统调用的函数定义其返回值是long类型,long类型的size和32位/64位相关,因此能保持兼容。
ALLOW_ERROR_INJECTION
也是一个和正常系统调用无关的宏,用于内核调试,向内核中注入错误,有兴趣可以自行了解。
所以SYSCALL_DEFINE0()
其实非常简单,其首先声明一个函数,然后完成这个函数的具体定义。##
是连接宏,将两个标识符连接成一个标识符,基本C语言预处理(基础宏标识符)可以参考下面文章:
- C之预处理_Once_day的博客-CSDN博客
完成宏展开后,对于getpid
系统通用,其在内核源码的函数定义如下:
asmlinkage long sys_getpid(void);
asmlinkage long sys_getpid(void)
{
return task_tgid_vnr(current);
}
对于一些特定架构,SYSCALL_DEFINE0()
宏定义会有一些不同:
// arch/arm64/include/asm/syscall_wrapper.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long __arm64_sys_##sname(const struct pt_regs *__unused); \
ALLOW_ERROR_INJECTION(__arm64_sys_##sname, ERRNO); \
asmlinkage long __arm64_sys_##sname(const struct pt_regs *__unused)
// arch/powerpc/include/asm/syscall_wrapper.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
long sys_##sname(const struct pt_regs *__unused); \
ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \
long sys_##sname(const struct pt_regs *__unused)
// arch/s390/include/asm/syscall_wrapper.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
long __s390x_sys_##sname(void); \
ALLOW_ERROR_INJECTION(__s390x_sys_##sname, ERRNO); \
long __s390_sys_##sname(void) \
__attribute__((alias(__stringify(__s390x_sys_##sname)))); \
long __s390x_sys_##sname(void)
// arch/x86/include/asm/syscall_wrapper.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
static long __do_sys_##sname(const struct pt_regs *__unused); \
__X64_SYS_STUB0(sname) \
__IA32_SYS_STUB0(sname) \
static long __do_sys_##sname(const struct pt_regs *__unused)
目前Linux kernel 6.2
版本中,有如上四类不同的定义,但整体上是类似,除了细节上有一些不同。
对于arm64
架构,其参数统一通过struct pt_regs
结构体来传递,该结构体存储了CPU的寄存器信息,位于内核栈中。对于无参数的系统调用,使用__unused
属性表明该参数没有被用到。
#define __unused __attribute__ ((unused))
对于s390
架构,其定义非常直接,__stringify
是对字符串化宏的封装,__attribute__((alias(“__s390x_sys_##sname”))))
表明__s390_sys_##sname
是__s390x_sys_##sname
的一个别名,简单来说,两个函数名字是等价的。
对于x86
架构,__do_sys_##sname
是其实际执行代码所在的函数定义,在它的基础上定义了两个桩函数(stub function)。例如对于getpid
函数调用,其函数定义如下:
static long __do_sys_getpid(const struct pt_regs *__unused)
{
return task_tgid_vnr(current);
}
很明显,static限定了文件作用域,因此其函数符号是通过X64
和IA32
两个桩函数导出去的。其宏定义如下:
// include/linux/compiler_attributes.h
#define __alias(symbol) __attribute__((__alias__(#symbol)))
// arch/x86/include/asm/syscall_wrapper.h
#define __SYS_STUB0(abi, name) \
long __##abi##_##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO); \
long __##abi##_##name(const struct pt_regs *regs) \
__alias(__do_##name);
#define __X64_SYS_STUB0(name) \
__SYS_STUB0(x64, sys_##name)
#define __IA32_SYS_STUB0(name) \
__SYS_STUB0(ia32, sys_##name)
__X64/IA32_SYS_STUB0(sname)
宏定义就是定义了一个桩函数,然后告诉编译器该桩函数是__do_sys_name
函数的别名,对于get_pid
,其完整定义展开如下:
static long __do_sys_getpid(const struct pt_regs *__unused);
long __x64_sys_getpid(const struct pt_regs *regs);
long __x64_sys_getpid(const struct pt_regs *regs) __alias(__do_sys_getpid);
long __ia32_sys_getpid(const struct pt_regs *regs);
long __ia32_sys_getpid(const struct pt_regs *regs) __alias(__do_sys_getpid);
static long __do_sys_getpid(const struct pt_regs *__unused)
{
return task_tgid_vnr(current);
}
本质也是比较简单的宏替换,以及C语言的函数定义和声明。
2.2 带参数的系统调用定义
带参数的系统调用比较复杂一点,因为涉及到参数的处理,内核里面统一接收的都是long
类型参数或者long long
类型参数。系统调用的参数可以是指针和整数,但不会是结构体或者浮点数之类(目前未看到这类系统调用)。
浮点数很好理解,内核编程本来就极不推荐使用浮点数,因为浮点运算单元需要手动保存值。
一般需要传递结构体数据的,都会传递指针,内核会在用户空间和内核空间执行严格的参数检查和数据复制,并不会直接使用内核空间的数据。
对于指针和整数,其类型大小和具体CPU字长有关,但long类型可以保持兼容,在32位和64位之间保持一致,但对于long long
类型仍有长度不一致(32位机器),因此也会存在long long
类型的函数参数。
先来看一下内核的标准宏定义:
// include/linux/syscalls.h
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
目前最多支持带6个参数的函数,一般而言,系统调用的参数都比较少,从而专注于做好一件事情。
所有定义最终汇总到一个宏上__SYSCALL_DEFINEx
,一般它的定义如下:
/*
* The asmlinkage stub is aliased to a function named __se_sys_*() which
* sign-extends 32-bit ints to longs whenever needed. The actual work is
* done within __do_sys_*().
*/
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage 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; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
相比于无参数的系统调用,__do_sys##name
函数依旧用于完成实际函数定义,但中间也有__se_sys##name
函数对实际参数做了一层封装。
2.2.1 __diag
诊断宏
首先来看__diag_push()
和__diag_ignore()
宏,这个和编译器版本有关,如GCC版本定义如下:
//include/linux/compiler-gcc.h
/*
* Turn individual warnings and errors on and off locally, depending
* on version.
*/
#define __diag_GCC(version, severity, s) \
__diag_GCC_ ## version(__diag_GCC_ ## severity s)
/* Severity used in pragma directives */
#define __diag_GCC_ignore ignored
#define __diag_GCC_warn warning
#define __diag_GCC_error error
#define __diag_str1(s) #s
#define __diag_str(s) __diag_str1(s)
#define __diag(s) _Pragma(__diag_str(GCC diagnostic s))
/*
* Common definitions for all gcc versions go here.
*/
#define GCC_VERSION (__GNUC__ * 10000 \
+ __GNUC_MINOR__ * 100 \
+ __GNUC_PATCHLEVEL__)
#if GCC_VERSION >= 80000
#define __diag_GCC_8(s) __diag(s)
#else
#define __diag_GCC_8(s)
#endif
#define __diag_ignore_all(option, comment) \
__diag_GCC(8, ignore, option)
其核心是_Pragma
,参数为string-literal
,即可迭代字符串,因此首先通过#flag
将flag字符串化。
option
是控制对应模块(ignored/warning/error)的属性参数,等价于下面形式:
#define __diag(s) _Pragma("GCC diagnostic s")
#define __diag_GCC(option) _Pragma("GCC diagnostic ignored/warning/error option")
针对不同的编译器(clang或GCC),又有以下的封装:
//include/linux/compiler_types.h
#define __diag_push() __diag(push)
#define __diag_pop() __diag(pop)
#define __diag_ignore(compiler, version, option, comment) \
__diag_ ## compiler(version, ignore, option)
#define __diag_warn(compiler, version, option, comment) \
__diag_ ## compiler(version, warn, option)
#define __diag_error(compiler, version, option, comment) \
__diag_ ## compiler(version, error, option)
#ifndef __diag_ignore_all
#define __diag_ignore_all(option, comment)
#endif
因此__SYSCALL_DEFINEx
宏开头整体的__diag
宏展开如下:
#define __SYSCALL_DEFINEx(x, name, ...)
_Pragma("GCC diagnostic pop")
_Pragma("GCC diagnostic ignored "-Wattribute-alias"")
......参数类型封装......
_Pragma("GCC diagnostic push")
在GCC下,#pragma GCC diagnostic push
用于记录当前的诊断状态,#pragma GCC diagnostic pop
用于恢复诊断状态。可以用于屏蔽局部代码的警告。(_Pragma
是#pragma
用于宏里面的版本,避免#
被解释成字符串化)。对于__SYSCALL_DEFINEx
宏,就是避免系统调用在进行参数封装时报和alias
相关的错误。
2.2.2 __MAP
参数映射处理宏
再看看__MAP()
和__SC_XXX
宏,如下:
//include/linux/syscalls.h
/*
* __MAP - apply a macro to syscall arguments
* __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
* m(t1, a1), m(t2, a2), ..., m(tn, an)
* The first argument must be equal to the amount of type/name
* pairs given. Note that this list of pairs (i.e. the arguments
* of __MAP starting at the third one) is in the same format as
* for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
*/
#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)
可以看到默认最大展开参数为6个,m
是映射的操作。例如:
#define mty(t,a) __typeof__(t) a
int test(__MAP(3, mty, int, a, float, b, char, c));
//等价于
int test(int a, float b, char c);
__MAP
就是对参数两两之间进行映射操作,具体的操作__SC_XXX
宏如下:
//include/linux/syscalls.h
/* Are two types/vars the same type (ignoring qualifiers)? */
#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
#define __force __attribute__((force))
#define __SC_DECL(t, a) t a
#define __TYPE_AS(t, v) __same_type((__force t)0, v)
#define __TYPE_IS_L(t) (__TYPE_AS(t, 0L))
#define __TYPE_IS_UL(t) (__TYPE_AS(t, 0UL))
#define __TYPE_IS_LL(t) (__TYPE_AS(t, 0LL) || __TYPE_AS(t, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (__force t) a
#define __SC_ARGS(t, a) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
里面__builtin_types_compatible_p
和__builtin_choose_expr
是内建表达式,即字面含义。
上面定义很明显,如果参数的类型不是unsigned long long
或者long long
类型,那么都转化为long
类型,但是最后__SC_TEST()
会对参数做一次检查,如果参数类型的实际size大于long
类型,那么编译将直接报错。
因此,从这里看,系统调用只能传递指针和整数类型,并且指针都会转化为long
类型。
2.2.3 protect
编译保护宏
最后看一看PROTECT
宏,如下:
//include/linux/syscalls.h
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
//include/linux/linkage.h
#define asmlinkage_protect(n, ret, args...) do { } while (0)
//arch/m68k/include/asm/linkage.h
#define asmlinkage_protect(n, ret, args...) \
__asmlinkage_protect##n(ret, ##args)
#define __asmlinkage_protect_n(ret, args...) \
__asm__ __volatile__ ("" : "=r" (ret) : "0" (ret), ##args)
#define __asmlinkage_protect0(ret) \
__asmlinkage_protect_n(ret)
#define __asmlinkage_protect1(ret, arg1) \
__asmlinkage_protect_n(ret, "m" (arg1))
......
该宏用来防止编译器优化调用参数,如尾递归、临时堆栈等场景,目前只有m68K
实际定义具体的操作。
2.2.4 socket
示例展开
下面以socket系统调用来举例上面__SYSCALL_DEFINEx
宏的实际展开代码:
//net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
//展开为:
_Pragma("GCC diagnostic pop")
_Pragma("GCC diagnostic ignored "-Wattribute-alias"")
asmlinkage long sys_socket(int family, int type, int protocol)
__attribute__((alias("__se_sys_socket")));
static inline long __do_sys_socket(int family, int type, int protocol);
asmlinkage long __se_sys_socket(long family, long type, long protocol);
asmlinkage long __se_sys_socket(long family, long type, long protocol)
{
long ret = __do_sys_socket((int)family, (int)type, (int)protocol);
return ret;
}
_Pragma("GCC diagnostic push")
static inline long __do_sys_socket(int family, int type, int protocol)
{
return __sys_socket(family, type, protocol);
}
可以看到,sys_socket系统调用类型和内核实际执行函数__do_sys_socket
的参数类型一致,但中间通过__se_sys_socket
转变一次,参数都以统一的long
类型传递进内核。
2.2.5 寄存器值传递arm64/power/s390
对于具体的架构,__SYSCALL_DEFINEx
宏的具体细节也有一些不同,如下:
// arch/arm64/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__arm64_sys##name, ERRNO); \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long __arm64_sys##name(const struct pt_regs *regs) \
{ \
return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__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__))
ARM64
架构不同点在于使用struct pt_regs
来传递参数,SC_ARM64_REGS_TO_ARGS
宏负责展开参数,其定义如下:
#define SC_ARM64_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
,,regs->regs[0],,regs->regs[1],,regs->regs[2] \
,,regs->regs[3],,regs->regs[4],,regs->regs[5])
寄存器的值都是64位,和long
类型是对应的,因此其逻辑和一般的形式是保持一致的。还是以socket
系统调用为例,其关键不同处展开为:
......
static long __se_sys_socket(long family, long type, long protocol);
asmlinkage long __arm64_sys_socket(const struct pt_regs *regs)
{
return __se_sys_socket(regs->regs[0], regs->regs[1], regs->regs[2]);
}
......
其他架构如power/s390
也是使用寄存器来读值,和arm64
的处理方式一致,如下:
// arch/powerpc/include/asm/syscall_wrapper.h
#define SC_POWERPC_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
,,regs->gpr[3],,regs->gpr[4],,regs->gpr[5] \
,,regs->gpr[6],,regs->gpr[7],,regs->gpr[8])
#define __SYSCALL_DEFINEx(x, name, ...) \
long sys##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
long sys##name(const struct pt_regs *regs) \
{ \
return __se_sys##name(SC_POWERPC_REGS_TO_ARGS(x,__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__))
// arch/s390/include/asm/syscall_wrapper.h
#define SYSCALL_PT_ARG6(regs, m, t1, t2, t3, t4, t5, t6)\
SYSCALL_PT_ARG5(regs, m, t1, t2, t3, t4, t5), \
m(t6, (regs->gprs[7]))
#define SYSCALL_PT_ARG5(regs, m, t1, t2, t3, t4, t5) \
SYSCALL_PT_ARG4(regs, m, t1, t2, t3, t4), \
m(t5, (regs->gprs[6]))
#define SYSCALL_PT_ARG4(regs, m, t1, t2, t3, t4) \
SYSCALL_PT_ARG3(regs, m, t1, t2, t3), \
m(t4, (regs->gprs[5]))
#define SYSCALL_PT_ARG3(regs, m, t1, t2, t3) \
SYSCALL_PT_ARG2(regs, m, t1, t2), \
m(t3, (regs->gprs[4]))
#define SYSCALL_PT_ARG2(regs, m, t1, t2) \
SYSCALL_PT_ARG1(regs, m, t1), \
m(t2, (regs->gprs[3]))
#define SYSCALL_PT_ARG1(regs, m, t1) \
m(t1, (regs->orig_gpr2))
#define SYSCALL_PT_ARGS(x, ...) SYSCALL_PT_ARG##x(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments"); \
long __s390x_sys##name(struct pt_regs *regs) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(__s390x_sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
long __se_sys##name(struct pt_regs *regs); \
__S390_SYS_STUBx(x, name, __VA_ARGS__) \
long __se_sys##name(struct pt_regs *regs) \
{ \
long ret = __do_sys##name(SYSCALL_PT_ARGS(x, regs, \
__SC_CAST, __MAP(x, __SC_TYPE, __VA_ARGS__))); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
return ret; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
2.2.6 桩函数+寄存器x86
x86
架构独特的使用到了桩函数:
// arch/x86/include/asm/syscall_wrapper.h
#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__))
关键点在于__X64/IA32_SYS_STUBx
宏定义,如下:
// arch/x86/include/asm/syscall_wrapper.h
#define __X64_SYS_STUBx(x, name, ...) \
__SYS_STUBx(x64, sys##name, \
SC_X86_64_REGS_TO_ARGS(x, __VA_ARGS__))
#define __IA32_SYS_STUBx(x, name, ...) \
__SYS_STUBx(ia32, sys##name, \
SC_IA32_REGS_TO_ARGS(x, __VA_ARGS__))
/* Mapping of registers to parameters for syscalls on x86-64 and x32 */
#define SC_X86_64_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
,,regs->di,,regs->si,,regs->dx \
,,regs->r10,,regs->r8,,regs->r9) \
/* Mapping of registers to parameters for syscalls on i386 */
#define SC_IA32_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
,,(unsigned int)regs->bx,,(unsigned int)regs->cx \
,,(unsigned int)regs->dx,,(unsigned int)regs->si \
,,(unsigned int)regs->di,,(unsigned int)regs->bp)
#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__); \
}
可以看到,X86架构也是使用寄存器结构体来传递值,还是以socket调用为例展开来说明:
asmlinkage long __se_sys_socket(long family, long type, long protocol);
static inline long __do_sys_socket(int family, int type, int protocol);
long __x64_sys_socket(const struct pt_regs *regs);
long __x64_sys_socket(const struct pt_regs *regs)
{
return __se_sys_socket(regs->di, regs->si, regs->dx);
}
long __ia32_sys_socket(const struct pt_regs *regs);
long __ia32_sys_socket(const struct pt_regs *regs)
{
return __se_sys_socket((unsigned int)regs->bx, (unsigned int)regs->cx, (unsigned int)regs->dx);
}
asmlinkage long __se_sys_socket(long family, long type, long protocol)
{
long ret = __do_sys_socket((int)family, (int)type, (int)protocol);
return ret;
}
static inline long __do_sys_socket(int family, int type, int protocol)
{
return __sys_socket(family, type, protocol);
}
2.2.7 COMPAT_SYSCALL 32位兼容系统调用
对于在64位系统上运行的32位程序需要兼容系统调用,如下:
/*
* The asmlinkage stub is aliased to a function named __se_compat_sys_*() which
* sign-extends 32-bit ints to longs whenever needed. The actual work is
* done within __do_compat_sys_*().
*/
#define COMPAT_SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long compat_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_compat_sys##name)))); \
ALLOW_ERROR_INJECTION(compat_sys##name, ERRNO); \
static inline long __do_compat_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_compat_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long __se_compat_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_compat_sys##name(__MAP(x,__SC_DELOUSE,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
return ret; \
} \
__diag_pop(); \
static inline long __do_compat_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
定义形式和一般的系统调用保持一致,除了__SC_DELOUSE
宏:
#define __SC_DELOUSE(t,v) ((__force t)(unsigned long)(v))
该宏会对参数先转换成unsigned long
然后再进行强制类型转换。可参考以下文档:
- Compatibility System Calls (Generic) — Adding a New System Call — The Linux Kernel documentation
对于大多数系统调用,即使用户空间程序本身是32位的,也可以调用相同的64位实现;即使系统调用的参数包含显式指针,这也是透明处理的。然而,在一些情况下,需要兼容层来处理32位和64位之间的大小差异。
第一种情况是64位内核也支持32位用户空间程序,因此需要解析用户程序内存中可以保存32位或64位值的区域。特别地,当系统调用参数是:
- 指向指针的指针
- 指向结构体的指针,该结构体包含指针。
- 指向可变长度类型的指针,如
time_t/off_t/long/...
等。 - 指向结构体的指针,该结构体包含可变长度的类型。
第二种需要兼容层的情况是,如果系统调用的参数之一具有显式64位类型,即使在32位体系结构上也是如此,例如loff_t
或__u64
。在这种情况下,从32位应用程序到达64位内核的值将被分割为两个32位值,然后需要在兼容性层中重新组装。注意,指向显式64位类型的指针的系统调用参数不需要兼容层。
对于compat_syscall
需要手动将32位应用程序的参数转换成64位的参数,然后再使用64位的系统调用完成系统调用,比如系统调用的参数涉及一个结构体,其在32位和64位下,有不同的内存分布模型,那么需要在include/linux/compat.h
头文件中定义一个兼容的32位结构体,在compat_syscall
调用中使用32位结构体来正确解析参数值。
3. 新增系统调用和用户空间使用
参考文档:
- Adding a New System Call — The Linux Kernel documentation。
- Linux系统调用详解(实现机制分析)–linux内核剖析(六)_CHENG Jian的博客-CSDN博客_系统调用
3.1 定义系统调用函数
直接包含include/linux/syscalls.h
文件,然后在C源文件中实现其逻辑代码:
SYSCALL_DEFINE2(test, int, a, void __user *, arg) {
do something;
}
如果涉及32位兼容系统调用,还需要定义:
COMPAT_SYSCALL_DEFINE2(test, int, a, void __user *, arg) {
do something;
}
3.2 定义系统调用表(syscall tables)的调用号
首先是通用表单,位于include/uapi/asmgeneric/unistd.h
:
#define __NR_test 451
__SYSCALL(__NR_test, sys_test)
#undef __NR_syscalls
#define __NR_syscalls 452
这里主要分配系统调用号,每一个调用号都是独一无二的,当用户空间的进程执行一个系统调用时,只会使用系统调用号来指明使用哪个系统调用。
系统调用号一旦被分配,就不会再改变,对于那些无法回收的系统调用,会专门使用sys_ni_syscall
来替代,它只会返回-ENOSYS
,而不做任何其他工作。
不同的架构,syscall_table
可能有自己的实现,一般需要根据具体情况填写。例如x86
架构,目录arch/x86/entry/syscalls
下有64位和32位syscall_table
。
// arch/x86/entry/syscalls/syscall_64.tbl
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0 common read sys_read
1 common write sys_write
2 common open sys_open
......
在kernel/sys_ni.c
中也提供了缺失桩函数定义,如下添加即可:
COND_SYSCALL(test)
需要注意,如果系统调用的32位和64位参数不兼容,那么32位跳转表里需要指向compat
版本系统调用,而不是64位版本的。
3.3 系统调用的执行
系统调用一般是通过特殊的CPU指令来操作,如X86系统上的int $0x80
中断,或者新的sysenter
指令,这些和硬件联系非常紧密。
系统调用另一个重点是参数检查,如下:
- 指针指向的内存区域属于用户空间,不能读取内核空间的数据。
- 指针指向的内存区域在进程的地址空间里,不能读取其他进程的数据。
- 读内存需要该内存标记为可读,写内存需要该内存标记为可写,等等。不能绕过内存访问限制。
一般使用copy_to_user
和copy_from_user
两个函数来完成内存在用户空间和内核空间的交换。
这两个函数,如果执行失败,会返回没能拷贝的数据的字节数。两个函数可能引起阻塞,如内存换页。
注意,内核执行系统调用时处于进程上下文,current
指向引发系统调用的那个进程。
在进程上下文中,内核可以休眠(比如系统调用阻塞或显示调用schedule的时候),并且可以被抢占。在另外一方面,这也需要系统调用是可重入的。
具体的系统调用执行时需要操作寄存器,因此这部分代码都是用汇编写的,一般在arch/xx/entry/entry.S
文件中,有兴趣可以自行了解。
3.4 用户空间直接调用系统调用
参考文档:
- linux内核编译及添加系统调用(详细版)_LitStronger的博客-CSDN博客
如下来用就可以:
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
int main(void)
{
int pid = syscall(SYS_getpid);
int pid2 = getpid();
printf("%d||%d\n", pid, pid2);
}
SYS_getpid
就是系统调用号,全局唯一标识。
3.5 常见系统调用总结
参见文档:
- Linux系统调用详解(实现机制分析)–linux内核剖析(六)_CHENG Jian的博客-CSDN博客_系统调用