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,路径参考工作区实际结构。)

相关推荐
DARLING Zero two♡8 分钟前
【Linux操作系统】简学深悟启示录:线程同步与互斥
linux·运维·服务器
hhwyqwqhhwy12 分钟前
linux 驱动iic
linux·运维·服务器
知识分享小能手17 分钟前
CentOS Stream 9入门学习教程,从入门到精通, CentOS Stream 9中的文件和目录管理(3)
linux·学习·centos
一只努力学习的Cat.18 分钟前
Linux:NAPT等其他补充内容
linux·运维·网络
摸鱼仙人~19 分钟前
VMware配置从开始踩坑总结-2025最新
linux·ubuntu
做咩啊~20 分钟前
CentOS 7部署OpenLDAP+phpLDAPadmin实现统一认证
linux·运维·centos
^乘风破浪^26 分钟前
Centos升级openssh及openssl
linux·运维·centos
满天星830357727 分钟前
【Linux】【进程间通信】管道
linux·运维·服务器
赖small强27 分钟前
【Linux驱动开发】Linux EXT4文件系统技术深度解析与实践指南
linux·驱动开发·ext4·superblock·super block·block bitmap·inode bitmap
linux修理工28 分钟前
CentOS Stream 9 软件仓库 清华源
linux·运维·centos