Linux系统调用实现原理(基于ARM 64, kernel-6.6)

用户态发起系统调用

当用户态代码调用系统调用时,将会触发一次异常,从而进入内核态。ARM64提供了汇编指令svc,实现该功能。其中x8寄存器存储了系统调用号,参数一般存在x0寄存器到x5寄存器。

以下案例是绕过libc,直接通过内联方法调用系统调用的案例:

arduino 复制代码
int main() {
    // 手动调用 write 系统调用(ARM64)
    // 参数:x0=1(STDOUT), x1=字符串地址, x2=长度, x8=64(write 系统调用号)
    const char *msg = "Raw Syscall: Hello ARM64\n";
    int len = 21;  // 字符串长度
    
    asm volatile (
        "mov x8, #64\n"    // x8 = write 系统调用号(64)
        "svc #0\n"         // 触发系统调用(ARM64 核心指令)
        :                   // 输出寄存器(无)
        : "r"(1), "r"(msg), "r"(len)  // 输入寄存器:x0=1, x1=msg, x2=len
        : "x8", "memory"    // 告诉编译器:x8 和内存会被修改,避免优化
    );

    // 手动调用 exit 系统调用(x8=93, x0=0)
    asm volatile (
        "mov x8, #93\n"    // x8 = exit 系统调用号(93)
        "svc #0\n"
        :
        : "r"(0)           // x0=0(退出码)
        : "x8"
    );

    return 0;
}

编译出中间汇编代码

arduino 复制代码
aarch64-linux-gnu-gcc -S -O0 -fverbose-asm -ffreestanding -nostdlib -o test.s test.c

编译出最后可执行文件

csharp 复制代码
aarch64-linux-gnu-gcc -static -ffreestanding -nostdlib -e main -o test.out test.c

内核执行系统调用

kernel会在内核态实现该异常处理。

el0t_64_sync

kernel的用户态入口实现一般在 arch/arm64/kernel/entry.S

ini 复制代码
entry_handler	0, t, 64, sync  ; 启用 EL0 64位 同步异常(如SVC/Abort/Trap)处理入口
entry_handler	0, t, 64, irq   ; 启用 EL0 64位 普通中断(IRQ)处理入口
entry_handler	0, t, 64, fiq   ; 启用 EL0 64位 快速中断(FIQ)处理入口
entry_handler	0, t, 64, error ; 启用 EL0 64位 系统错误(SError)处理入口

其中entry_handler为汇编中的宏,具体定义如下:

csharp 复制代码
; el:req, ht:req, regsize:req, label:req 代表4个参数
; el\el\ht\()_\regsize\()_\label 拼接出字符串,比如el0t_64_sync
	.macro entry_handler el:req, ht:req, regsize:req, label:req
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

<include/linux/linkage.h>

arduino 复制代码
/* SYM_L_* -- linkage of symbols */
#define SYM_L_GLOBAL(name)			.globl name
#define SYM_L_WEAK(name)			.weak name
#define SYM_L_LOCAL(name)			/* nothing */

/* SYM_A_* -- align the symbol? */
#define SYM_A_ALIGN				ALIGN
#define SYM_A_NONE				/* nothing */

#ifndef SYM_CODE_START_LOCAL
#define SYM_CODE_START_LOCAL(name)			\
	SYM_START(name, SYM_L_LOCAL, SYM_A_ALIGN)
#endif

// ASM_NL 汇编换行符
// 此处生成参数name的段

/* SYM_ENTRY -- use only if you have to for non-paired symbols */
#ifndef SYM_ENTRY
#define SYM_ENTRY(name, linkage, align...)		\
	linkage(name) ASM_NL				\
	align ASM_NL					\
	name:
#endif

/* SYM_START -- use only if you have to */
#ifndef SYM_START
#define SYM_START(name, linkage, align...)		\
	SYM_ENTRY(name, linkage, align)
#endif

所以会在entry.s中生成el0t_64_sync函数段,该函数非常简单,先执行el0t_64_sync_handler,最后调用ret_to_user返回用户态。

