问题背景

在大型 C++ 和 Android 工程中,尤其是架构历史包袱沉重、代码混乱的项目里,开发者经常遇到这样的需求:

我需要知道某个关键路径函数被谁调用了、参数是什么、返回值是什么、调用链路是怎样的——但我不能(或不想)修改原始源代码。

这个需求在调试、性能分析、安全审计、线上监控等场景中非常普遍。本文将系统性地调研所有可行的技术方案。

方案全景

根据侵入程度和技术原理,我们将所有方案分为 5 大类

  1. Hook / 函数拦截(PLT Hook、Inline Hook、LD_PRELOAD)
  2. 动态二进制插桩 DBI(Frida、Pin、DynamoRIO)
  3. 编译器插桩(LLVM XRay、-finstrument-functions)
  4. 内核级追踪(eBPF、bpftrace、BCC)
  5. Android 专属方案(ByteHook、ShadowHook)

一、PLT Hook(GOT 表替换)

原理

ELF 文件的 PLT(Procedure Linkage Table)+ GOT(Global Offset Table)机制允许在运行时替换 GOT 表条目,使该符号的调用跳转到自定义函数。仅对外部动态链接的函数有效。

适用场景

拦截目标库对 libc、libm 或其他 .so 的外部调用(如 openwritemallocsend/recv)。

主流工具

工具 平台 Star 特点
xHook(爱奇艺) Android 3.5k+ 纯 PLT Hook,稳定高效,支持 ARM/ARM64/x86
ByteHook(字节跳动) Android 2k+ xHook 增强版,支持延迟 Hook、单次 Hook、多线程安全,Maven 可用
PLTHook Linux 轻量 C 库,适用于 Linux 服务器端程序

优点

  • 实现简单,稳定性高(不修改代码段)
  • 可精确指定"哪个模块对哪个符号的调用"
  • 性能开销极低

缺点

  • ⚠️ 无法拦截静态链接或模块内部调用
  • 无法拦截通过函数指针直接使用的情况

代码示例(xHook)

#include "xhook.h"

static int (*orig_open)(const char *, int, ...);

int my_open(const char *pathname, int flags, ...) {
    LOG("open(%s, %d)", pathname, flags);
    return orig_open(pathname, flags);
}

// 注册 Hook
xhook_register("lib/*.so", "open", (void *)my_open, (void **)&orig_open);
xhook_refresh(0);

二、Inline Hook(指令重写)

原理

直接修改目标函数入口处的机器码,插入一条跳转指令(JMP)跳到 Hook 函数,Hook 函数执行后跳回原函数。不受 GOT/PLT 限制,可以 Hook 几乎任何函数。

适用场景

需要拦截模块内部函数、非导出函数、或静态链接的函数。

主流工具

工具 平台 Star 特点
ShadowHook(字节跳动) Android 1.5k+ 支持 Thumb/ARM32/ARM64,inline hook,Maven 可用
Dobby 全平台 3.5k+ 支持 ARM/x86/MIPS,跨平台 inline hook 框架
Microsoft Detours Windows 5k+ 微软出品,Windows 平台工业标准
MinHook Windows 4k+ 轻量、API 简洁、x86/x64
SubHook Linux/Windows 500+ 极简 inline hook,x86/x64
funchook Linux/Windows/macOS 1k+ 基于 diStorm3 反汇编,跨平台

优点

  • 无 PLT 限制,可 Hook 几乎任意函数
  • 灵活度最高

缺点

  • ⚠️ 修改代码段(.text),需要 mprotect 或特殊权限
  • ⚠️ 需要处理指令边界,架构相关
  • ⚠️ 多线程安全性复杂
  • ⚠️ 代码混淆/加壳会失败

代码示例(Dobby)

#include "dobby.h"

static int (*orig_func)(int arg1, const char *arg2);

int hook_func(int arg1, const char *arg2) {
    LOG("hook_func(%d, %s)", arg1, arg2);
    return orig_func(arg1, arg2);
}

DobbyHook(
    (void *)target_address,
    (dobby_dummy_func_t)hook_func,
    (dobby_dummy_func_t *)&orig_func
);

三、LD_PRELOAD(共享库预加载)

原理

利用 Linux 动态链接器的 LD_PRELOAD 环境变量,在程序启动时优先加载自定义 .so 文件,覆盖与目标同名的外部符号。

优点

  • 绝对零侵入——不修改任何代码、不修改二进制、不修改构建流程
  • 配置级操作,启动程序时设置环境变量即可
  • 适合 Docker/容器化部署

缺点

  • ⚠️ 仅对动态链接的外部符号有效
  • 需要重启程序生效
  • 静态链接的函数无法拦截

代码示例

// myhook.c — 编译为 libmyhook.so
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

ssize_t write(int fd, const void *buf, size_t count) {
    ssize_t (*orig_write)(int, const void *, size_t) = dlsym(RTLD_NEXT, "write");
    printf("[HOOK] write(%d, %p, %zu)\n", fd, buf, count);
    return orig_write(fd, buf, count);
}
gcc -shared -fPIC myhook.c -o libmyhook.so -ldl
LD_PRELOAD=./libmyhook.so ./target_program

四、动态二进制插桩(DBI)

Frida

