linux(07)之内核系统调用

news2024/12/22 22:31:18

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限定了文件作用域,因此其函数符号是通过X64IA32两个桩函数导出去的。其宏定义如下:

// 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_usercopy_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博客_系统调用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/148745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

go的安装、gin安装以及GoLand的配置

一.go的安装以及Goland配置go的安装&#xff1a;https://www.runoob.com/go/go-environment.htmlMAC 系统下你可以使用 .pkg 结尾的安装包直接双击来完成安装&#xff0c;安装目录在 /usr/local/go/ 下。mac中按 ⌘shift. 即可显示隐藏文件夹&#xff0c;再按一次&#xff0c;即…

Spring4 全细节回顾

spring细节回顾 1、IOC/DI ApplicationContext接口是BeanFactory接口的子接口&#xff0c;Spring所有的东西都扔到了这里边。 1、Beans&#xff1a;Spring负责创建类对象并管理对象&#xff1b; 2、Core&#xff1a;核心类&#xff1b; 3、Context&#xff1a;上下文参数&a…

机器学习(4)——周志华

归纳偏好 若非必要&#xff0c;勿增实体 若多个算法都能解决同一个问题&#xff0c;则选取最简单的算法 当前什么样的偏好与问题更匹配&#xff0c;必须对问题有一个清楚的认识

shader基础入门(2)(VertexHelper)

VertexHelper&#xff08;顶点帮助器/顶点辅助类/顶点助手&#xff09; 官方资料版本&#xff1a;2019.1 使用需要继承&#xff1a;using UnityEngine.UI; 描述 可以帮助为 UI 生成网格的实用程序类。 此类实现 IDisposable 以帮助进行内存管理。 属性 currentIndexCount …

【vim工具的使用】

目录&#xff1a;前言一、普通/命令模式1.文件中移动 - 12.文件中移动 - 23.复制、粘贴、剪切、删除4.行内删除5.撤回6.替换7.高亮选中8.逐单词移动 - 3二、底行模式1.退出vim2.设置行号3.替换4.搜索3.不退出vim的情况下进行操作4.多文件操作三、替换模式四、插入模式总结前言 …

Python递归小练习

递归算法是常见的基础算法之一&#xff0c;阶乘、青蛙跳台、兔子算法等便是递归算法中典型的例子。 1、阶乘以6的阶乘为例&#xff0c;计算的过程如下图所示 Python代码实现如下&#xff1a; def jiecheng(n):if n 1:result 1else:result n * jiecheng(n - 1)return resul…

蓝桥杯寒假集训第九天(回文日期)

没有白走的路&#xff0c;每一步都算数&#x1f388;&#x1f388;&#x1f388; 题目描述&#xff1a; 输入一行数据&#xff0c;这个数据是从10000101到89991231&#xff0c;输出这个数据后面的第一个回文数&#xff0c;以及输出第一个ABABBABA型的回文数。 注意&#xff1…

跟着博主一起刷题:《剑指offer》(第五天)

跟着博主一起刷题 这里使用的是题库&#xff1a; https://leetcode.cn/problem-list/xb9nqhhg/?page1 目录剑指 Offer 37. 序列化二叉树剑指 Offer 38. 字符串的排列剑指 Offer 40. 最小的k个数剑指 Offer 37. 序列化二叉树 剑指 Offer 37. 序列化二叉树 序列化&#xff1a; …

windows下mmclassification安装教程

文章目录mmclassification版本0.23.1一.官方安转教程二.安装教程mmclassification版本0.23.1 一.官方安转教程 官网 二.安装教程 创建conda环境 conda create --name openmmlab python3.8 -y进入创建的环境 conda activate openmmlab安转pytorcch conda install pytorch torc…

CSS知识点精学5-定位装饰

目录 一.定位 1&#xff0e;定位的基本介绍 2.定位的基本使用 3.静态定位 4.相对定位 5.绝对定位 6.子绝父相 a.优化学成网站推荐课程卡片 b.绝对定位盒子的居中&#xff08;案例&#xff09; 8.固定定位 9.元素的层级关系 二.装饰 1.垂直对齐方式 2.光标类型 3…

