news 2026/8/22 15:22:47

substitute 运行时 Hook 库核心原理深度解析:transform-dis 如何改写机器码生成 trampoline

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
substitute 运行时 Hook 库核心原理深度解析:transform-dis 如何改写机器码生成 trampoline

substitute 运行时 Hook 库核心原理深度解析:transform-dis 如何改写机器码生成 trampoline

【免费下载链接】substituteA free runtime modification library.项目地址: https://gitcode.com/gh_mirrors/su/substitute

substitute 是一款免费的运行时修改(runtime hook)库,其核心模块 transform-dis 负责逐条反汇编目标函数、改写机器码,并自动生成 trampoline(跳板码),让你既能拦截函数,又能安全地调用原实现。本文将用尽量少的代码,带你完整看懂这条改写流水线是如何工作的。

一、substitute 是什么?能做什么?

substitute 是一个面向 Darwin(macOS / iOS)的开源函数 Hook 库,对外提供三类能力:

  • 函数头改写substitute_hook_functions直接修改目标函数开头的机器码,把入口重定向到你的替换函数;
  • 导入表替换substitute_interpose_imports修改 GOT/PLT 条目,无需写可执行代码即可拦截导出函数;
  • Objective-C 方法替换substitute_hook_objc_message基于class_replaceMethod的线程安全方法 Hook。

本文聚焦最核心、也最烧脑的第一种:函数头改写(inline hook)

二、为什么必须改写机器码、又必须生成 trampoline?

Hook 一个函数的最直接做法是:把它开头的若干字节改成一条"跳转到你的替换函数"的跳转指令。但这带来两个难题:

  1. 被覆盖的指令去哪了?函数开头被覆盖的那些原始指令,仍然需要可执行——比如你通过old_ptr拿到的"原函数指针",或者被 Hook 的函数内部逻辑。substitute 的解法是把这些指令原样(或改写后)复制到一块新的可执行内存中,拼成所谓的outro trampoline(尾跳板):

    尾跳板 = 被覆盖指令的改写副本 + 一条跳回原函数"剩余部分"的跳转。

  2. 跳转距离可能不够。ARM 的条件分支只有 ±128MB 左右范围,x86 的相对跳转更短。如果替换函数离得太远,substitute 会先分配一块靠近目标的内存写入长跳转(make_jump_patch),函数入口先跳到这个intro trampoline(头跳板),再由它跳到最终目标。

因此一次完整的 Hook =头跳板(可选)+ 函数入口短跳转 + 尾跳板(改写指令 + 回跳),其中尾跳板正是 transform-dis 的产物。

三、整体流水线:从 API 调用到落盘

核心调度逻辑在lib/hook-functions.csubstitute_hook_functions中,整体分为两个阶段:

阶段 1:预检与生成(不碰原代码)

  1. 计算需要的覆盖字节数check_intro_trampoline根据"替换函数距离"和架构(ARM 8 或 10 字节,ARM64 8/12/16 字节,见lib/arm/jump-patch.hlib/arm64/jump-patch.h)决定 patch 区域大小;
  2. 生成 intro trampoline:若需要远跳转,先写入头跳板;
  3. 生成函数入口的跳转补丁make_jump_patch);
  4. 调用transform_dis_main生成 outro trampoline——本文主角,见下节;
  5. 调用jump_dis_main做安全性启发式扫描,检查函数后文是否有跳回 patch 区域的跳转(见第五节)。

阶段 2:原子提交

  • 把跳板内存页"密封"(seal),禁止继续修改;
  • 暂停其他线程,把跳转补丁写入原函数入口,同时校正正好停在被覆盖区域内的线程 PC(execmem_foreign_write_with_pc_patch)。

这种"先全部预演、再一次性提交"的设计,保证任何一步失败都不会破坏目标函数。

四、transform-dis:机器码改写的核心

4.1 通用骨架:一条指令一条指令地走

入口是lib/transform-dis.c中的transform_dis_main,它的工作循环非常朴素:

  1. 从 patch 区域起点开始,逐条反汇编指令;
  2. 判断指令类型,分两种处理:
    • 普通指令:原样复制到跳板区;
    • 含 PC 相关地址的指令:复制到跳板后地址就错了,必须重新编码
  3. 每复制一条,就在offset_by_pcdiff偏移表中记录"原始偏移 → 跳板内偏移"的映射,供后续修正被覆盖区域内的 PC 修正逻辑使用;
  4. 遇到错误(如 patch 区域中间出现ret)立即返回对应错误码。

架构相关的"怎么解码、怎么改写"则由lib/<arch>/arch-transform-dis.inc.hlib/<arch>/dis-main.inc.h提供,通过回调函数接入通用骨架。ARM 的反汇编还会按最低位自动分发到 Thumb / Thumb-2 / ARM 三种模式(见lib/arm/dis-main.inc.h)。

4.2 三类"必须改写"的指令

以 ARM 为例(lib/arm/arch-transform-dis.inc.h),transform-dis 需要处理三类 PC 相关指令:

