Linux 系统调用在 ARM 上的实现与工作机制
Linux 系统调用在 ARM 上的实现与工作机制
本文以 Linux 4.4.94 源码为依据,面向 ARM 架构系统调用(syscall)的实现原理、工作机制与调用路径进行系统化讲解,并结合真实源码片段与实际案例(write(2))帮助理解。最后提供流程图、时序图与关键源码位置索引,形成可独立阅读的参考文档。
内容概览
- 系统调用的角色与边界
- ARM 用户态→内核态:SVC/SWI 陷入机制与 ABI 差异
- 入口实现:
vector_swi、系统调用表与分发 - 参数与返回值传递规则(寄存器约定)
SYSCALL_DEFINE*宏、SyS包装与元数据- 用户内存访问与安全(
uaccess、copy_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读取指令编码)。
- EABI(现代 ARM Linux 用户态)使用寄存器
- 内核入口的通用流程:保存现场 → 获取系统调用号
scno→ 禁用用户访问窗口 → 取sys_call_table→ 越界/追踪检测 → 分发到sys_*函数 → 返回路径(快速或慢速)。
关键入口源码(节选)
文件:arch/arm/kernel/entry-common.S
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_BITaddne scno, r7, #__NR_SYSCALL_BASEldreq scno, [lr, #-4]
#else/* Legacy ABI:直接读指令 */ldr scno, [lr, #-4]
#endifuaccess_disable tbladr tbl, sys_call_table @ 取系统调用表地址local_restart:ldr r10, [tsk, #TI_FLAGS] @ 检查追踪标志tst r10, #_TIF_SYSCALL_WORKbne __sys_tracecmp 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
/* 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
#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_tls、cacheflush),入口根据号基数分支到相应处理流程。
参数与返回值传递(寄存器约定)
- 传参寄存器:ARM 最多使用
r0–r6传递参数;EABI 用r7传递系统调用号。 - 返回值:
r0用作返回值/错误码(负值表示错误)。 - 获取与设置接口:
arch/arm/include/asm/syscall.h提供统一访问封装。
/* 读取系统调用号与参数 */
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, ®s->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 使用)。
#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_write与SyS_write等包装,并注入可追踪的元数据。
用户内存访问与安全
uaccess_disable/uaccess_enable:在入口阶段根据需要禁用/启用用户访问窗口,避免错误的用户态访问。copy_from_user/copy_to_user:显式拷贝并检查用户态指针合法性(access_ok)。EFAULT/EPERM等错误码:在参数验证或权限检查失败时返回负值错误码。
示例位置:fs/read_write.c 的 rw_copy_check_uvector() 使用 copy_from_user() 批量检入 iovec。
案例解析:write(2) 完整路径
用户态
/* 用户态伪代码 */
ssize_t n = write(fd, buf, count); // r7=scno=__NR_write, r0..r2 传参
内核分发与实现(ARM 通用入口 → VFS 层)
文件:fs/read_write.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()释放引用。
端到端时序图
入口分支流程(EABI/OABI)
flowchart TDA{EABI?} -->|Yes| B[r7 -> scno]A -->|Legacy/OABI| C[读 SWI 立即数 (lr-4) -> scno]B --> D{scno < NR_syscalls?}C --> DD -->|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.S(vector_swi→sys_call_table)。 - 典型阻塞点:
fs/read_write.c→vfs_write→ 下层文件系统/驱动的.write_iter/.write;在管道/套接字路径可能入等待队列后schedule()。 - 追踪事件:
trace/events/syscalls/sys_enter_write、sys_exit_write、sched/sched_switch、sched/sched_wakeup。
- 入口与分发:
调度事件视角(时序图)
阻塞路径(流程图)
flowchart TDA[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 --> CD --> H[sys_exit_write 返回用户态]
提示:阻塞/唤醒具体由管道/套接字或对应文件系统驱动实现;在 4.4 时代常见写入路径通过 file->f_op->write_iter 或 write 进入,下层自行决定是否等待并调度。
兼容性与 ARM 私有调用
- OABI 兼容:
CONFIG_OABI_COMPAT下,入口从 SWI 指令解析旧 ABI 号,并在calls.S用ABI(...)选择对应实现(如sys_oabi_pread64)。 - ARM 私有号:
__ARM_NR_*(如set_tls、cacheflush)由入口通过号基数判断并分派到 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.S(vector_swi、sys_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.h(SYSCALL_DEFINE*、SyS*包装与 metadata)
- 案例实现:
fs/read_write.c(SYSCALL_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,路径参考工作区实际结构。)
