Linux之系统调用

1、背景

在应用开发中,我们经常会去操作IO设备,文件,网络,内存等,基本上都是通过高级语音(C/C++,JAVA等)提供的一些标准库或API去操作,那这些是怎么和操作系统联系在一起的呢?答案就是今天的主角–系统调用。

这里我们只讨论:

  • 硬件: Arm64
  • 系统: Linux系统 (Kernel-5.15-rc1)
  • 高级语言: C (glibc-2.34)
  • 模式: 64位 (即未定义CONFIG_COMPAT

2、什么是系统调用

Linux系统分为内核态和用户态,两者是相互隔离的。为了防止各种应用程序可能对系统资源的破坏,用户态的应用程序是没有权限直接去访问系统资源的,当需要访问时,就需要通过系统调用。

系统调用是内核提供给用户态应用程序的一系列统一接口,标准库或API在系统调用的基础上做了进一步抽象和封装。用户态的应用程序可以直接进行系统调用,也可以通过标准库或API来调用

一个系统调用有很多个步骤,其中一个很重要的就是用户态和内核态相互切换,包括CPU模式的切换, 内核栈、用户栈的保护与处理等

大致的流程为:

-----------------------------------------
                |
    用户态       |           内核态 
                |
标准库或API -> 模式切换 -> 调用准备
                |               \    
                |                 ->  处理
                |                 <-  函数
                |               /
标准库或API <- 模式切换 <- 调用善后   
                |
-----------------------------------------

下面我们就分别讨论用户态、内核态下的一些关键处理

3、用户态的处理

那么如何陷入内核态呢?主要是通过同步异常来实现。ARM64专门定义了svc指令,用于进入同步异常,也就是说,一旦执行了svc指令,cpu立即跳转到同步异常入口地址处,从这个地址进入内核态

下面已glic里面的系统调用为例,简单看看过程:
ARM64相关的代码主要在:sysdeps/unix/sysv/linux/aarch64

比如我们常用的glibc库函数ioctl(), 在arm64下,glibc的实现:

ENTRY(__ioctl)
	mov	x8, #__NR_ioctl
	sxtw	x0, w0
	svc	#0x0
	cmn	x0, #4095
	b.cs	.Lsyscall_error
	ret
PSEUDO_END (__ioctl)

其中#__NR_ioctl就对应的ioctl的系统调用号,其定义是在sysdeps/unix/sysv/linux/aarch64/arch-syscall.h,如下:

...

#define __NR_io_uring_setup 425
#define __NR_ioctl 29   //////
#define __NR_ioprio_get 31

...

这个系统调用号(29)就是上层标准库(API)与内核联系的桥梁,和内核中的定义是对应的(arm64: include/uapi/asm-generic/unistd.h):

...

/* fs/ioctl.c */
#define __NR_ioctl 29
__SC_COMP(__NR_ioctl, sys_ioctl, compat_sys_ioctl)

...

所以相关用户态的基本流程大致为:

  1. 将系统调用号存放在x8寄存器中
  2. 执行svc指令,陷入异常,并且从el0切换到el1

4、内核态的处理

当用户态进入同步异常, 便会跳转到同步异常入口地址,从而触发内核相应的处理动作。

在内核中,arm64对应的异常向量表为(arch/arm64/kernel/entry.S):

/*
 * Exception vectors.
 */
	.pushsection ".entry.text", "ax"

	.align	11
SYM_CODE_START(vectors)
	kernel_ventry	1, t, 64, sync		// Synchronous EL1t
	kernel_ventry	1, t, 64, irq		// IRQ EL1t
	kernel_ventry	1, t, 64, fiq		// FIQ EL1h
	kernel_ventry	1, t, 64, error		// Error EL1t

	kernel_ventry	1, h, 64, sync		// Synchronous EL1h
	kernel_ventry	1, h, 64, irq		// IRQ EL1h
	kernel_ventry	1, h, 64, fiq		// FIQ EL1h
	kernel_ventry	1, h, 64, error		// Error EL1h

	kernel_ventry	0, t, 64, sync		// Synchronous 64-bit EL0
	kernel_ventry	0, t, 64, irq		// IRQ 64-bit EL0
	kernel_ventry	0, t, 64, fiq		// FIQ 64-bit EL0
	kernel_ventry	0, t, 64, error		// Error 64-bit EL0

	kernel_ventry	0, t, 32, sync		// Synchronous 32-bit EL0
	kernel_ventry	0, t, 32, irq		// IRQ 32-bit EL0
	kernel_ventry	0, t, 32, fiq		// FIQ 32-bit EL0
	kernel_ventry	0, t, 32, error		// Error 32-bit EL0
SYM_CODE_END(vectors)

SYM_CODE_START 其实就是将其后面()里面的字符展开而已,并在这个展开之前加上一些属性(比如对齐规则),展开后就相当于”vectors:”,表示定义vectors函数,“:”后面就是vectors的具体实现.

设置不同mode下的异常向量表,异常可以分为4组,每组异常有4个,所以这里一共会设置16个entry。4组异常分别对应4种情况下发生异常时的处理。上面的4组,按照顺序分别对应如下4中情况:
(1)运行级别不发生切换,从ELx变化到ELx,使用SP_EL0,这种情况在Linux kernel都是不处理的,使用invalid entry。
(2)运行级别不发生切换,从ELx变化到ELx,使用SP_ELx。这种情况下在Linux中比较常见。
(3)异常需要进行级别切换来进行处理,并且使用aarch64模式处理,比如64位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch64模式处理异常。
(4)异常需要进行级别切换来进行处理,并且使用aarch32模式处理。比如32位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch32模式进行处理。

这里我们只讨论64位模式,所以系统调用是第3种情况
继续往下看,
展开kernel_ventry, kernel_ventry(arch/arm64/kernel/entry.S)是一个宏,通过.macro和.endm组合定义

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req

...

b	el\el\ht\()_\regsize\()_\label
.endm

里面会跳转到el\el\ht\()_\regsize\()_\label:

SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
	kernel_entry \el, \regsize
	mov	x0, sp
	bl	el\el\ht\()_\regsize\()_\label\()_handler
	.if \el == 0
	b	ret_to_user
	.else
	b	ret_to_kernel
	.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)

