系统调用
1、概述
linux中有很多的命令是通过用户态实现的调用,例如最基础的read
、write
函数,通过系统调用,陷入内核态,执行内核中的相关系统sys_read
、sys_write
API逻辑。
函数的命名也基本遵循上述规律,用户态的函数通常是xyz()
,而内核态中的相关调用则是sys_xyz()
。由于整个流程涉及到内核态与用户态的切换,所以系统调用的实现与架构强相关。
2、用户态实现
通常来说,标准的posix系统调用例如是被定义在glibc
的包里。
比如read函数sysdeps/unix/sysv/linux/read.c
/* Read NBYTES into BUF from FD. Return the number read or -1. */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (read, fd, buf, nbytes);
}
libc_hidden_def (__libc_read)
#define __INLINE_SYSCALL0(name) \
INLINE_SYSCALL (name, 0)
#define __INLINE_SYSCALL1(name, a1) \
INLINE_SYSCALL (name, 1, a1)
#define __INLINE_SYSCALL2(name, a1, a2) \
INLINE_SYSCALL (name, 2, a1, a2)
#define __INLINE_SYSCALL3(name, a1, a2, a3) \
INLINE_SYSCALL (name, 3, a1, a2, a3)
#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \
INLINE_SYSCALL (name, 4, a1, a2, a3, a4)
#define __INLINE_SYSCALL5(name, a1, a2, a3, a4, a5) \
INLINE_SYSCALL (name, 5, a1, a2, a3, a4, a5)
#define __INLINE_SYSCALL6(name, a1, a2, a3, a4, a5, a6) \
INLINE_SYSCALL (name, 6, a1, a2, a3, a4, a5, a6)
#define __INLINE_SYSCALL7(name, a1, a2, a3, a4, a5, a6, a7) \
INLINE_SYSCALL (name, 7, a1, a2, a3, a4, a5, a6, a7)
#define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
#define __INLINE_SYSCALL_NARGS(...) \
__INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,)
#define __INLINE_SYSCALL_DISP(b,...) \
__SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)
/* Issue a syscall defined by syscall number plus any other argument
required. Any error will be handled using arch defined macros and errno
will be set accordingly.
It is similar to INLINE_SYSCALL macro, but without the need to pass the
expected argument number as second parameter. */
#define INLINE_SYSCALL_CALL(...) \
__INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)
上述__VA_ARGS__
是C语言预处理器中的一个特殊宏,用于表示可变参数。在定义带有可变参数的宏时,可以使用__VA_ARGS__
来表示可变参数列表。当宏展开时,__VA_ARGS__
会被替换为实际传递给宏的参数列表。由于read在最初调用的时候代用了3个参数,所以最终经历上述一系列宏定义,最终会展开成__INLINE_SYSCALL3(read, fd, buf, nbytes)
进一步调用到INTERNAL_SYSCALL
,而这个函数则是与结构相关的。
#undef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr, args...) \
({ \
long int sc_ret = INTERNAL_SYSCALL (name, nr, args); \
__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret)) \
? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret)) \
: sc_ret; \
})
2.1-arm64
会通过svc命令,是系统陷入内核态,并且把name
对应的系统调用号写入到x8
寄存器,进一步由内核态处理相关调用。
# define INTERNAL_SYSCALL(name, nr, args...) \
INTERNAL_SYSCALL_RAW(SYS_ify(name), nr, args)
# define INTERNAL_SYSCALL_RAW(name, nr, args...) \
({ long _sys_result; \
{ \
LOAD_ARGS_##nr (args) \
register long _x8 asm ("x8") = (name); \
asm volatile ("svc 0 // syscall " # name \
: "=r" (_x0) : "r"(_x8) ASM_ARGS_##nr : "memory"); \
_sys_result = _x0; \
} \
_sys_result; })
而arrch64中关于read
的系统调用号则被定义在了sysdeps/unix/sysv/linux/aarch64/arch-syscall.h
#define __NR_read 63
2.2-x86_64
则是把三个参数分别传入rdi
、rsi
、rdx
中,把name
对应的系统调用号放到rax
寄存器里,通过syscall
的汇编函数陷入内核态。
并且"=a" (resultvar)
代表着把系统调用的返回值放入rax
寄存器里。(如果是x86的则是eax
寄存器)
#define INTERNAL_SYSCALL(name, nr, args...) \
internal_syscall##nr (SYS_ify (name), args)
#define internal_syscall3(number, arg1, arg2, arg3) \
({ \
unsigned long int resultvar; \
TYPEFY (arg3, __arg3) = ARGIFY (arg3); \
TYPEFY (arg2, __arg2) = ARGIFY (arg2); \
TYPEFY (arg1, __arg1) = ARGIFY (arg1); \
register TYPEFY (arg3, _a3) asm ("rdx") = __arg3; \
register TYPEFY (arg2, _a2) asm ("rsi") = __arg2; \
register TYPEFY (arg1, _a1) asm ("rdi") = __arg1; \
asm volatile ( \
"syscall\n\t" \
: "=a" (resultvar) \
: "0" (number), "r" (_a1), "r" (_a2), "r" (_a3) \
: "memory", REGISTERS_CLOBBERED_BY_SYSCALL); \
(long int) resultvar; \
})
ENTRY (syscall)
movq %rdi, %rax /* Syscall number -> rax. */
movq %rsi, %rdi /* shift arg1 - arg5. */
movq %rdx, %rsi
movq %rcx, %rdx
movq %r8, %r10
movq %r9, %r8
movq 8(%rsp),%r9 /* arg6 is on the stack. */
syscall /* Do the system call. */
cmpq $-4095, %rax /* Check %rax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
ret /* Return to caller. */
PSEUDO_END (syscall)
x86中read的系统调用号是sysdeps/unix/sysv/linux/x86_64/64/arch-syscall.h
#define __NR_read 0
3、内核态实现
上述glibc中定义的用户态接口已经完成了其使命,在进入内核态后,内核则会继续处理对应的逻辑。
3.1-arm64
arm64内核在通过svc
指令陷入同步异常
后,则会通过arch/arm64/kernel/entry.S
中的异常向量表来执行对应的处理逻辑
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 EL1t
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)
kernel_ventry是一个宏,用于生成异常处理程序的入口点。它接受4个参数:
- 第一个参数表示异常发生时处理器所处的异常级别。0表示EL0(用户模式),1表示EL1(内核模式)。
- 第二个参数表示处理器的运行模式。t表示Thumb模式,h表示Hyp模式。
- 第三个参数表示处理器的指令集。64表示64位指令集(AArch64),32表示32位指令集(AArch32)。
- 第四个参数表示异常类型。sync表示同步异常,irq表示IRQ中断,fiq表示FIQ中断,error表示错误。
由于是从用户态64位触发的同步异常,所以直接回调用到
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
而kernel_ventry的展开则是如下
.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
......
b 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)
.endm
最终会展开成el0t_64_sync_handler
这个c函数
asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_SVC64:
el0_svc(regs);
break;
......
}
}
通过一系列调用el0_svc
->do_el0_svc
->el0_svc_common
->invoke_syscall
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);
choose_random_kstack_offset(get_random_u16() & 0x1FF);
}
而在linux kernel中,则会把系统调用号定义在arch/arm64/kernel/sys.c
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
更进一步的可以看到在include/uapi/asm-generic/unistd.h
中的具体定义,从而最终调用到sys_read
的函数
#define __NR_read 63
__SYSCALL(__NR_read, sys_read)
3.2-x86
64位的x86会再初始化的时候设置一个系统调入的入口函数,而MSR_LSTAR
这个寄存器就是我们上文分析的syscall
指令会调用到的,通过下列函数,初始化号这个寄存器对用的入口函数
(syscall指令可以参考最新 x86_64 系统调用入口分析)
void syscall_init(void)
{
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
......
}
每次当通过syscall指令进入内核态,cpu会调用中的arch\x86\entry\entry_64.S
对用函数entry_SYSCALL_64
SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_ENTRY
ENDBR
swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp
SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
ANNOTATE_NOENDBR
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
/* IRQs are off. */
movq %rsp, %rdi
/* Sign extend the lower 32bit as syscall numbers are treated as int */
movslq %eax, %rsi
/* clobbers %rax, make sure it is after saving the syscall nr */
IBRS_ENTER
UNTRAIN_RET
call do_syscall_64 /* returns with IRQs disabled */
通过系列调用,最终调用到c函数do_syscall_64
->do_syscall_x64
/* Returns true to return using SYSRET, or false to use IRET */
__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)
{
add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr); // 检查一遍系统调用号
instrumentation_begin();
if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) { // 执行系统调用
/* Invalid system call, but still a system call. */
regs->ax = __x64_sys_ni_syscall(regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs); // 退出系统调用,把返回值写回rax寄存器
......
/* Use SYSRET to exit to userspace */
return true;
}
最后通过sys_call_table
来执行具体的系统调用
static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr)
{
/*
* Convert negative numbers to very high and thus out of range
* numbers for comparisons.
*/
unsigned int unr = nr;
if (likely(unr < NR_syscalls)) {
unr = array_index_nospec(unr, NR_syscalls);
regs->ax = sys_call_table[unr](regs);
return true;
}
return false;
}
可以看到sys_call_table
的实现和arm是类似的
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
而asm/syscalls_64.h
则是arch/x86/entry/syscalls/syscalltbl.sh
中的特殊脚本生成,而这个流程则是再make内核时候,由Makefile调用完成。
syscall32 := $(src)/syscall_32.tbl
syscall64 := $(src)/syscall_64.tbl
syshdr := $(srctree)/scripts/syscallhdr.sh
systbl := $(srctree)/scripts/syscalltbl.sh
offset :=
prefix :=
......
quiet_cmd_systbl = SYSTBL $@
cmd_systbl = $(CONFIG_SHELL) $(systbl) --abis $(abis) $< $@
......
$(uapi)/unistd_64.h: abis := common,64
$(uapi)/unistd_64.h: $(syscall64) $(syshdr) FORCE
$(call if_changed,syshdr)
而实际定义系统调用号的文件是arch/x86/entry/syscalls/syscall_64.tbl
0 common read sys_read
4、参考
https://zhuanlan.zhihu.com/p/626203155
https://blog.csdn.net/m0_70749039/article/details/135587884