原理: 通过 ptrace 注入 Agent 到目标进程,在进程内部运行 JavaScript/V8 引擎,动态 Hook 任何函数。

关键能力:

  • Interceptor.attach() — Hook C/C++ Native 函数,读取/修改参数和返回值
  • frida-trace — 一行命令即可 trace 指定函数
  • 支持非导出函数(通过基地址+偏移)
// Frida 脚本
Interceptor.attach(Module.findExportByName("libnative.so", "ProcessData"), {
    onEnter(args) {
        console.log(`ProcessData(buf=${args[0]}, len=${args[1].toInt32()})`);
        var buf = Memory.readByteArray(args[0], args[1].toInt32());
        console.log(hexdump(buf, { length: 64 }));
    },
    onLeave(retval) {
        console.log(`ProcessData → ${retval.toInt32()}`);
    }
});
# 命令行 trace
frida-trace -U -i "open" -i "ProcessData" com.example.app

适合场景: Android 逆向调试、安全审计、开发阶段快速验证。不适合生产环境长期部署。

Intel Pin / DynamoRIO

JIT 编译器框架,运行时翻译目标二进制代码并插入插桩。粒度可精确到指令级,但性能开销大(10x-100x 减速),适合离线分析而非实时日志。


五、编译器插桩

LLVM XRay

原理: LLVM 内置的函数调用追踪系统,编译时通过 -fxray-instrument 向每个函数入口/出口插入 nop 指令(sled),运行时通过 runtime library 动态启用/禁用追踪。

核心特性:

  • 零成本抽象: 未启用追踪时 nop sled 几乎无开销(~1ns/函数调用)
  • 支持动态控制(XRAY_OPTIONS 环境变量)
  • 可自定义 handler 写入自定义日志格式
# 编译时插桩
clang++ -fxray-instrument main.cpp -o app

# 运行时启用
XRAY_OPTIONS="verbosity=1 xray_mode=xray-basic" ./app

Google 内部大规模使用,是生产级函数追踪的首选方案。需要使用 Clang/LLVM 编译器重新编译。


六、内核级追踪(eBPF)

uprobe / bpftrace / BCC

原理: 利用 Linux 内核 eBPF 机制,在用户空间函数入口/出口设置断点(uprobe/uretprobe),内核执行 eBPF 程序读取参数/返回值。

# bpftrace 单行命令
bpftrace -e 'uprobe:/path/to/libnative.so:ProcessData {
    printf("arg0=%s arg1=%d\n", str(arg0), arg1);
}'

# BCC trace 工具
trace-bpfcc -p <PID> \
  'p:/path/to/libnative.so:ProcessData' \
  'r:/path/to/libnative.so:ProcessData'

优点:

  • 真正的零侵入——无需修改代码、无需重启进程
  • ✅ 内核级安全沙箱,稳定可靠
  • ✅ 性能开销低(纳秒级)
  • ✅ 可读取函数参数和返回值
  • ✅ 支持生产环境

缺点:

  • 需要 Linux 4.4+ 内核(推荐 5.x+)
  • 需要 debug symbols
  • C++ name mangling 需要处理
  • 复杂参数类型(struct、std::string)解析困难

七、Android 专属方案总结

方案 侵入程度 需要 root 性能开销 生产可用 参数读取
ByteHook (PLT) 需注入 so 否*
ShadowHook (Inline) 需注入 so 否*
Frida ❌ 调试用
Dobby + 自定义 agent 需注入 so 否*

* 如果将 Hook 代码编译进 APK 自身,则不需要 root


八、综合选型矩阵

维度 PLT Hook Inline Hook LD_PRELOAD Frida eBPF LLVM XRay
不改源码 ⚠️ 需重编译
不改构建
读取参数
内部函数
Android ⚠️ ⚠️
性能开销 极低 极低 极低
生产可用

九、推荐实践路线

场景 A:Android Native 调试阶段

推荐:Frida — 零代码改动,几分钟即可开始 trace,脚本灵活

场景 B:Android Native 线上/灰度监控

推荐:ByteHook (PLT) 或 ShadowHook (Inline) — 编译进 APK,无需 root,可远端配置动态开关

场景 C:Linux C++ 开发调试

推荐:LD_PRELOAD + eBPF/bpftrace — 绝对零侵入,LD_PRELOAD 拦截外部调用,eBPF 拦截内部函数

场景 D:Linux C++ 生产级全量追踪

推荐:LLVM XRay(可重编译)或 eBPF(不可重编译) — XRay 的 NOP sled 机制使生产环境几乎无开销

场景 E:Windows C++ 程序

推荐:Microsoft Detours 或 MinHook — 工业标准,稳定成熟


十、风险与注意事项

  1. 线程安全:务必在程序初始化阶段(单线程)完成 Hook 安装
  2. 反检测:生产环境部署需考虑安全检测机制(尤其是 Frida)
  3. 符号表:eBPF 方案高度依赖 debug symbols,C++ name mangling 需通过 c++filt 解析
  4. 性能:Inline Hook 和 Frida 有一定性能开销,高频调用路径需谨慎评估
  5. 合规:涉及用户隐私数据的场景需评估合规要求
  6. 维护成本:Hook 方案与二进制结构强耦合,版本升级后可能需重新适配

参考资源


本文由小Z撰写,如有疑问欢迎讨论。