系统调用
系统调用是内核给用户程序提供的编程接口。用户程序调用系统调用,通常使用glibc库针对单个系统调用封装的函数。如果glibc库没有针对某个系统调用封装的函数,用户程序可以使用通用的封装函数syscall():
#define _GNC_SOURCE
#include <unistd.h>
#include <sys/syscall.h> /* 定义SYS_xxx */
long syscall(long number, ...);
参数number是系统调用号,后面是传递给系统调用的参数。
返回值0表示成功,返回值-1表示错误,错误号存储在变量errno中。
例如,应用程序使用系统调用fork()创建子进程,有两种调用办法:
(1)ret = fork();
(2)ret = syscall(SYS_fork);
ARM64处理器提供的系统调用指令是svc,调用约定如下:
(1)64位应用程序使用寄存器x8传递系统调用号,32位应用程序使用寄存器x7传递系统调用号
(2)使用寄存器x0~x6最多可以传递7个参数
(3)当系统调用执行完的时候,使用寄存器x0存放返回值。
1. 定义系统调用
Linux内核使用宏SYSCALL_DEFINE定义系统调用,以创建子进程的系统调用fork为例:
kernel/fork.c
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
把宏“SYACALL_DEFINE0(fork)”展开以后是:
asmlinkage long sys_fork(void)
“SYSCALL_DEFINE”后面的数字表示系统调用的参数个数,“SYSCALL_DEFINE0”表示系统调用没有参数,“SYSCALL_DEFINE6”表示系统调用有6个参数,如果参数超过6个,使用宏“SYSCALL_DEFINEx”。头文件“include/linux/syscalls.h”定义了这些宏。
“asmlinkage”表示这个C语言函数可以被汇编代码调用。如果使用C++编译器,“asmlinkage”被定义为extern "C";如果使用C编译器,“asmlinkage”是空的宏。
系统调用的函数名称以“sys_”开头。
需要在系统调用表中保存系统调用号和处理函数的映射关系,ARM64架构定义的系统调用表sys_call_table如下:
arch/arm64/kernel/sys.c
#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>
};
对于ARM64架构,头文件“asm/unistd.h”是“arch/arm64/include/asm/unistd.h”。
arch/arm64/include/asm/unistd.h
#include <uapi/asm/unistd.h>
arch/arm64/include/uapi/asm/unistd.h
#include <asm-generic/unistd.h>
arch/arm64/include/asm-generic/unistd.h
#include <uapi/asm-generic/unistd.h>
arch/arm64/include/uapi/asm-generic/unistd.h
#define __NR_io_setup 0 /* 系统调用号0 */
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
...
#define __NR_rseq 293 /* 系统调用号293 */
__SYSCALL(__NR_rseq, sys_rseq)
#undef __NR_syscalls
#define __NR_syscalls 294
2. 执行系统调用
ARM64处理器把系统调用划分到同步异常,在异常级别1的异常向量表中,系统调用的入口有两个:
(1)如果64位应用程序执行系统调用指令svc,系统调用的入口是el0_sync;
(2)如果32位应用程序执行系统调用指令svc,系统调用的入口是el0_sync_compat。
el0_sync的代码如下:
arch/arm64/entry.S
el0_sync:
kernel_entry 0 // 把所有通用寄存器的值保存在当前进程的内核栈中
mrs x25, esr_el1 // 读取异常症状寄存器esr_el1
lsr x24, x25, #ESR_ELx_EC_SHIFT // 解析出异常症状寄存器的异常类别字段
cmp x24, #ESR_ELx_EC_SVC64 // 如果异常类别是系统调用,跳转到el0_svc
b.eq el0_svc
el0_svc负责执行系统调用,其代码如下:
arch/arm64/entry.S
el0_svc:
mov x0, sp
bl el0_svc_handler
b ret_to_user
ENDPROC(el0_svc)
el0_svc_handler执行系统调用,其代码如下:
arch/arm64/kernel/syscall.c
static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
const syscall_fn_t syscall_table[])
{
unsigned long flags = current_thread_info()->flags;
regs->orig_x0 = regs->regs[0];
regs->syscallno = scno;
cortex_a76_erratum_1463225_svc_handler();
local_daif_restore(DAIF_PROCCTX);
user_exit();
if (has_syscall_work(flags)) {
/* set default errno for user-issued syscall(-1) */
if (scno == NO_SYSCALL)
regs->regs[0] = -ENOSYS;
scno = syscall_trace_enter(regs);
if (scno == NO_SYSCALL)
goto trace_exit;
}
invoke_syscall(regs, scno, sc_nr, syscall_table);
/*
* The tracing status may have changed under our feet, so we have to
* check again. However, if we were tracing entry, then we always trace
* exit regardless, as the old entry assembly did.
*/
if (!has_syscall_work(flags) && !IS_ENABLED(CONFIG_DEBUG_RSEQ)) {
local_daif_mask();
flags = current_thread_info()->flags;
if (!has_syscall_work(flags)) {
/*
* We're off to userspace, where interrupts are
* always enabled after we restore the flags from
* the SPSR.
*/
trace_hardirqs_on();
return;
}
local_daif_restore(DAIF_PROCCTX);
}
trace_exit:
syscall_trace_exit(regs);
}
asmlinkage void el0_svc_handler(struct pt_regs *regs)
{
sve_user_discard();
el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}
ret_to_user从内核空间返回用户空间,其代码定义如下:
arch/arm64/entry.S
/*
* Ok, we need to do extra processing, enter the slow path.
*/
work_pending:
mov x0, sp // 寄存器x0存放第一个参数regs,寄存器x1存放第二个参数task_struct.thread_info.flags */
bl do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on // 在用户空间执行时开启中断
#endif
ldr x1, [tsk, #TSK_TI_FLAGS] // 重新检查单步执行
b finish_ret_to_user
/*
* "slow" syscall return path.
*/
ret_to_user:
disable_daif /* 禁止中断 */
ldr x1, [tsk, #TSK_TI_FLAGS]
and x2, x1, #_TIF_WORK_MASK
cbnz x2, work_pending
finish_ret_to_user:
enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
bl stackleak_erase
#endif
kernel_exit 0
ENDPROC(ret_to_user)
work_pending调用函数do_notify_resume,函数do_notify_resume的代码如下:
arch/arm64/kernel/signal.c
asmlinkage void do_notify_resume(struct pt_regs *regs,
unsigned long thread_flags)
{
/*
* The assembly code enters us with IRQs off, but it hasn't
* informed the tracing code of that for efficiency reasons.
* Update the trace code with the current status.
*/
trace_hardirqs_off();
do {
/* Check valid user FS if needed */
addr_limit_user_check();
if (thread_flags & _TIF_NEED_RESCHED) { /* 如果当前进程的thread_info.flags设置 */
/* Unmask Debug and SError for the next task */
local_daif_restore(DAIF_PROCCTX_NOIRQ); /* 了标志位_TIF_NEED_RESCHED, */
/* 那么以调度进程 */
schedule();
} else {
local_daif_restore(DAIF_PROCCTX);
if (thread_flags & _TIF_UPROBE) /* 如果设置了标志位_TIF_UPROBE,调用函数 */
uprobe_notify_resume(regs); /* uprobe_notify_resume()处理 */
if (thread_flags & _TIF_SIGPENDING) /* 如果设置了标志位_TIF_SIGPENDING,调用 */
do_signal(regs); /* 函数do_signal()处理信号 */
if (thread_flags & _TIF_NOTIFY_RESUME) { /* 如果设置了标志位_TIF_NOTIFY_RESUME, */
clear_thread_flag(TIF_NOTIFY_RESUME); /* 那么调用函数tracehook_notify_resume(), */
tracehook_notify_resume(regs); /* 执行返回用户模式之前的回调函数 */
rseq_handle_notify_resume(NULL, regs);
}
if (thread_flags & _TIF_FOREIGN_FPSTATE) /* 如果设置了标志_TIF_FOREIGN_FPSTATE, */
fpsimd_restore_current_state(); /* 那么恢复浮点寄存器 */
}
local_daif_mask();
thread_flags = READ_ONCE(current_thread_info()->flags);
} while (thread_flags & _TIF_WORK_MASK);
}