指令类型例子改写策略
数据运算中使用 PC 作为操作数adr r3, Xldr pc, [pc, r0, lsl #2]分析寄存器读写关系,分 4 种情况处理:直接重编码、借用空闲寄存器、或借用栈保存临时值(PUSH/POP + MOVW/MOVT
PC 相对寻址adr、字面量池ldr、PC 相对跳转把相对地址换算为绝对地址,改用MOVW/MOVT载入,必要时重写加载模式
分支/调用bbl、Thumbcbz条件分支先补一条反向条件跳转保持语义,再用MOVW/MOVT+BLX r14实现远跳转

其中 ThumbIT 块(指令谓词条件)处理得尤为精细:transform_dis_pre_dis/transform_dis_post_dis会把一个 IT 块拆解为"每条指令各自一条条件跳转"的形式,因此force_keep_transforming标志会让主循环强制继续消费整个 IT 块,避免在块中间截断。

此外还有一个TRANSFORM_DIS_BAN_CALLS选项(线程安全模式下默认开启):如果 patch 区域中间出现函数调用,就放弃 Hook(返回SUBSTITUTE_ERR_FUNC_CALLS_AT_START)。原因是调用产生的返回地址可能正停留在其他线程的栈上,落在被覆盖区域里就无法安全修正了。

4.3 trampoline 的拼装细节

回到hook-functions.ctransform_dis_main返回后:

  • pc_patch_end会被向后扩展到完整的指令边界(甚至整个 IT 块),确保覆盖区域不会"咬断"一条指令;
  • 在跳板尾部再写入一条跳回pc_patch_endmake_jump_patch,outro trampoline 就此完成;
  • hook->old_ptr指向的就是这个尾跳板入口——调用"原函数"时,实际执行的是"改写副本 + 原函数剩余部分"的无缝拼接。

offset_by_pcdiff偏移表则负责最后一块拼图:原函数中凡是引用被覆盖区域内部位置的 PC 相对指令,都可以按表把地址修正到跳板中的对应副本位置。

五、jump-dis:动手之前的安全检查

生成跳板后,lib/jump-dis.cjump_dis_main还会做一次启发式跳转分析:从函数开头向后再走最多 512 条指令,追踪所有分支目标,一旦发现某条指令会跳回 patch 区域内部(SUBSTITUTE_ERR_FUNC_JUMPS_TO_START),就放弃本次 Hook。

这个检查是"尽力而为"的——它不处理跳转表,遇到ret之后的代码也不再深入——但足以挡住绝大多数会把程序执行流带进已被覆盖区域、从而崩溃的情况。

六、常见错误码速查

substitute_hook_functions的失败原因一目了然,定义在lib/substitute.h

错误码含义
SUBSTITUTE_ERR_FUNC_TOO_SHORTpatch 区域中间就出现了ret,函数太短
SUBSTITUTE_ERR_FUNC_BAD_INSN_AT_START开头的指令属于暂不支持的特殊情况(如跳回本区域)
SUBSTITUTE_ERR_FUNC_CALLS_AT_START线程安全模式下,开头存在调用指令
SUBSTITUTE_ERR_FUNC_JUMPS_TO_STARTjump-dis 发现函数后文跳回被覆盖区域
SUBSTITUTE_ERR_OUT_OF_RANGE目标过远且拿不到足够近的跳板内存
SUBSTITUTE_ERR_VMmmap / mprotect / vm_remap 失败(如内核禁止页面可执行)

想在自己的环境验证这些行为,test/test-transform-dis.ctest/test-jump-dis.ctest/test-pc-patch.c以及各架构的test/transform-dis-cases-*.S用例是很好的起点。

七、关键源码导读路径

  • 对外 API 与文档注释:lib/substitute.h
  • Hook 主流程(预检 → 生成 → 原子提交):lib/hook-functions.c
  • 机器码改写核心循环:lib/transform-dis.c(接口在lib/transform-dis.h
  • 通用反汇编分发层:lib/dis.hlib/darwin/各架构dis-main.inc.h
  • ARM 的 PC 相关指令改写策略:lib/arm/arch-transform-dis.inc.h
  • 各架构跳转补丁(决定 patch 大小):lib/arm/jump-patch.hlib/arm64/jump-patch.hlib/x86/jump-patch.h
  • 跳转回溯安全检查:lib/jump-dis.c
  • 内存写入与线程 PC 修正:lib/darwin/execmem.c

八、总结

substitute 的巧妙之处在于把"改机器码"这件高风险的事拆成了清晰的四层:通用反汇编分发层保证多架构可移植,transform-dis 改写层逐条修正 PC 相关指令并顺带产出 trampoline,jump-dis 检查层用启发式分析拦截危险目标,execmem 提交层用暂停线程 + PC 校正保证原子性。理解了这条流水线,你就理解了绝大多数 inline hook 库的通用设计,也为读懂各类动态插桩工具打下了坚实基础。

【免费下载链接】substituteA free runtime modification library.项目地址: https://gitcode.com/gh_mirrors/su/substitute

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/22 15:17:11

ComfyUI-KJNodes 使用指南:5 步搞定模型编译与图像批处理节点

ComfyUI-KJNodes 使用指南&#xff1a;5 步搞定模型编译与图像批处理节点 【免费下载链接】ComfyUI-KJNodes Various custom nodes for ComfyUI 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-KJNodes ComfyUI-KJNodes 是给 ComfyUI 扩展用处的自定义节点合集&a…

作者头像 李华
网站建设 2026/8/22 15:15:09

5分钟跑通:res-downloader 跨平台资源下载工具上手指南

5分钟跑通&#xff1a;res-downloader 跨平台资源下载工具上手指南 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader res-downl…

作者头像 李华
网站建设 2026/8/22 15:11:53

网易云 NCM 转 MP3 三分钟搞定:ncmdump 拖一下就能打开保险柜

网易云 NCM 转 MP3 三分钟搞定&#xff1a;ncmdump 拖一下就能打开保险柜 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 把网易云下载的歌拷进车载 U 盘&#xff0c;插上车却全是"无法识别"&#xff1f;别慌&#xff0c;…

作者头像 李华