针对ARM64架构的系统调用挂钩新方法:SVC-Hook
本报告基于Akira Moroo、Hajime Tazaki与Kenichi Yasukata三位研究者发表于2025年12月(在线发表日期为2025年12月14日)的论文,该论文收录于第26届ACM International Middleware Conference (Middleware ‘25)的会议论文集。三位作者均来自日本:Akira Moroo隶属于Ricerca Security, Inc.,而Hajime Tazaki与Kenichi Yasukata均来自IIJ Research Laboratory。他们的工作题为“svc-hook: hooking system calls on arm64 by binary rewriting”,提出并实现了一种针对ARM64 CPU架构、基于二进制重写(binary rewriting)的新型系统调用挂钩(system call hook)机制。
一、研究背景与目标
系统调用是用户空间程序访问操作系统(OS)内核功能的核心接口。系统调用挂钩机制允许用户在系统调用被触发时拦截执行流,并将其重定向到用户自定义的处理函数中。这类机制是众多软件(如追踪工具、沙箱、操作系统仿真层)的基础组件。近年来,出于性能与安全性的考量,将操作系统子系统置于用户空间的设计趋势日益显著,这使得通过系统调用接口透明地“粘合”用户空间OS子系统与现有程序的需求变得尤为迫切,从而对高效、可靠的系统调用挂钩机制提出了更高要求。
在诸多挂钩方法中,基于二进制重写的方案因其一系列优势而备受关注。相较于依赖内核通用功能(如ptrace、seccomp)或BPF的方案,二进制重写通常能带来更低的性能开销,且能用于系统调用仿真,这对于用户空间OS子系统的透明集成至关重要。此外,它不依赖于对内核或标准库的非上游(non-upstreamed)修改,更易于用户接受和使用。
然而,现有研究存在显著的架构局限性。绝大多数基于二进制重写的系统调用挂钩机制都是为x86 CPU架构设计的。虽然已有少数针对ARM64架构的提案,但它们存在明显的缺陷,可能导致被挂钩程序出现预期外的行为。例如,Hermitux for ARM64方案结合了多种技术,但仍有约10%的系统调用无法通过二进制重写进行挂钩,必须依赖高开销的陷阱(trap)机制;其部分技术接受覆盖关键寄存器(如x30),可能引发程序逻辑错误。另一项工作ASC-Hook通过修改用于传递系统调用号的寄存器(x8)来规避问题,但这同样改变了程序预期的CPU寄存器状态,存在引发意外行为的风险。因此,尽管ARM64是一种极为流行的CPU架构,但其软件生态一直缺乏能够同时满足低性能开销、支持系统调用仿真、易于使用和指令级挂钩这些理想属性的挂钩机制。这一缺口尤其阻碍了用户空间OS子系统设计者将其系统集成到ARM64平台软件中。
为解决上述问题,本研究旨在提出一种名为SVC-Hook的新型系统调用挂钩机制。其技术目标是:在ARM64架构上,通过二进制重写挂钩系统调用,且不因重写操作而破坏被挂钩程序的应用程序逻辑。为此,SVC-Hook设定了两个核心要求:1) 仅替换目标程序中的svc指令,不覆盖其他指令;2) 在挂钩函数调用前后,被挂钩程序能维持除系统调用调用约定(calling convention)允许更改之外的通用CPU寄存器状态不变。通过满足这两点,SVC-Hook有望规避前述Hermitux和ASC-Hook方案因改变寄存器状态而带来的风险。
二、技术方案与工作流程
SVC-Hook的核心设计围绕解决ARM64架构特有的一个挑战展开。该挑战源于bl指令的设计:bl在跳转时会自动将返回地址保存到x30寄存器。如果简单地将触发系统调用的svc指令替换为bl指令以跳转到挂钩函数,那么原始函数原本保存在x30中的返回地址将被覆盖,导致其无法正确返回。
SVC-Hook的解决方案新颖而精巧,其工作流程主要包括初始设置阶段和运行时执行流两个部分,具体步骤如下:
初始设置阶段:
libsvchook.so的共享库形式存在。该库通过LD_PRELOAD机制在被挂钩程序主逻辑开始前加载并执行。首先,它扫描程序的所有可执行内存区域。由于ARM64采用4字节定长指令集且指令必须4字节对齐,识别svc指令无需复杂的反汇编,只需逐4字节检查操作码即可。所有找到的svc指令的地址被记录在一个列表中。svc指令,SVC-Hook需要为其创建一段蹦床代码。这是其创新关键。SVC-Hook选择将svc指令替换为b指令(跳转指令)。b指令不会保存返回地址到任何寄存器,从而避免了覆盖x30的问题。但随之而来的问题是:跳转后如何知道应该返回到哪里?SVC-Hook的答案是:将返回地址(即svc指令地址+4)作为常量数据,直接嵌入到为每个svc独立实例化的蹦床代码中。蹦床代码需要放置在距离原svc指令±128 MiB的地址范围内(这是b指令26位立即数偏移量经移位后所能覆盖的范围)。libsvchook.so会为每个svc在可用的内存区域中找到合适位置来放置其专属的蹦床代码。研究通过对Ubuntu软件仓库大量包的分析表明,绝大多数svc指令附近都能找到可用空间,此限制在实践中影响甚微。libsvchook.so遍历svc列表,将每个svc指令替换为一条b指令,该指令的跳转目标指向对应的专属蹦床代码地址。此过程通过mprotect系统调用临时将代码所在内存区域设为可写,完成替换后再恢复其原始权限。libsvchook.so会通过环境变量指定的路径,使用dlmopen加载一个用户提供的共享库,该库中包含用户实现的实际挂钩逻辑函数。运行时执行流程: 当程序运行到被替换的b指令时,执行流如下(以挂钩一个svc为例):
b指令,跳转到为该svc定制的蹦床代码处。此时x30寄存器未被改动。libsvchook.so提供的中间函数,该函数最终会调用用户实现的挂钩函数。svc指令后一条指令的地址)。至此,程序继续正常执行,仿佛系统调用刚刚完成(或被仿真),且整个过程中x30及其他受调用约定保护的寄存器状态均得以保持。三、主要实验结果
研究者通过实验评估了SVC-Hook的性能开销及其在实际应用场景中的表现。
挂钩开销评估: 实验测量了挂钩并仿真最简单的getpid系统调用所需的时间。对比机制包括ptrace、seccomp、基于brk指令的信号处理、SVC-Hook以及作为参考的LD_PRELOAD函数调用挂钩。结果如表2所示:
LD_PRELOAD高约17纳秒,这正体现了执行蹦床代码(保存/恢复寄存器、跳转)所带来的额外成本。尽管如此,与基于内核机制的方案相比,SVC-Hook的开销低了两个数量级以上,证明了其极高的效率。挂钩程序性能评估: 研究选取了“透明集成用户空间OS子系统”这一关键应用场景进行测试。实验将运行于DPDK之上的LWIP TCP/IP栈(用户空间网络栈)透明地集成到一个简单的HTTP服务器和Redis键值存储服务器中。系统调用被挂钩并重定向到用户空间的LWIP。测试平台为两台直接连接的10Gbps以太网机器。
LD_PRELOAD方案(后者在此场景下也可用,但如前文所述,它在通用性和指令级挂钩方面有局限)。四、结论与价值
本研究成功设计并实现了SVC-Hook,一种针对ARM64架构、基于二进制重写的系统调用挂钩机制。其核心贡献在于提出并实现了一种通过b指令跳转与每指令专属蹦床代码相结合的方法,在完全不破坏被挂钩程序寄存器状态(特别是x30)的前提下,实现了高效、可靠的指令级系统调用拦截。
该研究的科学价值与应用价值体现在: * 填补技术空白:为日益流行的ARM64平台提供了一个同时满足低开销、支持仿真、易于使用、指令级挂钩四大属性的系统调用挂钩方案,解决了该领域长期存在的选项有限问题。 * 推动用户空间OS发展:通过极低的性能开销,使得透明集成高性能用户空间OS子系统(如用户空间网络栈)成为可能,且能保持其性能优势,为未来系统软件设计提供了有力工具。 * 方法创新:其“替换为b指令+嵌入返回地址的专属蹦床”设计简洁而有效,规避了前人方案中改变寄存器状态可能导致的副作用,提升了挂钩的可靠性与安全性。
五、研究亮点
bl指令覆盖x30的固有挑战,创造性地采用b指令与定制化蹦床代码组合的方案,在实现跳转的同时完美保存了完整的寄存器上下文,这是本工作最重要的技术亮点。六、其他说明
论文还讨论了SVC-Hook的局限性,例如在那些通过运行时校验或严格内存权限限制来阻止二进制重写的系统(如OpenBSD、macOS)上可能无法应用。同时,作者指出其方法直接移植到x86或RISC-V架构存在困难,主要源于指令长度和跳转偏移量范围的差异。这些坦诚的讨论有助于读者全面理解该技术的适用范围。
SVC-Hook是一项针对实际需求、设计精巧、实验充分、结果出色的系统软件研究工作,为ARM64平台的系统级编程和性能优化提供了一个宝贵的新工具。