计算机网络实验报告

计算机网络实验报告 文章目录计算机网络实验报告一、验证性实验ipconfig实作一实作二问题ping实作一实作二问题tracert实作一问题一问题二问题三ARP实作一实作二实作三问题DHCP实作一问题netstat实作一实作二DNS实作一实作二实作三问题cache实作二二、Wireshark 实验数据链路层…

果蔬消毒机行业市场深度监测及发展趋势预测分析

2023-2029年中国果蔬消毒机行业市场深度监测及发展趋势预测报告报告编号&#xff1a;1691217本报告著作权归博研咨询所有&#xff0c;未经书面许可&#xff0c;任何组织和个人不得以任何形式复制、传播或输出中华人民共和国境外。任何未经授权使用本报告的相关商业行为都将违反…

STP生成树基础,一个难点

技术背景&#xff1a;二层交换机网络的冗余性与环路 二层环路带来的问题&#xff08;三层环路&#xff0c;ttl跳数达到一定次数会丢弃&#xff09; 典型问题 1&#xff1a;广播风暴不停的在接口内复制转发广播 2&#xff1a;MAC地址漂移 sw1接口1与2&#xff0c;对sw3的接收ma…

CentOS7.x下部署oracle19c环境

CentOS7.x下部署oracle19c环境 文章目录CentOS7.x下部署oracle19c环境前言一、环境准备工作1.1、虚拟机搭建及关闭防火墙和selinux1.2、RPM包预安装1.3、检查安装情况用户组已创建完成1.4、创建目录1.5、修改/etc/hosts 文件1.6、设置oracle口令1.7、设置环境变量1.8、将oracle…

[LeetCode周赛复盘] 第 327 场周赛20230108

[LeetCode周赛复盘] 第 327 场周赛20230108 一、本周周赛总结二、 [Easy] 6283. 正整数和负整数的最大计数1. 题目描述2. 思路分析3. 代码实现三、[Medium] 6285. 执行 K 次操作后的最大分数1. 题目描述2. 思路分析3. 代码实现四、[Medium] 6284. 使字符串总不同字符的数目相等…

10、ThingsBoard-租户

1、租户的概念 目前,有很多的系统都是以多租户的形式来设计的,目的是为了实现数据隔离,thingsboard中也是如此。但是thingsboard把租户在抽象成了一种实体,后续还会讲解其它的实体;用官方的语言解释租户:您可以将租户视为一个单独的业务实体:它是拥有或生产设备和资产的…

Java中ThreadLocal说明

1、ThreadLocal是什么 ThreadLocal&#xff0c;即线程变量&#xff0c;是一个以ThreadLocal对象为键、任意对象为值的存储结构。 这个结构被附带在线程上&#xff0c;也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。 ——《Java并发编程艺术》如…

GrapeCity Documents Data Viewer[GcDataViewer] Crack

GrapeCity Documents Data Viewer&#xff0c;简称GcDataViewer&#xff0c;是一个统一的基于 JavaScript 的客户端查看器&#xff0c;旨在加载和预览与数据相关的所有流行文档格式。目前&#xff0c;查看器支持 XLSX、SSJSON 和 CSV 格式。除了仅加载数据文件外&#xff0c;数…

卷径计算(厚度累计法/膜厚积分法)

卷径计算的截面积法请参看下面的文章链接: 卷径计算详解(通过卷绕的膜长和膜厚进行计算)_RXXW_Dor的博客-CSDN博客有关卷绕+张力控制可以参看专栏的系列文章,文章链接如下:变频器简单张力控制(线缆收放卷应用)_RXXW_Dor的博客-CSDN博客_收放卷应用张力控制的开闭环算法,…

“/ArcGIS/services”应用程序中的服务器错误

本文迁移自本人网易博客&#xff0c;写于2013年1月5日No Content说明: 执行当前 Web 请求期间&#xff0c;出现未处理的异常。请检查堆栈跟踪信息&#xff0c;以了解有关该错误以及代码中导致错误的出处的详细信息。 异常详细信息: System.Web.HttpException: No Content源错误…