Hooks and Tracing
Hooks and Tracing
XHook (Android, PLT hook)
XHook intercepts PLT entries — best for hooking library imports.
1IxHook xHook = XHookImpl.getInstance(emulator);
2
3xHook.register("libtarget.so", "malloc", new ReplaceCallback() {
4 @Override
5 public HookStatus onCall(Emulator<?> emulator, long originFunction) {
6 RegisterContext ctx = emulator.getContext();
7 int size = ctx.getIntArg(0);
8 System.out.println("malloc(" + size + ")");
9 return HookStatus.LR(emulator, 0); // replace return value
10 // return HookStatus.RET(originFunction); // call original
11 }
12});
13
14xHook.refresh(); // must call after registering
Key classes: unidbg-android/.../hook/xhook/IxHook.java, XHookImpl.java
HookZz (inline hook)
HookZz hooks at arbitrary addresses — works for non-exported functions.
1IHookZz hookZz = HookZz.getInstance(emulator);
2
3hookZz.wrap(module.base + 0x1234, new WrapCallback<RegisterContext>() {
4 @Override
5 public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
6 System.out.println("pre: arg0=" + ctx.getIntArg(0));
7 }
8
9 @Override
10 public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
11 System.out.println("post: ret=" + ctx.getLongArg(0));
12 }
13});
Key classes: unidbg-api/.../hook/hookzz/IHookZz.java, HookZz.java
Whale Hook
1IWhale whale = Whale.getInstance(emulator);
2whale.inlineHookFunction(address, new ReplaceCallback() { ... });
Key classes: unidbg-api/.../hook/whale/IWhale.java
Code tracing
Prefer
traceCodeTextovertraceCode— it is faster (async I/O + instruction cache), outputs more context (memory hexdump, JNI/syscall parsing, function call tracking), and supports module-name filtering and file redirect. UsetraceCodeonly when you need a rawTraceCodeListenercallback and nothing else.
1// traceCode — synchronous, no cache, minimal output
2emulator.traceCode(module.base, module.base + module.size);
3
4// With custom listener (the only reason to prefer traceCode over traceCodeText)
5emulator.traceCode(module.base, module.base + module.size, new TraceCodeListener() {
6 @Override
7 public void onInstruction(Emulator<?> emulator, long address, String asm) {
8 System.out.println(String.format("0x%x: %s", address, asm));
9 }
10});
traceCodeText — preferred tracer
traceCodeText is a superset of traceCode: it also hooks memory reads/writes and outputs them alongside each instruction, giving a full execution context dump. Returns a TraceHook that can be detached later.
1// Trace everything (stdout)
2TraceHook hook = emulator.traceCodeText();
3
4// Trace address range
5TraceHook hook = emulator.traceCodeText(module.base, module.base + module.size);
6
7// Redirect output to file (file is deleted and recreated each run)
8TraceHook hook = emulator.traceCodeText(module.base, module.base + module.size, "/tmp/trace.txt");
9
10// Redirect to PrintStream
11TraceHook hook = emulator.traceCodeText(module.base, module.base + module.size, System.err);
12
13// Filter by module name(s) — traces only instructions inside these modules
14TraceHook hook = emulator.traceCodeText(new String[]{"libnative.so"});
15TraceHook hook = emulator.traceCodeText(new String[]{"libnative.so", "libcrypto.so"}, "/tmp/trace.txt");
16
17// With custom TraceCodeListener
18TraceHook hook = emulator.traceCodeText(module.base, module.base + module.size,
19 System.out, new TraceCodeListener() { ... });
20
21// Stop tracing
22hook.detach();
traceCode vs traceCodeText
traceCode | traceCodeText | |
|---|---|---|
| Disassembly | yes | yes |
| Register read/write values | yes | yes |
| Memory read/write hexdump | no | yes |
| Syscall / JNI call parsing | no | yes |
| Function call tracking (enter/return) | no | yes |
| SMC (self-modifying code) detection | no | yes |
| Filter by module name | no | yes |
| Redirect to file | no | yes |
| Returns detachable hook | yes | yes (TraceHook) |
| Output thread | synchronous | async (dedicated logger thread) |
| Instruction cache | none | L1 direct-mapped (8M slots) + L2 LRU (1M) |
Performance characteristics of traceCodeText
traceCodeText is specifically engineered for high-throughput tracing:
- Async logging: output is written to a
LinkedBlockingQueue(capacity 100,000) and flushed by a dedicated background thread (Unidbg-Trace-Logger), so the emulation thread is not blocked by I/O. - 8MB block buffer: lines are accumulated in an in-memory
StringBuilder(8 MB) and enqueued in 50,000-line batches, minimizing queue overhead. - Two-level instruction cache: parsed
Instructionobjects (mnemonic, operands, register maps, precomputed prefix strings) are cached in an L1 direct-mapped array (8,388,608 slots ≈ covers 32 MB address space) with L2 LRU overflow (1M entries). Cache misses only happen on first encounter or SMC invalidation. - SMC detection: if
disableSMC=false(default), each instruction’s machine code is compared against the cached value; a mismatch invalidates the cache entry and forces re-disassembly. - Hexdump can be disabled: call
hook.setDisableHexdump(true)to skip memory read/write dumps when you only need disassembly + registers. - Function call tracking can be disabled: call
hook.setDisableFunctionCall(true)to skip thebl/blr/svcparsing overhead.
In practice traceCodeText is faster than traceCode for large traces because traceCode calls emulator.printAssemble() (which also reads registers and computes memory addresses) synchronously on every instruction without any caching or async I/O.
Tuning options (traceCodeText only)
1TraceHook hook = emulator.traceCodeText(module.base, module.base + module.size);
2
3hook.setDisableHexdump(true); // skip memory read/write dumps
4hook.setDisableFunctionCall(true); // skip bl/blr/svc call tracking
5hook.setDisableSMC(true); // skip machine-code cache validation (faster for non-SMC code)
Memory tracing
1// Trace memory reads
2emulator.traceRead(startAddress, endAddress);
3
4// Trace memory writes
5emulator.traceWrite(startAddress, endAddress);
6
7// With custom listeners
8emulator.traceRead(start, end, new TraceReadListener() {
9 @Override
10 public boolean onRead(Emulator<?> emulator, long address, int size, long value) {
11 System.out.printf("READ 0x%x size=%d val=0x%x%n", address, size, value);
12 return true;
13 }
14});
Verbose JNI logging
1vm.setVerbose(true); // prints every unhandled JNI call with its signature
Use this during development to discover which JNI methods the native library calls.
Debugger attachment
1// Attach GDB debugger (blocks until client connects)
2emulator.attach(DebuggerType.GDB_SERVER);
3
4// IDA debugger
5emulator.attach(DebuggerType.ANDROID_SERVER_V7);
Key listener interfaces: unidbg-api/.../listener/TraceCodeListener.java, TraceReadListener.java, TraceWriteListener.java