Linux 系统调用在 ARM 上的实现与工作机制

Linux 系统调用在 ARM 上的实现与工作机制

本文以 Linux 4.4.94 源码为依据,面向 ARM 架构系统调用(syscall)的实现原理、工作机制与调用路径进行系统化讲解,并结合真实源码片段与实际案例(write(2))帮助理解。最后提供流程图、时序图与关键源码位置索引,形成可独立阅读的参考文档。

内容概览

  • 系统调用的角色与边界
  • ARM 用户态→内核态:SVC/SWI 陷入机制与 ABI 差异
  • 入口实现:vector_swi、系统调用表与分发
  • 参数与返回值传递规则(寄存器约定)
  • SYSCALL_DEFINE* 宏、SyS 包装与元数据
  • 用户内存访问与安全(uaccesscopy_from_user
  • 实战案例:write(2) 的完整调用路径
  • 兼容与特殊调用(OABI/EABI、ARM 私有调用)
  • 追踪与性能(tracepoints、ftrace)
  • 常见问题与调试建议
  • 参考源码位置与扩展阅读

为什么需要系统调用

  • 用户态程序不能直接访问关键内核资源(进程管理、文件系统、网络栈、内存管理等)。
  • 系统调用提供受控的内核服务入口,保证权限隔离、内存安全与一致性。调用路径:用户态发起 → CPU 指令触发异常 → 进入内核入口 → 参数校验与权限检查 → 调用具体 sys_* 内核实现 → 返回结果。

ARM 用户态到内核态:SVC/SWI 陷入机制

  • 指令层次:ARMv7 等架构中通过 SVC(以前称 SWI)指令触发软中断异常,进入内核异常向量 vector_swi
  • ABI 差异:
    • EABI(现代 ARM Linux 用户态)使用寄存器 r7 传递系统调用号(scno)。
    • OABI(旧 ABI)从 SWI 指令立即数字段取号(从 lr - 4 读取指令编码)。
  • 内核入口的通用流程:保存现场 → 获取系统调用号 scno → 禁用用户访问窗口 → 取 sys_call_table → 越界/追踪检测 → 分发到 sys_* 函数 → 返回路径(快速或慢速)。

关键入口源码(节选)

文件:arch/arm/kernel/entry-common.S

asm 复制代码
ENTRY(vector_swi)
    /* ...保存现场、从 SPSR/PC 获取状态... */
    /* 获取系统调用号 */
#if defined(CONFIG_OABI_COMPAT)
    /* 旧 ABI:从 SWI 指令读取立即数 */
    ldr     r10, [lr, #-4]
    /* ...处理大小端、掩码、号基数... */
#elif defined(CONFIG_AEABI)
    /* 纯 EABI:用户态将 scno 放在 r7 */
    /* scno 已在 r7 */
#elif defined(CONFIG_ARM_THUMB)
    /* Legacy ABI + 可能是 thumb 模式 */
    tst     r8, #PSR_T_BIT
    addne   scno, r7, #__NR_SYSCALL_BASE
    ldreq   scno, [lr, #-4]
#else
    /* Legacy ABI:直接读指令 */
    ldr     scno, [lr, #-4]
#endif

    uaccess_disable tbl
    adr     tbl, sys_call_table        @ 取系统调用表地址

local_restart:
    ldr     r10, [tsk, #TI_FLAGS]      @ 检查追踪标志
    tst     r10, #_TIF_SYSCALL_WORK
    bne     __sys_trace

    cmp     scno, #NR_syscalls         @ 上限检查
    badr    lr, ret_fast_syscall       @ 设置返回地址
    ldrcc   pc, [tbl, scno, lsl #2]    @ 分发:跳到 sys_* 例程

    /* ...ARM 私有号处理与错误回退... */
ENDPROC(vector_swi)

    /* sys_call_table 声明与填充 */
ENTRY(sys_call_table)
#include "calls.S"
ENDPROC(sys_syscall)
  • 说明:入口根据 ABI 从寄存器或指令立即数获取 scno 后,访问 sys_call_table[scno],直接跳转到对应的 sys_* 实现。

系统调用表与号码来源

  • 表定义:sys_call_table 在 ARM 由 calls.S 汇编文件生成;每行对应一个系统调用入口函数。
  • 号码定义:arch/arm/include/uapi/asm/unistd.h__NR_SYSCALL_BASE + N 定义各 __NR_* 号,ARM 私有号以 __ARM_NR_BASE 开始。
  • 数量一致性:entry-common.S 中对 NR_syscalls 和表大小做编译期校验,确保一致。

calls.S(片段)

文件:arch/arm/kernel/calls.S

asm 复制代码
/* 0 */   CALL(sys_restart_syscall)
          CALL(sys_exit)
          CALL(sys_fork)
          CALL(sys_read)
          CALL(sys_write)
/* 5 */   CALL(sys_open)
          CALL(sys_close)
          CALL(sys_ni_syscall)        /* was sys_waitpid */
          CALL(sys_creat)
          CALL(sys_link)
/* 10 */  CALL(sys_unlink)
          CALL(sys_execve)
          CALL(sys_chdir)
          CALL(OBSOLETE(sys_time))    /* used by libc4 */
          CALL(sys_mknod)
/* ...中略... */
/* 120 */ CALL(sys_clone)
/* ...中略... */
/* 180 */ CALL(ABI(sys_pread64, sys_oabi_pread64))
          CALL(ABI(sys_pwrite64, sys_oabi_pwrite64))
/* ...中略... */
/* 225 */ CALL(sys_setxattr)
          CALL(sys_lsetxattr)
          CALL(sys_fsetxattr)

号段与 ARM 私有调用(片段)

文件:arch/arm/include/uapi/asm/unistd.h

c 复制代码
#define __NR_execveat      (__NR_SYSCALL_BASE+387)
#define __NR_userfaultfd   (__NR_SYSCALL_BASE+388)
#define __NR_membarrier    (__NR_SYSCALL_BASE+389)
#define __NR_mlock2        (__NR_SYSCALL_BASE+390)

/* ARM 私有 SWI 号段 */
#define __ARM_NR_BASE      (__NR_SYSCALL_BASE+0x0f0000)
#define __ARM_NR_breakpoint    (__ARM_NR_BASE+1)
#define __ARM_NR_cacheflush    (__ARM_NR_BASE+2)
#define __ARM_NR_set_tls       (__ARM_NR_BASE+5)
  • 说明:__ARM_NR_* 是 ARM 平台保留的 SWI 范围(如 set_tlscacheflush),入口根据号基数分支到相应处理流程。

参数与返回值传递(寄存器约定)

  • 传参寄存器:ARM 最多使用 r0--r6 传递参数;EABI 用 r7 传递系统调用号。
  • 返回值:r0 用作返回值/错误码(负值表示错误)。
  • 获取与设置接口:arch/arm/include/asm/syscall.h 提供统一访问封装。
c 复制代码
/* 读取系统调用号与参数 */
static inline int syscall_get_nr(struct task_struct *task, struct pt_regs *regs) {
    return task_thread_info(task)->syscall;
}

static inline void syscall_get_arguments(struct task_struct *task,
    struct pt_regs *regs, unsigned int i, unsigned int n, unsigned long *args)
{
    if (i == 0) { args[0] = regs->ARM_ORIG_r0; /* 原始 r0 */ args++; i++; n--; }
    memcpy(args, &regs->ARM_r0 + i, n * sizeof(args[0]));
}

/* 设置返回值 */
static inline void syscall_set_return_value(struct task_struct *task,
    struct pt_regs *regs, int error, long val)
{
    regs->ARM_r0 = (long) error ? error : val;
}

SYSCALL_DEFINE* 宏、SyS 包装与元数据

  • 宏位置:include/linux/syscalls.h
  • 作用:统一声明 sys_* 原型,生成 SyS* 别名包装,做参数类型拓宽/检查,并向 __syscalls_metadata 收集事件元数据(供 tracepoints 使用)。
c 复制代码
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define __SYSCALL_DEFINEx(x, name, ...)                      \
    asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\
        __attribute__((alias(__stringify(SyS##name))));       \
    static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
    asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \ \
    {                                                         \
        long ret = SYSC##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 SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
  • 说明:源文件中按 SYSCALL_DEFINE3(write, ...) 定义,宏负责生成 sys_writeSyS_write 等包装,并注入可追踪的元数据。

用户内存访问与安全

  • uaccess_disable/uaccess_enable:在入口阶段根据需要禁用/启用用户访问窗口,避免错误的用户态访问。
  • copy_from_user/copy_to_user:显式拷贝并检查用户态指针合法性(access_ok)。
  • EFAULT/EPERM 等错误码:在参数验证或权限检查失败时返回负值错误码。

示例位置:fs/read_write.crw_copy_check_uvector() 使用 copy_from_user() 批量检入 iovec。


案例解析:write(2) 完整路径

用户态

c 复制代码
/* 用户态伪代码 */
ssize_t n = write(fd, buf, count);  // r7=scno=__NR_write, r0..r2 传参

内核分发与实现(ARM 通用入口 → VFS 层)

文件:fs/read_write.c

c 复制代码
SYSCALL_DEFINE3(write, unsigned int, fd, const 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_write(f.file, buf, count, &pos);
        if (ret >= 0)
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }
    return ret;
}
  • 关键步骤:
    • fdget_pos(fd):解析文件描述符,获得 struct file* 与当前位置;失败则 -EBADF
    • vfs_write():进入 VFS 层,调度具体文件系统的 .write_iter/.write 实现;可能触发文件系统回写、页缓存更新等。
    • 位置更新与资源释放:成功时更新 f_pos,最后 fdput_pos() 释放引用。

端到端时序图

User-space ARM CPU Kernel SVC 异常进入 vector_swi 保存现场、解析 scno、uaccess_disable tbl=sys_call_table;边界检查与追踪处理 跳转 sys_write vfs_write → 具体文件系统实现 设置 r0 返回值;恢复现场 返回用户态,write() 得到结果 User-space ARM CPU Kernel

入口分支流程(EABI/OABI)

flowchart TD A{EABI?} -->|Yes| B[r7 -> scno] A -->|Legacy/OABI| C[读 SWI 立即数 (lr-4) -> scno] B --> D{scno < NR_syscalls?} C --> D D -->|Yes| E[跳转 sys_call_table[scno] → sys_*] D -->|No| F[ARM 私有号/未实现 → sys_ni_syscall] E --> G[ret_fast_syscall 返回路径] F --> H[错误码或特例处理]

(ASCII 备选)

复制代码
EABI: r7 -> scno
OABI: [lr - 4] SWI -> scno
   └─ 检查上限 → sys_call_table[scno] → sys_* → 返回

系统调用的上下文切换

  • 概念拆分:

    • 特权级/栈切换(必然发生):用户态执行 SVC/SWI 后进入内核 vector_swi,在 ARM 上从用户栈切到 SP_svc,保存寄存器到 pt_regs,随后分发到 sys_*
    • 任务级上下文切换(条件发生):仅当系统调用出现阻塞、等待或被抢占时,才会调用 schedule() 切到其他任务;否则在当前任务内完成并快速返回用户态。
  • 触发任务级切换的常见情形:

    • I/O 等待(磁盘/网络/管道/套接字缓冲区满)。
    • 互斥量/等待队列(mutex_lock 可睡眠,wait_event* 挂起当前任务)。
    • 主动让出(sched_yield())。
    • 抢占(打开 CONFIG_PREEMPT 时在可抢占点被高优先级任务切走)。
  • 源码对应:

    • 入口与分发:arch/arm/kernel/entry-common.Svector_swisys_call_table)。
    • 典型阻塞点:fs/read_write.cvfs_write → 下层文件系统/驱动的 .write_iter/.write;在管道/套接字路径可能入等待队列后 schedule()
    • 追踪事件:trace/events/syscalls/sys_enter_writesys_exit_writesched/sched_switchsched/sched_wakeup

调度事件视角(时序图)

User-space Kernel Scheduler sys_enter_write (trace/events/syscalls) vector_swi → 解析 scno → 跳转 sys_write sys_exit_write (ret_fast_syscall) schedule() 请求调度 事件 sched_switch 切到其他任务 当前任务被唤醒(sched_wakeup) 继续 sys_write → vfs_write → 下层实现 sys_exit_write (ret_slow_syscall 可含信号/重启) alt [无阻塞/不可抢占] [阻塞/抢占发生] User-space Kernel Scheduler

阻塞路径(流程图)

flowchart TD A[sys_write(fd, buf, count)] --> B[vfs_write] B --> C{缓冲区可写?} C -->|Yes| D[提交数据 → 更新 f_pos] C -->|No| E[加入等待队列 (pipe/socket/filesystem)] E --> F[schedule() → 任务切换] F --> G[被唤醒 (sched_wakeup)] G --> C D --> H[sys_exit_write 返回用户态]

提示:阻塞/唤醒具体由管道/套接字或对应文件系统驱动实现;在 4.4 时代常见写入路径通过 file->f_op->write_iterwrite 进入,下层自行决定是否等待并调度。


兼容性与 ARM 私有调用

  • OABI 兼容:CONFIG_OABI_COMPAT 下,入口从 SWI 指令解析旧 ABI 号,并在 calls.SABI(...) 选择对应实现(如 sys_oabi_pread64)。
  • ARM 私有号:__ARM_NR_*(如 set_tlscacheflush)由入口通过号基数判断并分派到 ARM 特定处理路径。

追踪与性能

  • 追踪路径:入口在 _TIF_SYSCALL_WORK 下调用 syscall_trace_enter/exit,结合 trace/events/syscalls.* 提供系统调用事件,便于 ftrace/perf/trace-cmd 等工具分析。
  • 元数据:SYSCALL_METADATA 将签名与参数类型注册到 __syscalls_metadata 段,配合事件系统生成统一视图。

常见问题与调试建议

  • 号错/越界:用户态传错 __NR_* 或越界会跳到 sys_ni_syscall 返回 -ENOSYS
  • ABI 不匹配:EABI 程序在 r7 放号;旧 OABI 需 SWI 立即数。交叉编译/兼容层需确认 ABI。
  • 指针非法:copy_from_user() 返回 -EFAULT;优先检查用户缓冲区是否可访问并长度合法。
  • 追踪排错:使用 strace -e trace=write 或 ftrace 的 events/syscalls/* 观察系统调用入参与耗时。

参考源码位置索引(Linux 4.4.94)

  • 入口与表:
    • arch/arm/kernel/entry-common.Svector_swisys_call_table、追踪分支)
    • arch/arm/kernel/calls.S(系统调用表项映射)
    • arch/arm/kernel/entry-armv.S / entry-v7m.S(不同 ARM 变体向量)
  • 号码与私有号:
    • arch/arm/include/uapi/asm/unistd.h__NR_*__ARM_NR_*
    • arch/arm/include/asm/unistd.h__NR_syscalls 数量)
  • 参数访问与返回:
    • arch/arm/include/asm/syscall.h
  • 宏与元数据:
    • include/linux/syscalls.hSYSCALL_DEFINE*SyS* 包装与 metadata)
  • 案例实现:
    • fs/read_write.cSYSCALL_DEFINE3(write)vfs_write

总结

  • 在 ARM 平台,系统调用通过 SVC/SWI 异常进入 vector_swi,依据 ABI 从寄存器或指令读取系统调用号,随后查表分发到对应 sys_* 函数。
  • 参数经寄存器传递并在入口阶段统一封装与校验;返回值通过 r0 返回(错误为负值)。
  • SYSCALL_DEFINE* 宏体系提供一致的声明、包装与事件元数据,便于可观测性与维护。
  • write(2) 为例,调用在 VFS 层进一步路由到具体文件系统,实现用户态到内核态的受控数据写入。

进一步阅读

  • Linux Kernel Documentation 与 trace/events/syscalls.*
  • ftrace/perf/trace-cmd 用户指南
  • ARM 体系结构参考手册(异常向量与寄存器约定)

(本文档对应源码版本:Linux 4.4.94,路径参考工作区实际结构。)

相关推荐
大锦终3 小时前
【Linux】网络层与数据链路层中重点介绍
linux·运维·服务器·网络
lht6319356124 小时前
从Windows通过XRDP远程访问和控制银河麒麟 v10服务器
linux·运维·服务器·windows
qiudaorendao4 小时前
作业11.9
linux·服务器·apache
阿豪学编程4 小时前
环境变量与程序地址空间
linux·运维·windows
秃秃秃秃哇4 小时前
X5的相机同步方案
linux
CaracalTiger5 小时前
本地部署 Stable Diffusion3.5!cpolar让远程访问很简单!
java·linux·运维·开发语言·python·微信·stable diffusion
ai_xiaogui5 小时前
AIStarter跨平台完工:Win/Mac/Linux一键部署Stable Diffusion
linux·macos·stable diffusion·aistarter·kritaai·跨平台部署
梁萌6 小时前
linux中使用docker安装MySQL
linux·运维·docker·容器·mysql安装
搬砖的小码农_Sky7 小时前
Ubuntu Desktop Linux 文件和文件夹操作命令详解
linux·运维·ubuntu