问题背景
在大型 C++ 和 Android 工程中,尤其是架构历史包袱沉重、代码混乱的项目里,开发者经常遇到这样的需求:
我需要知道某个关键路径函数被谁调用了、参数是什么、返回值是什么、调用链路是怎样的——但我不能(或不想)修改原始源代码。
这个需求在调试、性能分析、安全审计、线上监控等场景中非常普遍。本文将系统性地调研所有可行的技术方案。
方案全景
根据侵入程度和技术原理,我们将所有方案分为 5 大类:
- Hook / 函数拦截(PLT Hook、Inline Hook、LD_PRELOAD)
- 动态二进制插桩 DBI(Frida、Pin、DynamoRIO)
- 编译器插桩(LLVM XRay、-finstrument-functions)
- 内核级追踪(eBPF、bpftrace、BCC)
- Android 专属方案(ByteHook、ShadowHook)
一、PLT Hook(GOT 表替换)
原理
ELF 文件的 PLT(Procedure Linkage Table)+ GOT(Global Offset Table)机制允许在运行时替换 GOT 表条目,使该符号的调用跳转到自定义函数。仅对外部动态链接的函数有效。
适用场景
拦截目标库对 libc、libm 或其他 .so 的外部调用(如 open、write、malloc、send/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 — 工业标准,稳定成熟
十、风险与注意事项
- 线程安全:务必在程序初始化阶段(单线程)完成 Hook 安装
- 反检测:生产环境部署需考虑安全检测机制(尤其是 Frida)
- 符号表:eBPF 方案高度依赖 debug symbols,C++ name mangling 需通过
c++filt解析 - 性能:Inline Hook 和 Frida 有一定性能开销,高频调用路径需谨慎评估
- 合规:涉及用户隐私数据的场景需评估合规要求
- 维护成本:Hook 方案与二进制结构强耦合,版本升级后可能需重新适配
参考资源
- xHook — 爱奇艺 PLT Hook
- ByteHook — 字节跳动 PLT Hook
- ShadowHook — 字节跳动 Inline Hook
- Dobby — 全平台 Inline Hook
- Frida — 动态插桩工具包
- Microsoft Detours
- LLVM XRay
- BCC/BPF Tools
- bpftrace
本文由小Z撰写,如有疑问欢迎讨论。