8 min read

系统调用

1、概述

linux中有很多的命令是通过用户态实现的调用,例如最基础的readwrite函数,通过系统调用,陷入内核态,执行内核中的相关系统sys_readsys_writeAPI逻辑。

image-20240122201502485.png

函数的命名也基本遵循上述规律,用户态的函数通常是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

则是把三个参数分别传入rdirsirdx中,把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