Linux之系统调用
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)
...
所以相关用户态的基本流程大致为:
- 将系统调用号存放在
x8
寄存器中 - 执行
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;
}