其中便会调用对应的el\el\ht\()_\regsize\()_\label\()_handler函数

通过解析,sync的处理对应的就是el0t_64_sync_handler()函数,跟踪代码,该函数的处理流程:

el0t_64_sync_handler() [arch/arm64/kernel/entry-common.c]
    -> el0_svc() 
        -> do_el0_svc() [arch/arm64/kernel/syscall.c]
            -> el0_svc_common()
                -> invoke_syscall() 
                    -> __invoke_syscall()

其中最主要的流程在el0_svc()函数:

static void noinstr el0_svc(struct pt_regs *regs)
{
	enter_from_user_mode(regs);
	cortex_a76_erratum_1463225_svc_handler();
	do_el0_svc(regs);
	exit_to_user_mode(regs);
}

最终会调用到invoke_syscall(), 该函数会根据传入的系统调用号, 在sys_call_table中找到对应的系统调用函数, 并执行

static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
			   unsigned int sc_nr,
			   const syscall_fn_t syscall_table[])
{
	long ret;

	add_random_kstack_offset();

	if (scno < sc_nr) {
		syscall_fn_t syscall_fn;
		syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
		ret = __invoke_syscall(regs, syscall_fn);
	} else {
		ret = do_ni_syscall(regs, scno);
	}

	syscall_set_return_value(current, regs, 0, ret);

	/*
	 * Ultimately, this value will get limited by KSTACK_OFFSET_MAX(),
	 * but not enough for arm64 stack utilization comfort. To keep
	 * reasonable stack head room, reduce the maximum offset to 9 bits.
	 *
	 * The actual entropy will be further reduced by the compiler when
	 * applying stack alignment constraints: the AAPCS mandates a
	 * 16-byte (i.e. 4-bit) aligned SP at function boundaries.
	 *
	 * The resulting 5 bits of entropy is seen in SP[8:4].
	 */
	choose_random_kstack_offset(get_random_int() & 0x1FF);
}

4.1、sys_call_table

sys_call_table的定义:

/// arch/arm64/kernel/sys.c

asmlinkage long __arm64_sys_ni_syscall(const struct pt_regs *__unused)
{
	return sys_ni_syscall();
}

/*
 * Wrappers to pass the pt_regs argument.
 */
#define __arm64_sys_personality		__arm64_sys_arm64_personality

#undef __SYSCALL
#define __SYSCALL(nr, sym)	asmlinkage long __arm64_##sym(const struct pt_regs *);
#include <asm/unistd.h>

#undef __SYSCALL
#define __SYSCALL(nr, sym)	[nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = {
	[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};

首先会将sys_call_table都初始化为sys_ni_syscall(),这里使用了GCC的扩展语法:指定初始化
sys_ni_syscall()为一个空函数,未做任何操作:

/// kernel/sys_ni.c

asmlinkage long sys_ni_syscall(void)
{
	return -ENOSYS;
}

然后包含asm/unistd.h的进行逐项初始化,asm/unistd.h最终会包含到uapi/asm-generic/unistd.h头文件:

...

#ifdef __SYSCALL_COMPAT
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
#else
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
#endif

#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
#define __NR_io_destroy 1
__SYSCALL(__NR_io_destroy, sys_io_destroy)
#define __NR_io_submit 2
__SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
#define __NR_io_cancel 3
__SYSCALL(__NR_io_cancel, sys_io_cancel)

...

4.2 SYSCALL_DEFINEx

内核中具体的系统调用实现使用SYSCALL_DEFINEx来定义, 其中x代表传入参数的个数,
SYSCALL_DEFINEx相关代码:

/// 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__)

而对于ARM64,__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__))

由上面可以看出,SYSCALL_DEFINEx来定义的函数就和sys_call_table中由__SYSCALL确定的函数对应了,即__arm64_sys##name

例如ioctl()内核态的实现为:

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
	struct fd f = fdget(fd);
	int error;

	if (!f.file)
		return -EBADF;

	error = security_file_ioctl(f.file, cmd, arg);
	if (error)
		goto out;

	error = do_vfs_ioctl(f.file, fd, cmd, arg);
	if (error == -ENOIOCTLCMD)
		error = vfs_ioctl(f.file, cmd, arg);

out:
	fdput(f);
	return error;
}

5、参考