在 ARM64(AArch64)架构下的 Linux 系统中,系统调用(syscall)是用户态程序访问内核功能的接口。以下是详细流程,包括用户态准备、陷入内核、内核处理及返回的完整过程:


1. 用户态流程(触发系统调用)

步骤1: 准备参数和系统调用号

  • 参数传递:通过寄存器传递参数(符合 ARM64 AAPCS64 调用约定):
  • x0 – x5:系统调用的前 6 个参数(arg0–arg5)。
  • x8:存储系统调用号(定义在 asm-generic/unistd.h 中,如 write=64)。
  • 更多参数:若参数超过 6 个,通过栈传递(但内核标准系统调用一般不超过 6 个参数)。

步骤2: 执行 svc 指令

  • 用户程序执行 svc #0(Supervisor Call)指令,触发异常(同步异常),切换到内核态(EL1)。
  • 硬件自动行为(CPU 完成):
  1. 保存返回地址到 ELR_EL1(PC+4)。
  2. 保存状态寄存器 PSTATE 到 SPSR_EL1。
  3. 切换至内核栈指针 SP_EL1。
  4. 设置异常类型为 “同步异常”(ESR_EL1 寄存器记录原因)。

示例代码(汇编)

mov x0, #1          // fd = stdout (arg0)
ldr x1, =message    // buffer address (arg1)
mov x2, #12         // length (arg2)
mov x8, #64         // syscall number: __NR_write (64)
svc #0              // 触发系统调用

2. 内核态流程(处理系统调用)

步骤1: 异常向量表跳转

  • 硬件行为
  • CPU 根据 VBAR_EL1(向量基址寄存器)跳转到异常向量表。
  • 系统调用属于 EL0(用户态)触发的同步异常,定位到向量表项 el0_sync。
  • 入口代码:arch/arm64/kernel/entry.S:
  .align 11          // 向量表2KB对齐
  VECTORS_BASE:
  ...
  el0_sync:          // EL0同步异常入口
    kernel_entry 0   // 保存用户态现场
    mrs x25, esr_el1 // 读异常原因
    lsr x24, x25, #ESR_ELx_EC_SHIFT
    cmp x24, #ESR_ELx_EC_SVC64 // 检查是否为SVC异常
    b.eq el0_svc     // 是系统调用,跳转处理

步骤2: 系统调用处理 (el0_svc)

  • 调用内核函数:
  • el0_svc 从 sys_call_table 根据系统调用号(scno = w8)获取处理函数地址:
el0_svc:
  adrp stbl, sys_call_table // 加载系统调用表
  uxtw scno, w8 // 系统调用号存入 scno (x26)
  ldr x16, [stbl, scno, lsl #3] // 查表(64位地址)
  blr x16 // 执行系统调用函数

步骤3: 返回用户态

  • 恢复上下文
    在内核栈的 pt_regs 中恢复用户寄存器(含更新后的返回值 x0)。
  • 调用 kernel_exit 宏
  kernel_exit 0      // 恢复寄存器,包括ELR_EL1和SPSR_EL1
  • 执行 eret 指令
  • CPU 从 ELR_EL1 恢复用户态 PC(svc #0 的下一条指令)。
  • 从 SPSR_EL1 恢复状态寄存器(切回用户态 EL0)。
  • 继续执行用户程序。

关键机制详解

系统调用表 (sys_call_table):

  • 定义sys_call_table表:arch/arm64/kernel/sys.c
/*
 * The sys_call_table array must be 4K aligned to be accessible from
 * kernel/entry.S.
 */
void * const sys_call_table[__NR_syscalls] __aligned(4096) = {
	[0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include <asm/unistd.h> // 通过宏展开填充所有系统调用
};

系统调用号

  • 定义所有系统调用号:include/uapi/asm-generic/unistd.h
/* fs/read_write.c */
#define __NR3264_lseek 62
__SC_3264(__NR3264_lseek, sys_llseek, sys_lseek)
#define __NR_read 63
__SYSCALL(__NR_read, sys_read)
#define __NR_write 64
__SYSCALL(__NR_write, sys_write)
#define __NR_readv 65
__SC_COMP(__NR_readv, sys_readv, compat_sys_readv)
#define __NR_writev 66
__SC_COMP(__NR_writev, sys_writev, compat_sys_writev)
#define __NR_pread64 67
__SC_COMP(__NR_pread64, sys_pread64, compat_sys_pread64)
#define __NR_pwrite64 68
__SC_COMP(__NR_pwrite64, sys_pwrite64, compat_sys_pwrite64)
#define __NR_preadv 69
__SC_COMP(__NR_preadv, sys_preadv, compat_sys_preadv)
#define __NR_pwritev 70
__SC_COMP(__NR_pwritev, sys_pwritev, compat_sys_pwritev)

系统调用函数

  • 定义sys_read系统调用函数:fs/read_write.c
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos = file_pos_read(f.file);
		ret = vfs_read(f.file, buf, count, &pos);
		if (ret >= 0)
			file_pos_write(f.file, pos);
		fdput_pos(f);
	}
	return ret;
}

SYSCALL_DEFINE3宏展开过程中会产生一个函数,即可得到sys_read系统调用函数:asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

内核处理流程

以sys_read为例,逐级调用过程:sys_read->vfs_read->__vfs_read->file->f_op->read(用户驱动代码)


完整流程图

用户态:
  准备参数: x0–x5, 系统调用号 x8
  └─ 执行 svc #0 (触发异常)
      │
内核态:
  硬件保存 ELR_EL1, SPSR_EL1 → 跳转到 VBAR_EL1 + 0x400 (el0_sync)
      ├─ kernel_entry 0 (保存用户现场)
      ├─ 识别为 SVC → el0_svc
      ├─ 检查系统调用号有效性
      ├─ 调用 sys_call_table[syscall](x0..x5) 
      │   ├─ 执行具体功能 (如 sys_write)
      │   └─ 结果存入 x0
      ├─ kernel_exit 0 (恢复现场)
      └─ eret (返回用户态继续执行)

此流程确保用户态程序通过标准化接口安全访问内核功能,是 Linux 进程隔离和权限控制的核心机制。

Tags:

No responses yet

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注