el0t_64_sync_handler

<arch/arm64/kernel/entry-common.c>

arduino 复制代码
asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
{
	// 从esr寄存器读取陷入异常的原因
	unsigned long esr = read_sysreg(esr_el1);

	switch (ESR_ELx_EC(esr)) {
	case ESR_ELx_EC_SVC64: // 64 位用户态 SVC 系统调用
		el0_svc(regs);
		break;
	case ESR_ELx_EC_DABT_LOW:
		el0_da(regs, esr);
	...
}

el0_svc -> do_el0_svc -> el0_svc_common(arch/arm64/kernel/syscall.c文件中) -> invoke_syscall

invoke_syscall

<arch/arm64/kernel/syscall.c>

scss 复制代码
void do_el0_svc(struct pt_regs *regs)
{
	// sys_call_table 系统调用表
	// regs->regs[8]中获取到系统调用号
	el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}

static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
			   const syscall_fn_t syscall_table[])
{
	...
	invoke_syscall(regs, scno, sc_nr, syscall_table);
	...
}

static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
	return syscall_fn(regs);
}

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);

	/*
	 * This value will get limited by KSTACK_OFFSET_MAX(), which is 10
	 * bits. The actual entropy will be further reduced by the compiler
	 * when applying stack alignment constraints: the AAPCS mandates a
	 * 16-byte aligned SP at function boundaries, which will remove the
	 * 4 low bits from any entropy chosen here.
	 *
	 * The resulting 6 bits of entropy is seen in SP[9:4].
	 */
	choose_random_kstack_offset(get_random_u16());
}

内核实现系统调用

系统调用表

arduino 复制代码
...

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

// 通过宏声明系统调用的实现函数
// 拼接的函数名,比如 __arm64_mmap
#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>
};

unistd.h头文件嵌套: arch/arm64/include/asm/unistd.h -> arch/arm64/include/uapi/asm/unistd.h -> include/uapi/asm-generic/unistd.h
<include/uapi/asm-generic/unistd.h> 中罗列了系统调用列表。

arduino 复制代码
...
#define __NR_setxattr 5
__SYSCALL(__NR_setxattr, sys_setxattr)
#define __NR_lsetxattr 6
__SYSCALL(__NR_lsetxattr, sys_lsetxattr)
#define __NR_fsetxattr 7
__SYSCALL(__NR_fsetxattr, sys_fsetxattr)
#define __NR_getxattr 8
__SYSCALL(__NR_getxattr, sys_getxattr)
...

定义系统调用

系统调用表中的函数,是通过SYSCALL_DEFINE宏定义的。 比如mmap的系统调用实现如下:

<arch/arm64/kernel/sys.c>

arduino 复制代码
#include <linux/compiler.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/export.h>
#include <linux/sched.h>
#include <linux/slab.h>
// 会嵌套调用 #include <asm/syscall_wrapper.h>
#include <linux/syscalls.h>

#include <asm/cpufeature.h>
#include <asm/syscall.h>

...

SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
		unsigned long, prot, unsigned long, flags,
		unsigned long, fd, unsigned long, off)
{
	if (offset_in_page(off) != 0)
		return -EINVAL;

	return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

<include/linux/syscalls.h>

arduino 复制代码
#include <linux/signal.h>
#include <linux/list.h>
#include <linux/bug.h>
#include <linux/sem.h>
#include <asm/siginfo.h>
#include <linux/unistd.h>
#include <linux/quota.h>
#include <linux/key.h>
#include <linux/personality.h>
#include <trace/syscall.h>

// 如果打开该宏,引入syscall_wrapper头文件
// arm64上使用该头文件的 SYSCALL_DEFINE 宏定义
 
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
#include <asm/syscall_wrapper.h>
#endif /
...

<arch/arm64/include/asm/syscall_wrapper.h>

scss 复制代码
#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);		\
	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__))

