2026 年 11 月 2 日,开发者 guitmz 在 Hacker News 发布了重构后的 Linux 系统调用检索平台与接口服务 syscall.sh,一次性收录了从 Linux 3.3 到 7.3、横跨 27 种架构与 ABI 的全量系统调用表。与过去散落在内核源码树或个人博客里的静态网页不同,该项目把调用号、参数签名、触发指令与寄存器分配规则全部转化为可直接下载的 /<arch>/syscalls.json 静态文件,并提供了支持跨版本比对的 /v2/ REST API。表面看这只是一次开发工具升级,但把 27 张表放在同一时间轴上比对,实质上是给 Linux 内核背负了三十年的二进制历史包袱做了一次断层扫描。
为什么 32 位老架构比 64 位新架构多出上百个系统调用
翻开 syscall.sh 的首页统计,一个反常的数字对比立刻跳出来。在 32 位 x86(i386)架构下,系统调用总数高达 441 个,Motorola 68000(m68k)有 435 个,32 位 ARM 也有 426 个;而到了现代 64 位架构,x86_64 降到了 369 个,AArch64(arm64)只有 326 个,RISC-V 64 位是 327 个,2022 年才合入主线 5.19 的龙芯 LoongArch64 更是只有 323 个。
这种老架构臃肿、新架构精简的倒挂现象,根源在于 Linux 内核恪守的不破坏用户空间(Never break userspace)铁律。系统调用表是内核与用户态程序之间最底层的二进制契约,一旦某个调用号在主线版本中分配出去,即便后来设计了更合理的替代接口,旧接口也几乎不能删除。
早期架构在进入主线时各自维护独立的系统调用表。当年为了解决 32 位系统上文件大小超过 2GB、用户 ID 超过 65535 或 2038 年时间戳溢出等问题,内核不得不在 stat、chown、mmap 等老调用之外,成批追加带 64 或 32 后缀的修补版系统调用。这些历史接口在 i386 和 m68k 的表里层层堆叠。而 arm64、riscv64 与 loongarch64 等晚近加入的架构,直接强制采用内核统一维护的通用系统调用表(generic syscall table),从诞生第一天起就只保留现代 64 位接口与 openat 系列扩展,甩掉了上百个历史冗余包袱。
在 Hacker News 的讨论中,开发者 PhilipRoman 提到,不同统计口径下各架构的调用数量常有微小出入,这取决于生成器如何处理 syscall.tbl 中的关联条目及底层语义差异。syscall.sh 此次除了加入内核版本选择器与移动端适配,还把 Linux man-pages 项目和 liburing 的功能简述整合进表格,试图解决评论区诟病的传统文档滞后于新接口的问题。至于社区提出增加同架构相似调用推荐的想法,作者 guitmz 认为仅靠函数名匹配很容易误导,后续需要更严谨的语义归类。
静态扫宏与编译镜像:三大查询工具的路线分野
对于写系统底层代码的工程师来说,查系统调用表并不是一件新鲜事,但不同工具在底层数据生成机制上的取舍,直接决定了它们各自的适用边界与盲区。
syscall.sh 走的是源码静态解析路线。它的生成器遍历每个主线内核版本的源码树,直接读取各架构的调用表文件并扫描 SYSCALL_DEFINE 宏来提取调用号与参数签名。这种做法的优势是快且全,能轻松拉出横跨十余年的版本差集(diff),把每个调用首次出现的版本(since)和极少数被移除的版本(removed_in)记录在案。
放在同赛道里横向对比,老牌的 Marcin Juszkiewicz 系统调用表(其 2026 年 10 月 7 日生成的快照已跟进至 Linux 7.3.0-rc1)提供了多达 47 列的跨架构对比大矩阵,并配套了 Python 模块 system_calls,但它只管调用号映射,不提供汇编级的寄存器调用约定。另一款硬核工具 syscalls.mebeim.net 则基于 Systrack 构建,它不信静态文本扫描,而是实打实地编译出 21 种架构/ABI 的 vmlinux 内核镜像再做二进制分析,因此能精准给出每个系统调用真正落地的内核符号、源码行号以及生效所需的 Kconfig 编译选项。
看懂这三条路线的差异,就能避开使用 syscall.sh 时的第一个认知坑。在 syscall.sh 的数据里,如果某个系统调用的 since 字段恰好等于该架构最早被扫描的内核版本——比如 x64 标为 3.3、arm 标为 4.10,或者 ppc64、mips64、sparc64 统一标为 5.0——这并不代表该接口是在那个版本才发明的,仅仅是因为扫描器从那一版才开始抓取该架构的数据。拿这个字段去判断老旧嵌入式内核的接口兼容性,很容易得出错误结论。
查表能省掉翻源码的功夫,但填不平底层 ABI 的暗礁
把分散在内核各处的 .tbl 文件变成一行 curl 就能拉取的 JSON,最直接的受益者是三类人:编写跨架构 Shellcode 的安全研究员与 CTF 选手、维护 Go/Rust/Zig 等不依赖 libc 直接发起系统调用的编译器运行时团队,以及编写容器安全规则的云原生工程师。
在云原生与容器安全场景下,工程师需要为不同内核版本的节点生成 Seccomp 沙箱白名单,或者在 eBPF 探针里挂载系统调用追踪点。过去为了确认某个新引入的 io_uring 相关调用或文件操作接口在目标机器的内核版本上是否存在、对应几号,往往得去翻对应版本的内核头文件;现在通过 CI/CD 流水线直接调取 /v2/ 的版本差集接口,就能自动化生成跨版本、跨架构的过滤策略。
速查表能给全寄存器名字,却给不出架构私藏的对齐与压栈潜规则。
但对于真正要手写汇编或开发编译器后端的工程师,单看 syscall.sh 首页那张整齐的 ARG0 到 ARG5 寄存器速查表,依然会在运行时踩中底层 ABI 的暗礁。
| 架构 / ABI | 触发指令 | 调用号寄存器 | 潜藏的 ABI 边界与传参特殊规则 |
|---|---|---|---|
| x64 | syscall | rax | 内核会破坏 rcx 与 r11 寄存器,第 4 参数用 r10 而非 C 约定的 rcx |
| x32 | syscall | rax | 复用 64 位寄存器,但调用号必须额外或上 __X32_SYSCALL_BIT 掩码 |
| arm (32位 EABI) | svc #0 | r7 | 传递 64 位参数时需拆成两个寄存器并满足偶数寄存器对齐,导致后续参数顺延 |
| mipso32 | syscall | v0 | a0-a3 只够放前 4 个参数,第 5、第 6 参数必须压入用户栈 sp+16 与 sp+20 |
正如 Linux 官方手册 syscall(2) 所警告的那样,二维表格装不下各架构古怪的边界条件。在 MIPS o32 架构下,寄存器只管前四个参数,第五和第六个参数必须由调用方提前塞进用户栈的 sp+16 和 sp+20 偏移处;在 32 位 ARM EABI 下,遇到 64 位宽度的参数(如 loff_t 文件偏移量),必须拆分放进一对连续寄存器且要求偶数号对齐,这会直接挤占掉后面参数的寄存器槽位;而在 x86_64 上跑 32 位指针的 x32 ABI,虽然同样列着 369 个调用,发指令前却必须给调用号加上 __X32_SYSCALL_BIT 掩码。此外,不同架构用单独标志位还是负数区间来判定错误返回,以及内核返回时会覆盖破坏(clobber)哪些临时寄存器,都无法在精简的 JSON 字段里一览无余。
- 提醒.自动化安全工具可以放心直接消费 syscall.sh 的 JSON 接口来维护调用号映射,但涉及跨架构汇编生成与二进制插桩时,仍需把
syscall(2)手册与具体内核的 Kconfig 裁剪情况作为兜底校验。
