ARM64 函数级 VM 执行与指令跟踪框架。把任意 native 函数放进 Unicorn 引擎中执行,暴露每条指令、外部调用、SVC、内存访问的全部细节。
本工程已包含预编译的 libtrace.so、头文件和 JNI 示例。克隆后直接编译运行即可查看结果。
预编译动态库:app/src/main/jniLibs/arm64-v8a/libtrace.so
对外头文件:app/src/main/cpp/include/ARM64Emulator.h
trace() — 最简路径,一键出日志,一般用这个就够了,第二个参数是指定一个路径,不要指定名字,它支持多线程调用的,你用的时候,trace这个包装函数会返回一个同等功能的函数指针,你直接用inlinehook或者无痕hook等手段替换到原来地址,等app自己调用,或者你来传参调用都可以
#include "ARM64Emulator.h"
// 本地文件输出(LZ4 压缩,per-thread 自动分 .lz4 文件)
auto fn = (int(*)(int))trace((void*)target_func, "/data/data/pkg/trace_dir");
fn(123); // 调用 → 自动写 trace 日志
freeTrace((uint64_t)fn); // 用完释放
// 输出文件在 trace_dir/ 下,用 trace_receiver.py decode *.lz4 还原为文本
// TCP 远程输出(LZ4 压缩,实时传到 PC,适合超大 trace),在使用的时候adb forward该端口即可
auto fn2 = (int(*)(int))trace((void*)target_func, "tcp:9876");
// ↑ 阻塞等待 PC 连接,PC 端运行: python trace_receiver.py receive,这个的具体使用写在最末尾
fn2(123);
freeTrace((uint64_t)fn2);vm_context* ctx = nullptr;
auto fn = (int(*)(int, int))vc_make_handle((void*)target_func, &ctx);
// 注册 hook
vc_hook_h hh;
vc_hook_add(ctx, &hh, VC_HOOK_EXTERNAL_JUMP, (void*)my_jump_cb, nullptr, 0, 0);
int result = fn(1, 2); // 在 VM 中执行
vc_free(ctx);replace_trace((void*)func_addr, "/data/data/pkg/trace_dir");
// 之后所有对 func_addr 的调用都自动走 VM trace
restore_function((void*)func_addr); // 恢复原函数每种 hook 类型有对应的回调签名:
// 拦截外部函数调用(最常用)
void my_jump_cb(vm_context* ctx, uint64_t address,
const char* symbol_name, vc_event_action* action, void* ud) {
if (symbol_name && strcmp(symbol_name, "getuid") == 0) {
uint64_t fake_uid = 0;
vc_reg_write(ctx, VC_REG_X0, &fake_uid);
*action = VC_ACTION_SKIP; // 跳过真实调用
}
}
// SVC 系统调用监控
void my_svc_cb(vm_context* ctx, uint64_t address, uint32_t syscall_nr, void* ud) {
LOGD("SVC #%u @ 0x%lx", syscall_nr, address);
}
// 基本块
void my_block_cb(vm_context* ctx, uint64_t address, uint32_t size, void* ud) {
// 每个基本块入口触发
}
// 内存读写
void my_mem_cb(vm_context* ctx, vc_mem_type type,
uint64_t address, int size, int64_t value, void* ud) {
if (type == VC_MEM_WRITE) {
LOGD("MEM WRITE @ 0x%lx size=%d val=0x%lx", address, size, value);
}
}| 类型 | 回调签名 | 用途 |
|---|---|---|
VC_HOOK_BLOCK |
vc_cb_hookcode_t |
每个基本块入口 |
VC_HOOK_CODE |
vc_cb_hookcode_t |
每条指令(慢 ~10x) |
VC_HOOK_INTR |
vc_cb_hookintr_t |
中断(含 SVC) |
VC_HOOK_MEM_READ |
vc_cb_hookmem_t |
内存读 |
VC_HOOK_MEM_WRITE |
vc_cb_hookmem_t |
内存写 |
VC_HOOK_MEM_READ_AFTER |
vc_cb_hookmem_t |
内存读后(有值) |
VC_HOOK_SVC |
vc_cb_hooksvc_t |
SVC 系统调用 |
VC_HOOK_EXTERNAL_JUMP |
vc_cb_hookjump_t |
外部函数调用 |
uint64_t pc, x0;
vc_reg_read(ctx, VC_REG_PC, &pc);
vc_reg_read(ctx, VC_REG_X0, &x0);
uint64_t fake_ret = 0;
vc_reg_write(ctx, VC_REG_X0, &fake_ret);
// 批量读
uint64_t x0_val, x1_val, sp_val;
vc_reg regs[] = { VC_REG_X0, VC_REG_X1, VC_REG_SP };
void* vals[] = { &x0_val, &x1_val, &sp_val };
vc_reg_read_batch(ctx, regs, vals, 3);
// SIMD/FP 寄存器(128 位)
__uint128_t q0;
vc_reg_read(ctx, VC_REG_Q0, &q0);| SO 类型 | 默认行为 |
|---|---|
| 目标 SO | VM 内执行 |
| 其他用户 SO | VM 内执行 |
| 系统库(libc 等) | 跳出到 host |
// 某些 SO 不需要 trace,让它们在 host 上跑更快
const char* blacklist[] = { "libutils.so", "libcrypto.so", nullptr };
vc_set_jump_blacklist(blacklist, nullptr, 0);
// 也可以用地址范围
uint64_t ranges[][2] = { { base, base + 0x200000 } };
vc_set_jump_blacklist(nullptr, ranges, 1);
// 清除
vc_clear_jump_blacklist();// 只模拟目标 SO 本身,其他所有用户库都跳出到 host
vc_set_external_jump_enabled(false);所有单步/断点 API 在回调中调用。
// 单步 — 执行 N 条指令后暂停,再次触发回调链
vc_single_step(ctx, 1); // 执行 1 条后暂停
vc_single_step(ctx, 100); // 执行 100 条后暂停
// 回调中不调用 → 恢复正常执行
// 设置停止地址(临时断点),到达后自动清除
vc_set_until(ctx, target_addr);
vc_set_until(ctx, 0); // 手动清除void debugger_cb(vm_context* ctx, uint64_t addr, uint32_t size, void* ud) {
uint64_t pc;
vc_reg_read(ctx, VC_REG_PC, &pc);
// 反汇编当前指令
vc_insn insn;
if (vc_disasm(pc, 1, &insn) > 0) {
LOGD("0x%lx: %s %s", insn.address, insn.mnemonic, insn.op_str);
}
// 打印寄存器
uint64_t x0, x1, sp;
vc_reg_read(ctx, VC_REG_X0, &x0);
vc_reg_read(ctx, VC_REG_X1, &x1);
vc_reg_read(ctx, VC_REG_SP, &sp);
LOGD(" X0=0x%lx X1=0x%lx SP=0x%lx", x0, x1, sp);
vc_single_step(ctx, 1); // 继续单步
}// 不需要 vm_context(identity mapping)
vc_insn insns[10];
int count = vc_disasm(address, 10, insns);
for (int i = 0; i < count; i++) {
LOGD("0x%lx: [%08x] %s %s",
insns[i].address, insns[i].bytes,
insns[i].mnemonic, insns[i].op_str);
}// 停止 VM
vc_emu_stop(ctx);
// CPU 状态快照
vc_cpu_context* snap = nullptr;
vc_context_save(ctx, &snap);
// ... 执行一些操作 ...
vc_context_restore(ctx, snap);
vc_context_free(snap);
// 符号查询
const char* sym = vc_lookup_symbol(ctx, address);// 内置监控(每个 ctx 各只能添加一次)
trace_read(ctx, 0, 0); // 全范围读监控
trace_write(ctx, 0, 0); // 全范围写监控
// 自定义 watchpoint
void mem_watch(vm_context* ctx, vc_mem_type type,
uint64_t address, int size, int64_t value, void* ud) {
uint64_t pc;
vc_reg_read(ctx, VC_REG_PC, &pc);
LOGD("WATCHPOINT: [0x%lx] written by PC=0x%lx val=0x%lx", address, pc, value);
}
vc_hook_h hh;
vc_hook_add(ctx, &hh, VC_HOOK_MEM_WRITE, (void*)mem_watch, nullptr,
watch_addr, watch_addr + 8);void anti_detect(vm_context* ctx, uint64_t addr,
const char* sym, vc_event_action* action, void* ud) {
if (!sym) return;
if (strcmp(sym, "access") == 0) {
uint64_t path_ptr;
vc_reg_read(ctx, VC_REG_X0, &path_ptr);
const char* path = (const char*)path_ptr;
if (strstr(path, "su") || strstr(path, "magisk")) {
uint64_t ret = (uint64_t)-1;
vc_reg_write(ctx, VC_REG_X0, &ret);
*action = VC_ACTION_SKIP;
}
}
else if (strcmp(sym, "getuid") == 0) {
uint64_t ret = 10000;
vc_reg_write(ctx, VC_REG_X0, &ret);
*action = VC_ACTION_SKIP;
}
else if (strcmp(sym, "exit") == 0 || strcmp(sym, "abort") == 0) {
*action = VC_ACTION_SKIP;
}
}
vm_context* ctx = nullptr;
auto fn = (int(*)())vc_make_handle((void*)target, &ctx);
vc_hook_h hh;
vc_hook_add(ctx, &hh, VC_HOOK_EXTERNAL_JUMP, (void*)anti_detect, nullptr, 0, 0);
fn();
vc_free(ctx);trace 输出为 LZ4 压缩格式(.lz4 文件),用 trace_receiver.py decode 还原为文本。
还原后每行一条指令:
so+0xOFFSET 0xPC: mnemonic operands reg_reads => reg_writes mem_r[0xADDR] / mem_w[0xADDR]=VAL
- str:"..." — 访问地址是 C 字符串时完整输出
- ->"..." — 加载的值是字符串指针时输出指向的字符串
- [CALL] func(args) — 外部函数调用
- >>> JNIEnv->Method() — JNI 调用
| API | 用途 |
|---|---|
trace(func, path) |
快速 trace(path 为目录 → 本地 .lz4,"tcp:PORT" → 远程) |
trace(func, path, &ctx) |
带 ctx 的 trace |
freeTrace(wrapper) |
释放 trace 句柄 |
replace_trace(func, path) |
全局替换式 trace |
restore_function(func) |
恢复被替换的函数 |
vc_make_handle(func, &ctx) |
创建裸 VM 句柄 |
vc_free(ctx) |
释放 VM 上下文 |
vc_hook_add(ctx, &hh, type, cb, ud, begin, end) |
注册 hook |
vc_hook_del(ctx, hh) |
删除 hook |
vc_reg_read / vc_reg_write |
寄存器读写 |
vc_reg_read_batch / vc_reg_write_batch |
批量读写 |
vc_emu_stop(ctx) |
停止 VM |
vc_single_step(ctx, count) |
执行 N 条后暂停 |
vc_set_until(ctx, addr) |
设置临时断点 |
vc_disasm(addr, count, out) |
反汇编 |
vc_context_save / restore / free |
CPU 快照 |
vc_lookup_symbol(ctx, addr) |
地址查符号 |
vc_set_jump_blacklist(names, ranges, n) |
设置跳转黑名单 |
vc_clear_jump_blacklist() |
清除黑名单 |
vc_set_external_jump_enabled(enabled) |
全局跳转开关 |
trace_read / trace_write |
内存监控 |
| 模式 | 速度 | 适用场景 |
|---|---|---|
| vc_make_handle(默认) | ~2-5x 慢 | 功能验证、外部调用监控 |
| trace() 指令级 | ~50-100x 慢 | 详细分析、逆向工程 |
| vc_make_handle + CODE | ~10-20x 慢 | 自定义逐指令监控 |
| vc_single_step(ctx, 1) | ~100-200x 慢 | 精确调试 |
点击界面上的 Run Native Demo 后会按顺序运行:
- 自定义回调示例 —
vc_make_handle+ 事件回调 + 伪造返回值 - JNI trace 示例 —
trace()在 Android 环境中跟踪 JNI 调用 - 原子指令测试 — LDXR/STXR/CAS/SWP/LDADD + std::atomic 正确性验证
- 普通 trace 示例 — 最小接入路径
运行后在 filesDir 下生成日志文件,界面输出中显示完整路径。
AndroidPrybar/
|-- TraceDemo/ ← Android 接入示例工程
| `-- app/src/main/
| |-- cpp/include/ARM64Emulator.h ← API 头文件
| |-- cpp/native-lib.cpp ← Demo 示例代码
| `-- jniLibs/arm64-v8a/libtrace.so ← 预编译库
|-- tools/
| |-- trace_receiver.py ← TCP 接收 + LZ4 解码工具
| `-- build_calltree.py ← trace → 函数调用树/调用图
|-- .claude/skills/unicorn-trace/ ← 附带的 Claude Code 用法 skill
`-- README.md
trace 输出统一为 LZ4 压缩格式(.lz4 文件),压缩比约 7-8x。本工具负责解码和 TCP 接收。
手机上 trace 产生的文件是 .lz4 格式,先 adb pull 到 PC,再用 decode 还原为可读文本:
# 从手机拉取 trace 目录
adb pull /data/data/com.xxx/trace_dir/ .
# 解码
python tools/trace_receiver.py decode trace.lz4 # → trace.log
python tools/trace_receiver.py decode *.lz4 # 批量解码所有 .lz4
python tools/trace_receiver.py decode trace.lz4 --stdout # 输出到 stdout(可 pipe 给 grep 等)
python tools/trace_receiver.py decode trace.lz4 -o out.log # 指定输出文件名trace 量很大时(几百 MB ~ 几 GB),不适合写手机存储,用 TCP 实时传到 PC:
# 1. PC 端先启动接收(自动 adb forward,等待设备连接)
python tools/trace_receiver.py receive
# 2. App 中调用 trace(func, "tcp:9876") → 设备阻塞等待 PC 连接
# 连接建立后 trace 数据实时流式传输到 PC
# freeTrace() 后自动结束
# 自定义选项
python tools/trace_receiver.py receive -p 12345 -o my.log # 自定义端口和输出前缀
python tools/trace_receiver.py receive --no-adb --host 192.168.1.x # WiFi 直连(不走 USB)
python tools/trace_receiver.py receive --stdout # 输出到 stdouttrace 支持多线程并发调用,每个线程的 trace 数据带有 tid 标识:
- 本地文件模式:每个线程自动生成独立文件,如
func_tid1234_0.lz4、func_tid1234_1.lz4(同一线程多次调用自动递增序号) - TCP 模式:所有线程数据通过同一 socket 传输,接收端自动按 tid 分文件,如
trace_tid1234_0.log、trace_tid5678_0.log。同一 tid 多次接收时序号自动递增,不会覆盖已有文件 - decode 多线程 .lz4:如果一个
.lz4文件中包含多个 tid 的数据,decode 会自动拆分为_tid{N}.log
接收/解码时终端会实时显示各线程数据量:
[*] frames=42 total=3.2MB ratio=7.6x speed=12.5MB/s [t1234:2.1M t5678:1.1M]
从 trace 重建函数调用树/调用图(谁调了谁、各函数调用次数)。脚本侧建树,部分/被打断的 trace 也能建;新引擎的多线程输出(每线程一文件的目录)传目录即自动每线程一棵树。
python tools/build_calltree.py <trace文件或目录> [so名] [入口偏移hex] [最大行数]
# 输出: <base>_calltree.txt(调用树) + <base>_callgraph.txt(调用图/计数)本仓库内置一个 Claude Code skill:unicorn-trace。用 Claude Code 打开本仓库干活时,它会自动带上 libtrace 的完整用法,AI 直接知道怎么调 trace() / vc_make_handle / 各类 hook,不用每次解释。
SKILL.md:精简速查(两个入口、API 速查表、Hook 类型、实战示例索引、trace 格式、性能、限制)。GUIDE.md:完整指南(完整 API + 实战示例 + trace 格式)。- 想在别的项目用:把
.claude/skills/unicorn-trace/复制到那个项目的.claude/skills/或用户级~/.claude/skills/。
欢迎扫码进群,一起学习和交流 Android Native / Trace / VM 相关内容。
交流群二维码
这个工具我断断续续写了两年。当时几乎没有好用且公开的 trace 工具,也无人分享开发此类工具的方法。我一点点摸索、测试、推倒重来,仅为了编写一个支持自动传参的 JIT,就思考测试了许久。
同时,我知道在 AI 时代,即使不开放源码,通过 IDA 反汇编也可以很快复刻出工具并学习其中的思路。但这并不重要。
不求署名,不求分润,不求你在任何地方提及我的名字。我只求你在按下"复制"或"生成"之前,在心里,对这段熬了两年的代码,说一声谢谢。不需要我这份刚燃起开源的心冷却下去,不需要写在任何文档里。只要你自己知道,这东西不是凭空从石头里蹦出来的,它是某个人在无数个深夜里,一行一行从混沌里捞出来的,这就够了。