#define SYSCALL_DEFINE0(sname)							\
	SYSCALL_METADATA(_##sname, 0);						\
	asmlinkage long __arm64_sys_##sname(const struct pt_regs *__unused);	\
	ALLOW_ERROR_INJECTION(__arm64_sys_##sname, ERRNO);			\
	asmlinkage long __arm64_sys_##sname(const struct pt_regs *__unused)

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

ARM64上默认开启了ARCH_HAS_SYSCALL_WRAPPER宏。

<arch/arm64/Kconfig>

lua 复制代码
config ARM64
	def_bool y
	...
	select ARCH_HAS_SYSCALL_WRAPPER

所以 SYSCALL_DEFINE6 mmap 会定义 __arm64_sys_mmap 函数,即为系统调用表中mmap系统调用的实现。

CPU如何找到异常处理函数

执行svc指令,为什么会调到el0t_64_sync函数呢? 其实是通过查询启动时设置进ARM规定的寄存器内异常向量表,找到异常处理函数后进行跳转的。

生成异常向量表

异常向量表也是通过汇编宏实现的:
<arch/arm64/kernel/entry.S>
1. 定义汇编宏

less 复制代码
.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
	.align 7
; 第一步,如果是从el0进el1,备份和清理tpidrro_el0
.Lventry_start\@:
	.if	\el == 0
	/*
	 * This must be the first instruction of the EL0 vector entries. It is
	 * skipped by the trampoline vectors, to trigger the cleanup.
	 */
	b	.Lskip_tramp_vectors_cleanup\@
	.if	\regsize == 64
	mrs	x30, tpidrro_el0
	msr	tpidrro_el0, xzr
	.else
	mov	x30, xzr
	.endif
.Lskip_tramp_vectors_cleanup\@:
	.endif

; 第二步,在stack上构建异常栈帧
	sub	sp, sp, #PT_REGS_SIZE

; 第三步,跳转到异常处理函数段,比如el0t_64_sync
	b	el\el\ht\()_\regsize\()_\label
.org .Lventry_start\@ + 128	// Did we overflow the ventry slot?
	.endm

2. 拼接构建异常向量表

go 复制代码
/*
 * 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 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)

异常向量表的布局,需要严格遵守ARM的约定:

《Learn the architecture - AArch64 Exception Model》

中文解释如下:

相对基址偏移 异常入口类型 异常等级/模式 触发场景(举例) 对应内核代码中的宏
0x000 Synchronous EL0 (AArch64) 用户态系统调用(svc #0)、未定义指令 tramp_ventry (EL0,64)
0x080 IRQ EL0 (AArch64) 用户态外设中断(如键盘中断) tramp_ventry (EL0,64)
0x100 FIQ EL0 (AArch64) 用户态快速中断(极少使用) tramp_ventry (EL0,64)
0x180 Error EL0 (AArch64) 用户态数据访问错误(如非法地址) tramp_ventry (EL0,64)
0x200 Synchronous EL1t (AArch64) 内核态同步异常(如空指针访问) kernel_ventry(1,t,64,sync)
0x280 IRQ EL1t (AArch64) 内核态外设中断(如网卡中断) kernel_ventry(1,t,64,irq)
0x300 FIQ EL1t (AArch64) 内核态快速中断(极少使用) kernel_ventry(1,t,64,fiq)
0x380 Error EL1t (AArch64) 内核态数据访问错误(如页表错误) kernel_ventry(1,t,64,error)
0x400 Synchronous EL1h (AArch64) 虚拟化模式内核同步异常 kernel_ventry(1,h,64,sync)
0x480 IRQ EL1h (AArch64) 虚拟化模式内核IRQ中断 kernel_ventry(1,h,64,irq)
0x500 FIQ EL1h (AArch64) 虚拟化模式内核FIQ中断 kernel_ventry(1,h,64,fiq)
0x580 Error EL1h (AArch64) 虚拟化模式内核错误异常 kernel_ventry(1,h,64,error)
0x600 Synchronous EL0 (AArch32) 32位用户态系统调用 tramp_ventry (EL0,32)
0x680 IRQ EL0 (AArch32) 32位用户态IRQ中断 tramp_ventry (EL0,32)
0x700 FIQ EL0 (AArch32) 32位用户态FIQ中断 tramp_ventry (EL0,32)
0x780 Error EL0 (AArch32) 32位用户态错误异常 tramp_ventry (EL0,32)

备份清理tpidrro_el0

tpidrro_el0:ARM64 架构中EL0(用户态)只读的线程局部存储(TLS)寄存器,核心作用是为用户态线程提供 "线程私有数据的快速访问入口"。

ini 复制代码
.if	\regsize == 64
	; 将tpidrro_el0备份到x30寄存器
	mrs	x30, tpidrro_el0
	; 用零值寄存器xzr清理tpidrro_el0
	msr	tpidrro_el0, xzr

为什么要保存tpidrro_el0寄存器

  1. 防止用户态数据泄露到内核态,引起安全性问题
  2. 调试等操作可能污染tpidrro_el0寄存器数据
  3. tpidrro_el0寄存器不会自动保存,此处不保存的话,如果切换任务后,tpidrro_el0会被其他用户态进程干扰污染
  4. tpidrro_el0是用户态只读寄存器,因此依赖内核做备份和清理操作

为什么使用x30寄存器

x30寄存器,在arm64中为Link Register (LR)链接寄存器,存储函数调用的返回地址(bl 指令会自动将返回地址写入 x30);若无函数调用时,可作为普通通用寄存器使用。

  1. 陷入异常时,并未发生函数调用,因此x30可以复用。而x0-x29等通用寄存器需要保留,后续会用于保存栈帧。
  2. 异常切换时,CPU只会自动保存/恢复通用寄存器(x0-x30),所以会用x30作为备份寄存器。

设置异常向量表

<arch/arm64/kernel/head.S>

scss 复制代码
SYM_FUNC_START_LOCAL(__primary_switched)
	adr_l	x4, init_task
	init_cpu_task x4, x5, x6

	// 加载异常向量表(vectors)的虚拟地址到x8
	adr_l	x8, vectors			// load VBAR_EL1 with virtual
	// 将向量表地址写入VBAR_EL1寄存器
	msr	vbar_el1, x8			// vector table address
	// 指令同步屏障,确保VBAR_EL1修改立即生效
	isb
	...

调用顺序:primary_entry -> __primary_switch -> __primary_switched,而 primary_entry 是 ARM64 主 CPU 核上电后执行的第一个汇编入口。

VBAR_EL1 是 ARM64 架构中EL1(内核态)的向量基地址寄存器(Vector Base Address Register),核心作用是存储异常向量表的虚拟基地址。
因此发生系统调用时,cpu通过vbar_el1查询到异常处理函数地址,进而跳转到异常处理函数。

相关推荐
chasten082 小时前
Android开发wsl直接使用adb方法
操作系统
Trouvaille ~17 小时前
【Linux】理解“一切皆文件“与缓冲区机制:Linux文件系统的设计哲学
linux·运维·服务器·操作系统·进程·文件·缓冲区
添砖java‘’2 天前
Linux信号机制详解:从产生到处理
linux·c++·操作系统·信号处理
元亓亓亓2 天前
考研408--操作系统--day9--I/O设备(上)
考研·操作系统·i/o·408
法欧特斯卡雷特2 天前
如何解决 Kotlin/Native 在 Windows 下 main 函数的 args 乱码?
后端·操作系统·编程语言
小林up2 天前
【MIT-OS6.S081作业-4-2】Lab4-traps-Backtrace
操作系统·xv6
fakerth2 天前
【OpenHarmony】USB服务组件
操作系统·openharmony
悄悄敲敲敲3 天前
Linux:信号(二)
linux·操作系统·信号
青春pig头少年3 天前
决战408:OS大题我拿拿拿(非PV)
操作系统·学习笔记·408