1. 1. 前言
  2. 2. 基本原理
    1. 2.1. ELF 文件结构
    2. 2.2. 动态链接重定位过程
  3. 3. 源码分析
    1. 3.1. 初始化流程
    2. 3.2. Hook 单个调用者流程
      1. 3.2.1. 步骤1:创建 Hook 任务
      2. 3.2.2. 步骤2:添加到任务管理器
      3. 3.2.3. 步骤3:执行 Hook
    3. 3.3. ELF 解析与符号查找
    4. 3.4. 动态段解析
    5. 3.5. 跳板(Trampoline)机制
      1. 3.5.1. 跳板内存管理
      2. 3.5.2. 跳板代码模板
  4. 4. 核心特性分析
    1. 4.1. 1. CFI 绕过
    2. 4.2. 2. 无锁跳板(Lock-Free Trampoline)
      1. 4.2.1. TLS(Thread Local Storage)栈
      2. 4.2.2. 核心流程
      3. 4.2.3. 深入剖析:内存屏障与并发控制
        1. 4.2.3.1. 1. 快照机制(Snapshot)是核心
        2. 4.2.3.2. 2. 内存屏障的作用
        3. 4.2.3.3. 3. 完整的并发控制总结
        4. 4.2.3.4. 4. 对比:如果使用原子读取会怎样?
    3. 4.3. 3. Native 崩溃兜底
    4. 4.4. 4. 延迟销毁机制(Delayed Destruction)
      1. 4.4.1. 延迟销毁的实现
      2. 4.4.2. 延迟销毁的目的
      3. 4.4.3. 两层延迟机制
      4. 4.4.4. 延迟销毁的潜在问题
      5. 4.4.5. 改进方案
      6. 4.4.6. 实际评估
      7. 4.4.7. 完整的内存安全保障
    5. 4.5. 5. Init/Fini Hook(需要 Inline Hook 支持)
      1. 4.5.1. 什么是 .init_array?
      2. 4.5.2. 为什么需要 Inline Hook?
        1. 4.5.2.1. 实现原理
        2. 4.5.2.2. 对比:有无 Inline Hook 的区别
      3. 4.5.3. 实际案例分析
      4. 4.5.4. 架构限制
      5. 4.5.5. 后置回调:清理资源
      6. 4.5.6. 总结:PLT Hook + Inline Hook 的必要性
  5. 5. 架构设计总结
    1. 5.1. 核心数据结构关系
    2. 5.2. 执行流程
    3. 5.3. 性能优化要点
    4. 5.4. 稳定性保障
  6. 6. 回答开篇问题
    1. 6.1. 1. 线程安全如何保障?
    2. 6.2. 2. 性能如何保障?
    3. 6.3. 3. 崩溃如何兜底?
    4. 6.4. 4. 为什么需要 Inline Hook?
  7. 7. 总结
  8. 8. 参考资料

温故知新-bhook原理分析

深入分析优秀的 PLT Hook 框架 bhook 的实现原理与工程实践,与 AI 合作完成。

前言

PLT Hook 在 Android 上是比较成熟稳定的 Hook 技术,其中目前最为健壮的方案主要是字节跳动开源的 bhook。本文将深入分析其中各种优秀的工程实践,以及我们能够学习借鉴的部分。

在开始之前,让我们带着以下几个核心问题来深入学习:

  1. 线程安全:如何保障线程安全,怎么避免同时 hook/unhook 造成的影响?
  2. 性能优化:怎么保障性能,避免 hook 点成为线程阻塞的瓶颈?
  3. 稳定性保障:如何尽量兜底避免可能的内存异常崩溃?
  4. 架构演进:为什么新版本要依赖 shadowhook inline hook?

基本原理

PLT Hook 的基本原理 bhook 官方文档已经介绍得十分详细,建议先阅读:https://github.com/bytedance/bhook/blob/main/doc/overview.zh-CN.md

ELF 文件结构

后续分析会反复涉及到 ELF 文件中的各种 section 和 segment:

img

动态链接重定位过程

下图展示了动态链接器进行符号重定位的过程:

img

PLT Hook 的核心思想就是修改 GOT 表中的函数地址,将其指向我们的 hook 函数。

源码分析

分析版本:1.1.1
commit:ecb90454a64137c1cde5d9d3866af6999e13e0fd

初始化流程

bhook 的初始化主要完成以下几个关键模块的准备工作:

1
2
3
4
5
6
7
8
if (__predict_false(0 != bh_linker_init())) GOTO_END(BYTEHOOK_STATUS_CODE_INITERR_SYM);
if (__predict_false(0 != bytesig_init(SIGSEGV))) GOTO_END(BYTEHOOK_STATUS_CODE_INITERR_SIG);
if (__predict_false(0 != bytesig_init(SIGBUS))) GOTO_END(BYTEHOOK_STATUS_CODE_INITERR_SIG);
if (__predict_false(0 != bh_cfi_disable_slowpath())) GOTO_END(BYTEHOOK_STATUS_CODE_INITERR_CFI);
if (__predict_false(0 != bh_safe_init())) GOTO_END(BYTEHOOK_STATUS_CODE_INITERR_SAFE);
if (BYTEHOOK_IS_AUTOMATIC_MODE) {
if (__predict_false(0 != bh_hub_init())) GOTO_END(BYTEHOOK_STATUS_CODE_INITERR_HUB);
}

各模块功能说明:

  • bh_linker_init:在不同 Android 版本下完成动态链接器(linker)初始化,核心任务是获取并保存 linker 内部的全局互斥锁以及 dlopen 相关函数指针,为后续 hook / 监控 so 加载做准备
  • **bytesig_init(SIGSEGV/SIGBUS)**:注册 SIGSEGV 和 SIGBUS 信号处理器,用于捕获 native 崩溃,提供崩溃兜底机制
  • **bh_cfi_disable_slowpath()**:禁用 arm64 的 CFI(Control Flow Integrity)控制流完整性慢路径检查,避免 CFI 机制阻止 hook 执行
  • **bh_safe_init()**:获取 libc 的 pthread_getspecific、pthread_setspecific、abort 等关键函数地址,确保在异常情况下也能安全调用
  • **bh_hub_init()**:在自动模式下初始化 hub 管理器,用于管理跳板(trampoline)和代理(proxy)

Hook 单个调用者流程

我们以 bytehook_hook_single 为例,分析单个调用者的 hook 流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bytehook_stub_t bytehook_hook_single(const char *caller_path_name, const char *callee_path_name,
const char *sym_name, void *new_func, bytehook_hooked_t hooked,
void *hooked_arg) {
const void *caller_addr = __builtin_return_address(0);
if (NULL == caller_path_name || NULL == sym_name || NULL == new_func) return NULL;
if (BYTEHOOK_STATUS_CODE_OK != bytehook_init_errno) return NULL;

bh_task_t *task = bh_task_create_single(caller_path_name, callee_path_name, sym_name, new_func, hooked,
hooked_arg, false);
if (NULL != task) {
bh_task_manager_add(task);
bh_task_manager_hook(task);
bh_recorder_add_hook(task->status_code, caller_path_name, sym_name, (uintptr_t)new_func, (uintptr_t)task,
(uintptr_t)caller_addr);
}
return (bytehook_stub_t)task;
}

步骤1:创建 Hook 任务

1
2
3
bh_task_t *bh_task_create_single(const char *caller_path_name, const char *callee_path_name,
const char *sym_name, void *new_func, bytehook_hooked_t hooked,
void *hooked_arg, bool is_invisible);

创建一个 task 结构体,保存 hook 所需的各种信息(调用者路径、被调用者路径、符号名、新函数地址等)。

步骤2:添加到任务管理器

1
void bh_task_manager_add(bh_task_t *task);

将 task 加入到一个尾队列(tail queue)中,本质是双向链表,支持正反向遍历。

步骤3:执行 Hook

1
void bh_task_manager_hook(bh_task_t *task);

实际执行 hook 操作。核心流程在 bh_task_hook 中:

1
2
3
4
5
void bh_task_hook(bh_task_t *self) {
bh_task_log(self, NULL, -1);
if (0 != bh_task_check_pre_hook(self)) return;
bh_task_handle(self);
}

ELF 解析与符号查找

在执行 hook 之前,需要先验证被调用者符号是否存在:

1
2
3
4
5
6
7
8
void *bh_elf_manager_find_export_addr(const char *pathname, const char *sym_name) {
bh_elf_t *elf = bh_elf_manager_find_elf(pathname);
if (NULL == elf) return NULL;

void *addr = bh_elf_find_export_func_addr_by_symbol_name(elf, sym_name);
bh_elf_decrement_ref_count(elf);
return addr;
}

然后根据任务类型查找调用者 ELF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void bh_task_handle(bh_task_t *self) {
switch (self->type) {
case BH_TASK_TYPE_SINGLE: {
bh_elf_t *caller_elf = bh_elf_manager_find_elf(self->caller_path_name);
if (NULL != caller_elf) {
bh_task_hook_or_unhook(self, caller_elf);
bh_elf_decrement_ref_count(caller_elf);
}
break;
}
case BH_TASK_TYPE_ALL:
case BH_TASK_TYPE_PARTIAL:
bh_elf_manager_iterate(bh_task_elf_iterate_cb, (void *)self);
break;
}
}

注意:bhook 是针对调用者(caller)进行修改的,而不是被调用者(callee)。

动态段解析

在调用者的 ELF 中查找符号和 GOT 表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ElfW(Sym) *bh_elf_find_symbol_and_gots_by_symbol_name(bh_elf_t *self, const char *sym_name, void *callee_addr,
bh_array_t *gots, bh_array_t *prots) {
if (self->error) return NULL;
if (0 != bh_elf_parse_dynamic(self)) return NULL;

ElfW(Sym) *sym = NULL;

BH_SIG_TRY(SIGSEGV, SIGBUS) {
sym = bh_elf_find_symbol_and_gots_by_symbol_name_unsafe(self, sym_name, callee_addr, gots, prots);
}
BH_SIG_CATCH() {
self->error = true;
sym = NULL;
}
BH_SIG_EXIT

return sym;
}

解析 PT_DYNAMIC 段是关键步骤,需要提取各种重定位信息和符号表信息:

d_tag 对应的 ELF 节区 存到结构体里的意义
DT_JMPREL .rel.plt.rela.plt 记录 PLT 延迟重定位表 的首地址(rel_plt
DT_PLTRELSZ .rel.plt/.rela.plt 的大小 计算并保存 PLT 重定位项个数rel_plt_cnt
DT_REL / DT_RELA .rel.dyn.rela.dyn 记录 非 PLT 重定位表 的首地址(rel_dyn
DT_RELSZ / DT_RELASZ .rel.dyn/.rela.dyn 的大小 计算并保存 非 PLT 重定位项个数rel_dyn_cnt
DT_ANDROID_REL / DT_ANDROID_RELA Android 专用 APS2 重定位段 记录 APS2 压缩重定位数据的首地址(rel_dyn_aps2
DT_ANDROID_RELSZ / DT_ANDROID_RELASZ APS2 重定位段的大小 保存 APS2 数据总字节数(rel_dyn_aps2_sz
DT_SYMTAB .dynsym 保存 动态符号表 首地址(dynsym
DT_STRTAB .dynstr 保存 动态字符串表 首地址(dynstr
DT_HASH .hash 解析并保存 SysV 哈希表 结构(sysv_hash.*
DT_GNU_HASH .gnu.hash 解析并保存 GNU 哈希表 结构(gnu_hash.*
DT_NULL 动态段结束标志;循环遇到即终止

解析代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
static void bh_elf_parse_dynamic_unsafe(bh_elf_t *self, ElfW(Dyn) *dynamic) {
// iterate the dynamic segment
for (ElfW(Dyn) *entry = dynamic; entry && entry->d_tag != DT_NULL; entry++) {
switch (entry->d_tag) {
//.rel.plt / .rela.plt
case DT_JMPREL:
self->rel_plt = (const Elf_Reloc *)(self->load_bias + entry->d_un.d_ptr);
break;
case DT_PLTRELSZ:
self->rel_plt_cnt = (size_t)entry->d_un.d_val / sizeof(Elf_Reloc);
break;

//.rel.dyn / .rela.dyn
case DT_REL:
case DT_RELA:
self->rel_dyn = (const Elf_Reloc *)(self->load_bias + entry->d_un.d_ptr);
break;
case DT_RELSZ:
case DT_RELASZ:
self->rel_dyn_cnt = (size_t)entry->d_un.d_val / sizeof(Elf_Reloc);
break;

//.rel.dyn / .rela.dyn (APS2 format)
case DT_ANDROID_REL:
case DT_ANDROID_RELA:
self->rel_dyn_aps2 = (uint8_t *)(self->load_bias + entry->d_un.d_ptr);
break;
case DT_ANDROID_RELSZ:
case DT_ANDROID_RELASZ:
self->rel_dyn_aps2_sz = (size_t)entry->d_un.d_val;
break;

//.dynsym
case DT_SYMTAB:
self->dynsym = (ElfW(Sym) *)(self->load_bias + entry->d_un.d_ptr);
break;

//.dynstr
case DT_STRTAB:
self->dynstr = (const char *)(self->load_bias + entry->d_un.d_ptr);
break;

//.hash
case DT_HASH:
self->sysv_hash.buckets_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[0];
self->sysv_hash.chains_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[1];
self->sysv_hash.buckets = &(((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[2]);
self->sysv_hash.chains = &(self->sysv_hash.buckets[self->sysv_hash.buckets_cnt]);
break;

//.gnu.hash
case DT_GNU_HASH:
self->gnu_hash.buckets_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[0];
self->gnu_hash.symoffset = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[1];
self->gnu_hash.bloom_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[2];
self->gnu_hash.bloom_shift = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[3];
self->gnu_hash.bloom = (const ElfW(Addr) *)(self->load_bias + entry->d_un.d_ptr + 16);
self->gnu_hash.buckets = (const uint32_t *)(&(self->gnu_hash.bloom[self->gnu_hash.bloom_cnt]));
self->gnu_hash.chains = (const uint32_t *)(&(self->gnu_hash.buckets[self->gnu_hash.buckets_cnt]));
break;

default:
break;
}
}

// check and fix APS2
if (NULL != self->rel_dyn_aps2) {
char *rel = (char *)self->rel_dyn_aps2;
if (self->rel_dyn_aps2_sz < 4 || rel[0] != 'A' || rel[1] != 'P' || rel[2] != 'S' || rel[3] != '2') {
self->rel_dyn_aps2 = 0;
self->rel_dyn_aps2_sz = 0;
} else {
self->rel_dyn_aps2 += 4;
self->rel_dyn_aps2_sz -= 4;
}
}
}

解析完成后,bhook 会优先使用 hash 表(SysV hash 或 GNU hash)快速查找符号。如果没有 hash 表,则线性扫描 .rel.plt.rel.dyn 重定位表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int bh_elf_check_reloc(bh_elf_t *self, const Elf_Reloc *rel, const char *sym_name, void *callee_addr,
bh_array_t *gots, bh_array_t *prots, ElfW(Sym) **sym, bool plt_jump) {
const uint32_t r_type = BH_ELF_R_TYPE(rel->r_info);
const uint32_t r_sym = BH_ELF_R_SYM(rel->r_info);
void *got_addr = (void *)(self->load_bias + rel->r_offset);

if (plt_jump) {
if (BH_ELF_R_JUMP_SLOT != r_type) return 0; // continue;
} else {
#ifndef __riscv
if (BH_ELF_R_GLOB_DAT != r_type && BH_ELF_R_ABS != r_type) return 0; // continue;
#endif
}

if (NULL == *sym) {
// 比较符号名
if (0 == strcmp(self->dynstr + self->dynsym[r_sym].st_name, sym_name)) {
*sym = &self->dynsym[r_sym];
// 验证 GOT 地址是否匹配被调用者地址
if (NULL != callee_addr && *((void **)got_addr) != callee_addr) return -1; // callee mismatch
if (0 != bh_array_push(gots, (uintptr_t)got_addr)) return -1; // oom
if (0 != bh_array_push(prots, (uintptr_t)bh_elf_get_protect(self, got_addr))) return -1; // oom
}
} else {
if (&self->dynsym[r_sym] == *sym) {
if (NULL != callee_addr && *((void **)got_addr) != callee_addr) return -1; // callee mismatch
if (0 != bh_array_push(gots, (uintptr_t)got_addr)) return -1; // oom
if (0 != bh_array_push(prots, (uintptr_t)bh_elf_get_protect(self, got_addr))) return -1; // oom
}
}

return 0; // continue;
}

这里的关键逻辑是:

  1. 检查重定位类型是否是 PLT 跳转或全局数据引用
  2. 匹配符号名
  3. 验证 GOT 地址内容是否指向预期的被调用者地址
  4. 收集所有匹配的 GOT 地址和对应的内存保护属性

跳板(Trampoline)机制

找到 GOT 表项后,bhook 的核心创新在于使用跳板机制实现多个 hook 的链式调用。第一次 hook 时会创建一个跳板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
bh_hub_t *bh_hub_create(uintptr_t *trampo) {
size_t code_size = (uintptr_t)(&bh_hub_trampo_template_data) - (uintptr_t)(bh_hub_trampo_template_start());
size_t data_size = sizeof(void *) + sizeof(void *);

bh_hub_t *self = malloc(sizeof(bh_hub_t));
if (NULL == self) return NULL;
SLIST_INIT(&self->proxies);
pthread_mutex_init(&self->proxies_lock, NULL);
self->orig_addr = 0;

// alloc memory for trampoline
if (0 == (self->trampo = bh_trampo_alloc(&bh_hub_trampo_mgr))) {
free(self);
return NULL;
}

// fill in code
BH_SIG_TRY(SIGSEGV, SIGBUS) {
memcpy((void *)self->trampo, bh_hub_trampo_template_start(), code_size);
}
BH_SIG_CATCH() {
bh_trampo_free(&bh_hub_trampo_mgr, self->trampo);
free(self);
BH_LOG_WARN("hub: fill in code crashed");
return NULL;
}
BH_SIG_EXIT

// fill in data
void **data = (void **)(self->trampo + code_size);
*data++ = (void *)bh_hub_push_stack;
*data = (void *)self;

// clear CPU cache
bh_util_clear_cache(self->trampo, code_size + data_size);

#if defined(__arm__) && defined(__thumb__)
*trampo = self->trampo + 1;
#else
*trampo = self->trampo;
#endif

BH_LOG_INFO("hub: create trampo at %" PRIxPTR ", size %zu + %zu = %zu", *trampo, code_size, data_size,
code_size + data_size);
return self;
}

跳板内存管理

bh_trampo_alloc 使用巧妙的内存池管理机制:

  1. Bitmap 标记:使用位图标记每个跳板槽的使用状态
  2. 按需分配:首次使用时 mmap 一整页内存,然后分割成多个跳板槽
  3. 延迟释放:释放的槽不会立即复用,而是延迟一段时间(默认 5 秒),防止其他线程仍在执行旧的跳板代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
uintptr_t bh_trampo_alloc(bh_trampo_mgr_t *mgr) {
uintptr_t trampo = 0;
uintptr_t new_ptr;
uintptr_t new_ptr_prctl = (uintptr_t)MAP_FAILED;
size_t trampo_page_size = bh_util_get_page_size();
size_t count = trampo_page_size / mgr->trampo_size;

struct timeval now;
if (mgr->delay_sec > 0) gettimeofday(&now, NULL);

pthread_mutex_lock(&mgr->pages_lock);

// try to find an unused trampo
bh_trampo_page_t *page;
SLIST_FOREACH(page, &mgr->pages, link) {
for (uintptr_t i = 0; i < count; i++) {
size_t flags_idx = i / 32;
uint32_t mask = (uint32_t)1 << (i % 32);
if (0 == (page->flags[flags_idx] & mask)) // check flag
{
// check timestamp - 延迟释放检查
if (mgr->delay_sec > 0 &&
(now.tv_sec <= page->timestamps[i] || now.tv_sec - page->timestamps[i] <= mgr->delay_sec))
continue;

// OK
page->flags[flags_idx] |= mask;
trampo = page->ptr + (mgr->trampo_size * i);
memset((void *)trampo, 0, mgr->trampo_size);
goto end;
}
}
}

// alloc a new memory page
new_ptr = (uintptr_t)(mmap(NULL, trampo_page_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
if ((uintptr_t)MAP_FAILED == new_ptr) goto err;
new_ptr_prctl = new_ptr;

// create a new trampo-page info
if (NULL == (page = calloc(1, sizeof(bh_trampo_page_t)))) goto err;
memset((void *)new_ptr, 0, trampo_page_size);
page->ptr = new_ptr;
new_ptr = (uintptr_t)MAP_FAILED;
if (NULL == (page->flags = calloc(1, BH_UTIL_ALIGN_END(count, 32) / 8))) goto err;
page->timestamps = NULL;
if (mgr->delay_sec > 0) {
if (NULL == (page->timestamps = calloc(1, count * sizeof(time_t)))) goto err;
}
SLIST_INSERT_HEAD(&mgr->pages, page, link);

// alloc trampo from the new memory page
page->flags[0] |= (uint32_t)1;
trampo = page->ptr;

end:
pthread_mutex_unlock(&mgr->pages_lock);
if ((uintptr_t)MAP_FAILED != new_ptr_prctl)
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, new_ptr_prctl, trampo_page_size, mgr->page_name);
return trampo;

err:
pthread_mutex_unlock(&mgr->pages_lock);
if (NULL != page) {
if (0 != page->ptr) munmap((void *)page->ptr, trampo_page_size);
if (NULL != page->flags) free(page->flags);
if (NULL != page->timestamps) free(page->timestamps);
free(page);
}
if ((uintptr_t)MAP_FAILED != new_ptr) munmap((void *)new_ptr, trampo_page_size);
return 0;
}

跳板代码模板

跳板代码需要完成以下任务:

  1. 保存所有寄存器状态(参数寄存器、浮点寄存器、链接寄存器)
  2. 调用 bh_hub_push_stack 获取实际要执行的 hook 函数地址
  3. 恢复寄存器状态
  4. 跳转到 hook 函数

以 ARM64 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
extern void *bh_hub_trampo_template_data __attribute__((visibility("hidden")));
__attribute__((naked)) static void bh_hub_trampo_template(void) {
__asm__(
// Save parameter registers, XR(X8), LR
"stp x0, x1, [sp, #-0xd0]! \n"
"stp x2, x3, [sp, #0x10] \n"
"stp x4, x5, [sp, #0x20] \n"
"stp x6, x7, [sp, #0x30] \n"
"stp x8, lr, [sp, #0x40] \n"
"stp q0, q1, [sp, #0x50] \n" // 浮点寄存器
"stp q2, q3, [sp, #0x70] \n"
"stp q4, q5, [sp, #0x90] \n"
"stp q6, q7, [sp, #0xb0] \n"

// Call bh_hub_push_stack()
"ldr x0, hub_ptr \n" // 第一个参数:hub 指针
"mov x1, lr \n" // 第二个参数:返回地址
"ldr x16, push_stack \n"
"blr x16 \n" // 调用函数

// Save the hook function's address to IP register
"mov x16, x0 \n" // 保存返回的 hook 函数地址

// Restore parameter registers, XR(X8), LR
"ldp q6, q7, [sp, #0xb0] \n"
"ldp q4, q5, [sp, #0x90] \n"
"ldp q2, q3, [sp, #0x70] \n"
"ldp q0, q1, [sp, #0x50] \n"
"ldp x8, lr, [sp, #0x40] \n"
"ldp x6, x7, [sp, #0x30] \n"
"ldp x4, x5, [sp, #0x20] \n"
"ldp x2, x3, [sp, #0x10] \n"
"ldp x0, x1, [sp], #0xd0 \n"

// Call hook function
"br x16 \n"

"bh_hub_trampo_template_data:"
".global bh_hub_trampo_template_data;"
"push_stack:"
".quad 0;"
"hub_ptr:"
".quad 0;");
}

跳板代码的最后两个 .quad 0 是数据段,在创建跳板时会被填充为实际的函数指针:

  • push_stack:指向 bh_hub_push_stack 函数
  • hub_ptr:指向当前 hub 结构体

核心特性分析

1. CFI 绕过

CFI(Control Flow Integrity,控制流完整性) 是 Android 7.0+ 引入的安全机制,用于防止控制流劫持攻击。它会在运行时检查函数调用是否合法。

bhook 需要修改 GOT 表,这会触发 CFI 检查。bhook 的解决方案是:

1
int bh_cfi_disable_slowpath(void);

该函数会 hook linker 中的 CFI 慢路径检查函数(__cfi_slowpath__cfi_slowpath_diag),让它们直接返回,从而绕过 CFI 检查。

实现原理

  1. 找到 linker 中的 __cfi_slowpath 符号
  2. 修改其函数入口,使其直接返回而不执行检查
  3. 针对不同架构使用不同的返回指令(ARM: ret, ARM64: ret, x86_64: retq

2. 无锁跳板(Lock-Free Trampoline)

这是 bhook 最精妙的设计之一。传统的 hook 方案在执行 hook 函数时需要加锁,这会带来严重的性能问题。bhook 通过以下机制实现了无锁设计:

TLS(Thread Local Storage)栈

每个线程维护自己的调用栈,存储在 TLS 中:

1
2
3
4
5
6
7
8
9
10
typedef struct {
size_t frames_cnt;
bh_hub_frame_t frames[BH_HUB_STACK_FRAME_MAX];
} bh_hub_stack_t;

typedef struct {
bh_hub_proxy_list_t proxies; // 当前有效的 proxy 列表快照
uintptr_t orig_addr; // 原始函数地址
void *return_address; // 返回地址
} bh_hub_frame_t;

核心流程

  1. 进入跳板:跳板代码调用 bh_hub_push_stack(hub, return_address)
  2. 获取栈:从 TLS 或全局缓存中获取线程私有栈
  3. 复制 proxy 列表:将当前 hub 的 proxy 列表快照复制到栈帧中(这是关键!)
  4. 返回 hook 函数:返回第一个 enabled 的 proxy 的函数地址
  5. 链式调用:hook 函数可以调用 BYTEHOOK_CALL_PREV 来调用下一个 hook 或原函数

为什么是无锁的?

  • 每个线程有自己的栈,不需要跨线程同步
  • proxy 列表是快照,即使其他线程修改了 hub 的 proxy 列表,也不影响当前线程的执行
  • 使用原子操作和内存屏障保证可见性

深入剖析:内存屏障与并发控制

这里有一个非常精妙的设计细节,值得仔细分析:

问题:为什么写入 proxy->enabled 使用 __ATOMIC_SEQ_CST,但读取时却是普通读取?

查看源码会发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// bh_hub_add_proxy - 添加 proxy 时
int bh_hub_add_proxy(bh_hub_t *self, uintptr_t proxy_func) {
pthread_mutex_lock(&self->proxies_lock);

// ...existing code...

// 重新启用已存在的 proxy
if (proxy->func == (void *)proxy_func) {
if (!proxy->enabled)
__atomic_store_n((bool *)&proxy->enabled, true, __ATOMIC_SEQ_CST); // ← SEQ_CST
goto end;
}

// 插入新 proxy 到链表头
SLIST_NEXT(proxy, link) = SLIST_FIRST(&self->proxies);
__atomic_store_n((uintptr_t *)(&SLIST_FIRST(&self->proxies)),
(uintptr_t)proxy, __ATOMIC_RELEASE); // ← RELEASE

pthread_mutex_unlock(&self->proxies_lock);
}

// bh_hub_del_proxy - 删除 proxy 时
int bh_hub_del_proxy(bh_hub_t *self, uintptr_t proxy_func, bool *have_enabled_proxy) {
pthread_mutex_lock(&self->proxies_lock);

SLIST_FOREACH(proxy, &self->proxies, link) {
if (proxy->func == (void *)proxy_func) {
if (proxy->enabled)
__atomic_store_n((bool *)&proxy->enabled, false, __ATOMIC_SEQ_CST); // ← SEQ_CST
deleted = true;
}
}

pthread_mutex_unlock(&self->proxies_lock);
}

// bh_hub_push_stack - 执行 hook 时(热路径)
static void *bh_hub_push_stack(bh_hub_t *self, void *return_address) {
// ...existing code...

bh_hub_proxy_t *proxy;
SLIST_FOREACH(proxy, &self->proxies, link) {
if (proxy->enabled) { // ← 普通读取,没有使用 __atomic_load_n
// 找到第一个启用的 proxy
frame->proxies = self->proxies; // ← 复制链表头(快照)
return proxy->func;
}
}
}

// bh_hub_get_prev_func - 获取下一个 hook 函数
void *bh_hub_get_prev_func(void *func) {
// 从线程栈中获取快照的 proxy 列表
bh_hub_frame_t *frame = &stack->frames[stack->frames_cnt - 1];

bool found = false;
bh_hub_proxy_t *proxy;
SLIST_FOREACH(proxy, &(frame->proxies), link) { // ← 遍历快照
if (!found) {
if (proxy->func == func) found = true;
} else {
if (proxy->enabled) break; // ← 普通读取
}
}
return proxy ? proxy->func : (void *)frame->orig_addr;
}

答案:这是一个精心设计的无锁并发模型,结合了多种技术:

1. 快照机制(Snapshot)是核心

最关键的设计是这行代码:

1
2
// 在 bh_hub_push_stack 中
frame->proxies = self->proxies; // 复制链表头指针(快照)

这意味着什么?

  • 当线程进入跳板时,会将当前时刻的 proxy 链表头指针保存到线程私有栈中
  • 后续的 bh_hub_get_prev_func 使用的是快照,而不是全局的 self->proxies
  • 即使其他线程修改了全局链表,当前线程看到的仍然是进入时的快照
2. 内存屏障的作用

虽然读取 proxy->enabled 是普通读取,但写入时使用了严格的内存屏障:

1
__atomic_store_n(&proxy->enabled, false, __ATOMIC_SEQ_CST);

__ATOMIC_SEQ_CST 提供的保证:

  • 全局顺序一致性:所有线程看到的原子操作顺序是一致的
  • 完全内存屏障:防止编译器和 CPU 重排序
  • 立即可见性:修改会立即同步到其他 CPU 的缓存

为什么读取可以是普通读取?

关键在于这几点:

  1. 读取位置不同

    • bh_hub_push_stack 读取 self->proxies(全局链表)
    • bh_hub_get_prev_func 读取 frame->proxies(线程私有快照)
  2. 容忍旧值

    • bh_hub_push_stack 中,即使读到旧的 proxy->enabled 值,也是安全的
    • 最坏情况:读到 enabled=true(已经被禁用),多执行一次 hook
    • 或者读到 enabled=false(已经被启用),跳过这次 hook
    • 这两种情况都不会导致崩溃或数据损坏
  3. 数据依赖保证

    • 链表节点的插入使用了 __ATOMIC_RELEASE
    • 这保证了 proxy 结构体的所有字段(包括 func、enabled)在插入前都是完整的
3. 完整的并发控制总结
1
2
3
4
5
6
7
8
9
写操作(在 mutex 保护下):
├─ 修改 proxy->enabled: __ATOMIC_SEQ_CST
├─ 插入链表节点: __ATOMIC_RELEASE
└─ 其他操作: mutex 保护

读操作(无锁):
├─ 复制链表头: 普通读取(利用数据依赖)
├─ 读取 proxy->enabled: 普通读取(容忍旧值)
└─ 遍历快照链表: 线程私有数据,无需同步

内存屏障层次

  1. SEQ_CST(最强):enabled 的修改,保证全局顺序一致性
  2. RELEASE(中等):链表节点插入,保证数据完整性
  3. 普通读取(最弱):快照和遍历,依赖上层保证
4. 对比:如果使用原子读取会怎样?

假设改成:

1
2
3
if (__atomic_load_n(&proxy->enabled, __ATOMIC_ACQUIRE)) {
// ...
}

性能影响

  • 每次读取都需要内存屏障
  • 在 ARM64 上是 dmb ish 指令,有数十个时钟周期的开销
  • 对于频繁调用的 hook 点,性能损失明显

收益

  • 更早看到 enabled 的变化
  • 但由于快照机制,这个”更早”没有实际意义

结论:没有必要,反而降低性能。

3. Native 崩溃兜底

bhook 在多处使用信号处理机制来捕获可能的崩溃:

1
2
3
#define BH_SIG_TRY(...)   bytesig_try(__VA_ARGS__)
#define BH_SIG_CATCH() bytesig_catch()
#define BH_SIG_EXIT bytesig_exit()

应用场景

  1. 解析 ELF 时:防止读取损坏的 ELF 文件导致 SIGSEGV
  2. 填充跳板代码时:防止写入不可写内存导致 SIGBUS
  3. 修改 GOT 表时:防止内存保护属性问题

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册信号处理器
int bytesig_init(int signum);

// 使用 sigsetjmp/siglongjmp 实现异常捕获
BH_SIG_TRY(SIGSEGV, SIGBUS) {
// 可能崩溃的代码
memcpy((void *)self->trampo, bh_hub_trampo_template_start(), code_size);
}
BH_SIG_CATCH() {
// 崩溃后的恢复逻辑
bh_trampo_free(&bh_hub_trampo_mgr, self->trampo);
free(self);
return NULL;
}
BH_SIG_EXIT

当信号发生时,信号处理器会通过 siglongjmp 跳转到 BH_SIG_CATCH 块,从而避免崩溃。

优点

  • 提高稳定性,防止整个进程崩溃
  • 可以记录错误信息,方便调试
  • 标记 ELF 为 error 状态,避免反复尝试

4. 延迟销毁机制(Delayed Destruction)

延迟销毁是 bhook 保证线程安全的关键机制之一。让我们详细分析它的实现和潜在问题(老版本我印象中应该是没有做销毁只标记关闭)。

延迟销毁的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#define BH_HUB_DELAY_SEC 10  // 延迟 10 秒

// 延迟销毁队列
static bh_hub_list_t bh_hub_delayed_destroy;
static pthread_mutex_t bh_hub_delayed_destroy_lock;

void bh_hub_destroy(bh_hub_t *self, bool with_delay) {
struct timeval now;
gettimeofday(&now, NULL);

// 1. 清理过期的延迟销毁项
if (!LIST_EMPTY(&bh_hub_delayed_destroy)) {
pthread_mutex_lock(&bh_hub_delayed_destroy_lock);
bh_hub_t *hub, *hub_tmp;
LIST_FOREACH_SAFE(hub, &bh_hub_delayed_destroy, link, hub_tmp) {
if (now.tv_sec - hub->destroy_ts > BH_HUB_DELAY_SEC) { // 超过 10 秒
LIST_REMOVE(hub, link);
bh_hub_destroy_inner(hub); // 真正释放内存
}
}
pthread_mutex_unlock(&bh_hub_delayed_destroy_lock);
}

// 2. 根据参数决定立即销毁还是延迟销毁
if (with_delay) {
self->destroy_ts = now.tv_sec; // 记录销毁时间戳
bh_trampo_free(&bh_hub_trampo_mgr, self->trampo); // 立即释放跳板槽
self->trampo = 0;

pthread_mutex_lock(&bh_hub_delayed_destroy_lock);
LIST_INSERT_HEAD(&bh_hub_delayed_destroy, self, link); // 加入延迟队列
pthread_mutex_unlock(&bh_hub_delayed_destroy_lock);
} else {
bh_hub_destroy_inner(self); // 立即销毁
}
}

// 真正的销毁函数
static void bh_hub_destroy_inner(bh_hub_t *self) {
pthread_mutex_destroy(&self->proxies_lock);

if (0 != self->trampo) bh_trampo_free(&bh_hub_trampo_mgr, self->trampo);

// 释放所有 proxy
while (!SLIST_EMPTY(&self->proxies)) {
bh_hub_proxy_t *proxy = SLIST_FIRST(&self->proxies);
SLIST_REMOVE_HEAD(&self->proxies, link);
free(proxy); // ← 这里释放 proxy 结构体
}

free(self); // 释放 hub 结构体
}

延迟销毁的目的

延迟销毁主要解决以下问题:

场景:线程 A 正在执行 hook 函数,线程 B unhook

1
2
3
4
5
6
时间轴:
T1: 线程 A 进入跳板 -> frame->proxies = hub->proxies (保存快照)
T2: 线程 A 执行第一个 proxy 的 hook 函数
T3: 线程 B 执行 unhook -> bh_hub_del_proxy() -> proxy->enabled = false
T4: 线程 A 执行 BYTEHOOK_CALL_PREV() -> 需要遍历 frame->proxies 链表
T5: 如果 proxy 被立即 free(),线程 A 会访问已释放内存 ❌

延迟销毁的保护

  • proxy 只是标记为 enabled = false,但不立即 free()
  • proxy 结构体和链表在延迟期间(10 秒)保持有效
  • 线程 A 可以安全地遍历链表,即使 proxy 已被禁用

两层延迟机制

bhook 实际上有两层延迟销毁:

  1. 跳板延迟释放(5 秒):

    1
    2
    3
    4
    5
    6
    7
    #define BH_HUB_TRAMPO_DELAY_SEC 5

    // 在 bh_trampo_alloc 中
    if (mgr->delay_sec > 0 &&
    (now.tv_sec <= page->timestamps[i] ||
    now.tv_sec - page->timestamps[i] <= mgr->delay_sec))
    continue; // 跳过最近释放的槽
  2. Hub/Proxy 延迟销毁(10 秒):

    1
    2
    3
    4
    5
    6
    #define BH_HUB_DELAY_SEC 10

    // 10 秒后才真正释放 hub 和 proxy 内存
    if (now.tv_sec - hub->destroy_ts > BH_HUB_DELAY_SEC) {
    bh_hub_destroy_inner(hub);
    }

延迟销毁的潜在问题

虽然延迟销毁大大提高了安全性,但并不是 100% 没有问题

问题 1:长时间运行的 Hook 函数

1
2
3
4
5
6
7
8
9
10
// 假设有这样一个 hook 函数
void *my_hook_malloc(size_t size) {
void *ptr = BYTEHOOK_CALL_PREV(my_hook_malloc, size);

// 执行一些耗时操作
sleep(15); // ← 超过 10 秒的延迟时间!

log_allocation(ptr, size);
return ptr;
}

风险

  • T0: 线程 A 进入 hook,保存 frame->proxies 快照
  • T5: 线程 B unhook,proxy 进入延迟销毁队列
  • T15: 10 秒后,其他线程调用 unhook,触发清理过期项
  • T15.1: bh_hub_destroy_inner() 释放 proxy ❌
  • T16: 线程 A 仍在 sleep,栈帧中的 frame->proxies 指向已释放内存
  • T20: 线程 A 从 sleep 返回,继续执行…访问野指针!💥

问题 2:快照中的 Proxy 指针失效

1
2
3
4
5
typedef struct {
bh_hub_proxy_list_t proxies; // ← 这只是链表头指针
uintptr_t orig_addr;
void *return_address;
} bh_hub_frame_t;

关键点:

  • frame->proxies 只是复制了链表头指针,不是深拷贝整个链表
  • 链表中的 proxy 节点仍然是共享的
  • 如果 proxyfree(),所有持有该指针的快照都失效

问题 3:内存泄漏风险

1
2
3
4
5
6
void bh_hub_destroy(bh_hub_t *self, bool with_delay) {
// ...
if (with_delay) {
LIST_INSERT_HEAD(&bh_hub_delayed_destroy, self, link);
}
}

风险

  • 如果程序快速退出(没有再次调用 unhook 触发清理)
  • 延迟队列中的 hub 和 proxy 可能永远不会被释放
  • 造成内存泄漏(虽然进程退出后 OS 会回收)

问题 4:清理时机不确定

1
2
3
4
5
6
void bh_hub_destroy(bh_hub_t *self, bool with_delay) {
// 只有在下次 unhook 时才清理过期项
if (!LIST_EMPTY(&bh_hub_delayed_destroy)) {
// ...
}
}

问题

  • 没有独立的清理线程
  • 依赖下次 unhook 操作触发清理
  • 如果长时间没有 unhook,内存会一直占用

改进方案

虽然有这些潜在问题,但在实际使用中出现的概率很低,因为:

  1. Hook 函数通常很快:很少有 hook 函数会执行超过 10 秒
  2. 保守的延迟时间:10 秒对大多数场景足够了
  3. 快照保护:即使 proxy 被禁用,在延迟期内仍然可以安全访问

如果要做得更安全,可以考虑

  1. 引用计数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    typedef struct bh_hub_proxy {
    void *func;
    bool enabled;
    atomic_int ref_count; // ← 新增引用计数
    SLIST_ENTRY(bh_hub_proxy, ) link;
    } bh_hub_proxy_t;

    // push_stack 时增加引用
    SLIST_FOREACH(proxy, &self->proxies, link) {
    if (proxy->enabled) {
    __atomic_fetch_add(&proxy->ref_count, 1, __ATOMIC_ACQUIRE);
    // ...
    }
    }

    // pop_stack 时减少引用
    __atomic_fetch_sub(&proxy->ref_count, 1, __ATOMIC_RELEASE);

    // 销毁时检查引用计数
    if (__atomic_load_n(&proxy->ref_count, __ATOMIC_ACQUIRE) == 0) {
    free(proxy);
    }
  2. 后台清理线程

    1
    2
    3
    4
    5
    6
    7
    // 定期清理延迟队列,不依赖 unhook 触发
    void *cleanup_thread(void *arg) {
    while (running) {
    sleep(5);
    bh_hub_cleanup_delayed_destroy();
    }
    }
  3. 更长的延迟时间

    1
    #define BH_HUB_DELAY_SEC 30  // 从 10 秒增加到 30 秒

实际评估

在 bhook 的当前实现中,延迟销毁机制是否足够?

对于绝大多数场景是安全的

  • Hook 函数通常执行时间在毫秒级
  • 10 秒的延迟时间提供了足够的安全边界
  • 实际生产环境中很少出现问题

⚠️ 但不是 100% 无懈可击

  • 理论上存在极端情况(长时间运行的 hook + 精确的时序)
  • 这是性能和安全性的权衡
  • 完全消除风险需要更复杂的机制(如引用计数),会牺牲性能

bhook 的设计哲学

  • 优先保证热路径(hook 执行)的性能
  • 使用保守的延迟时间覆盖 99.9% 的场景
  • 接受极小概率的边界情况风险
  • 这是工程上的务实选择

完整的内存安全保障

bhook 通过多层机制保障内存安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第一层:快照隔离
↓ 线程看到的是进入时的链表状态

第二层:标记删除
↓ proxy->enabled = false,不立即 free()

第三层:延迟销毁(10秒)
↓ 等待足够长时间,确保没有线程在使用

第四层:跳板延迟释放(5秒)
↓ 跳板槽也不立即复用

第五层:信号捕获
↓ 即使出现野指针访问,也尝试捕获崩溃

这是一个多层防御的设计,每一层都降低了出问题的概率。

5. Init/Fini Hook(需要 Inline Hook 支持)

问题背景

传统的 PLT Hook 只能 hook 已加载的 so。如果一个 so 在 hook 之后才加载,就无法 hook 它。

更严重的问题是:无法 hook .init_array 中的函数调用

什么是 .init_array?

.init_array 是 ELF 文件中的一个特殊段,包含一组函数指针,这些函数会在 so 加载后、执行任何其他代码之前被自动调用。这是 C++ 全局对象构造函数、__attribute__((constructor)) 函数的实现机制。

典型场景

1
2
3
4
5
6
7
// libfoo.so 的代码
__attribute__((constructor))
static void init_function(void) {
// 这个函数会在 dlopen("libfoo.so") 后立即执行
void *ptr = malloc(1024); // ← 想要 hook 这个 malloc 调用
// ...
}

问题

1
2
3
4
5
6
7
8
9
10
时间轴:
T1: 应用启动,bhook 初始化
T2: bhook 对已加载的 so 执行 hook
T3: dlopen("libfoo.so") 开始加载
T4: linker 重定位 libfoo.so 的 GOT 表
T5: linker 执行 .init_array 中的构造函数 ← malloc() 被调用
T6: dlopen() 返回
T7: bhook 检测到新 so 加载,尝试 hook...

问题:T5 时刻调用的 malloc() 已经错过了!

传统 PLT Hook 的局限:

  • 只能在 dlopen 返回后才能检测到新 so
  • 此时 .init_array 已经执行完毕
  • 构造函数中的调用无法被 hook

为什么需要 Inline Hook?

bhook 通过 inline hook linker 内部函数来解决这个问题。

核心思路:在 .init_array 执行之前插入 hook 逻辑。

实现原理
  1. Hook linker 的关键函数

bhook 使用 shadowhook(inline hook 库)hook 了 linker 中负责调用构造函数的内部函数:

1
2
3
// 在不同 Android 版本中,函数名可能不同,例如:
// Android 5.0+: soinfo::call_constructors()
// 这个函数负责调用 .init_array 中的所有函数
  1. 注册回调
1
2
3
4
5
6
7
8
9
10
11
#if BH_LINKER_MAYBE_SUPPORT_DL_INIT_FINI_MONITOR
static int bh_task_manager_init_dl_init_fini_monitor(void) {
bh_elf_manager_load();

// 注册 dl_init 前置回调(在 .init_array 执行前)
shadowhook_register_dl_init_callback(bh_task_manager_dl_init_pre, NULL, NULL);

// 注册 dl_fini 后置回调(在 .fini_array 执行后)
shadowhook_register_dl_fini_callback(NULL, bh_task_manager_dl_fini_post, NULL);
}
#endif
  1. 前置回调:抢在 .init_array 执行前 hook
1
2
3
4
5
6
7
8
9
10
11
12
13
static void bh_task_manager_dl_init_pre(struct dl_phdr_info *info, size_t size, void *data) {
BH_LOG_INFO("task manager: dl_init, load_bias %" PRIxPTR ", %s",
(uintptr_t)info->dlpi_addr, info->dlpi_name);

// 1. 将新加载的 so 加入 ELF 管理器
bh_elf_t *elf = bh_elf_manager_add(info);

if (NULL != elf) {
// 2. 立即对这个 so 执行所有待处理的 hook 任务
bh_task_manager_hook_elf(elf); // ← 关键!在这里完成 hook
bh_elf_decrement_ref_count(elf);
}
}
  1. 完整的执行流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
dlopen("libfoo.so") 被调用

linker 加载并重定位 libfoo.so

linker 准备调用 soinfo::call_constructors()

【inline hook 拦截】

bhook 的 bh_task_manager_dl_init_pre() 被调用
├─ 解析 libfoo.so 的 ELF 结构
├─ 遍历所有待 hook 的任务
├─ 修改 libfoo.so 的 GOT 表,将 malloc 指向跳板
└─ hook 完成 ✓

继续执行原始的 soinfo::call_constructors()

执行 .init_array 中的构造函数
├─ init_function() 被调用
├─ malloc(1024) 被调用
└─ GOT 表已被修改,跳转到 bhook 的跳板 ✓

bhook 的 hook 函数被执行

.init_array 执行完毕

dlopen() 返回
对比:有无 Inline Hook 的区别

没有 Inline Hook(传统方案)

1
2
3
4
5
6
7
8
9
dlopen("libfoo.so")

.init_array 执行 → malloc() 调用 [未被 hook ❌]

dlopen() 返回

bhook 检测到新 so → 进行 hook

后续的 malloc() 调用才能被 hook [太晚了!]

有 Inline Hook(bhook 方案)

1
2
3
4
5
6
7
dlopen("libfoo.so")

【bhook 的回调先执行】→ 完成 hook ✓

.init_array 执行 → malloc() 调用 [已被 hook ✓]

dlopen() 返回

实际案例分析

参考 GitHub Issue #18(https://github.com/bytedance/bhook/issues/18):

问题场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// libA.so
__attribute__((constructor))
void init_lib_a() {
// 在构造函数中调用 pthread_create
pthread_t tid;
pthread_create(&tid, NULL, worker_thread, NULL); // ← 想 hook 这个调用
}

// 应用代码
int main() {
bytehook_init(BYTEHOOK_MODE_AUTOMATIC);
bytehook_hook_all(NULL, "pthread_create",
(void*)my_pthread_create_hook, NULL, NULL);

dlopen("libA.so", RTLD_NOW); // ← libA.so 的构造函数会立即执行
// 问题:构造函数中的 pthread_create 是否能被 hook?
}

传统 PLT Hook

  • 无法 hook,因为 hook 逻辑在 dlopen 返回后才执行
  • pthread_create 在 dlopen 内部的 .init_array 阶段就被调用了

bhook + shadowhook

  • 可以 hook!
  • 因为 inline hook 让 bhook 在 .init_array 执行前就完成了 GOT 表修改

架构限制

从源码可以看到,bhook 的 init/fini 监控只支持特定架构:

1
2
3
4
5
6
7
8
9
bool bh_linker_is_support_dl_init_fini_monitor(void) {
#if defined(__aarch64__)
return true; // ARM64 支持
#elif defined(__arm__)
return bh_util_get_api_level() >= __ANDROID_API_L__; // ARM 32位需要 Android 5.0+
#else
return false; // x86/x86_64 不支持
#endif
}

原因:

  • ARM/ARM64:指令集规整,inline hook 实现相对容易
  • x86/x86_64:变长指令集,inline hook 复杂度高,暂不支持

后置回调:清理资源

bhook 还注册了 fini 回调,用于 so 卸载时的清理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void bh_task_manager_dl_fini_post(struct dl_phdr_info *info, size_t size, void *data) {
BH_LOG_INFO("task manager: dl_fini, load_bias %" PRIxPTR ", %s",
(uintptr_t)info->dlpi_addr, info->dlpi_name);

// 1. 释放 ELF 信息、switch、hub 等资源
bh_elf_manager_del(info);

// 2. 重置单次 hook 任务的状态,允许再次 hook(如果 so 重新加载)
pthread_rwlock_rdlock(&bh_tasks_lock);
bh_task_t *task;
TAILQ_FOREACH(task, &bh_tasks, link) {
if (BH_TASK_TYPE_SINGLE == task->type &&
info->dlpi_addr == task->caller_load_bias) {
task->caller_load_bias = 0;
task->status = BH_TASK_STATUS_UNFINISHED; // 重置为未完成状态
}
}
pthread_rwlock_unlock(&bh_tasks_lock);
}

这样如果 so 被卸载后重新加载,bhook 可以再次对其进行 hook。

总结:PLT Hook + Inline Hook 的必要性

技术 作用 Hook 的对象
PLT Hook 修改 GOT 表 应用代码中通过 PLT 调用的外部函数
Inline Hook 修改函数入口指令 linker 内部函数(call_constructors 等)

两者配合才能实现:

  1. PLT Hook:hook 应用和库代码中的函数调用
  2. Inline Hook:监控 so 加载/卸载事件,hook .init_array 中的调用

核心价值

  • ✅ 支持延迟加载的 so(dlopen 加载的库)
  • ✅ 支持 .init_array 中的函数调用(C++ 全局对象构造函数)
  • ✅ 支持 .fini_array 中的函数调用(析构函数)
  • ✅ 实现完整的生命周期覆盖

这就是为什么 bhook 新版本引入 shadowhook 的根本原因:单纯的 PLT Hook 无法覆盖 so 初始化阶段的函数调用,必须结合 inline hook 才能实现完整的 hook 能力。

架构设计总结

核心数据结构关系

1
2
3
4
5
6
7
8
9
10
11
bh_task_t (hook任务)

bh_elf_t (ELF文件信息)

bh_hub_t (跳板管理器)
├── trampo (跳板代码地址)
└── proxies (proxy列表)

bh_hub_proxy_t (单个hook代理)
├── func (hook函数)
└── enabled (是否启用)

执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
原始调用

GOT 表(被修改为跳板地址)

跳板代码
├── 保存寄存器
├── 调用 bh_hub_push_stack
├── 恢复寄存器
└── 跳转到 hook 函数

hook 函数1

BYTEHOOK_CALL_PREV()

hook 函数2

BYTEHOOK_CALL_PREV()

原始函数

性能优化要点

  1. 无锁设计:TLS + 快照机制
  2. 内存池:跳板槽复用 + 延迟释放
  3. 哈希加速:优先使用 GNU hash 查找符号
  4. 缓存策略:ELF 信息缓存、栈缓存

稳定性保障

  1. 信号捕获:防止内存访问崩溃
  2. 引用计数:防止 ELF 信息被过早释放
  3. 延迟销毁:跳板和 hub 延迟销毁,防止野指针
  4. 原子操作:关键状态变更使用原子操作

回答开篇问题

1. 线程安全如何保障?

  • TLS 隔离:每个线程有独立的调用栈
  • 快照机制:进入跳板时复制 proxy 列表,后续修改不影响当前执行
  • 原子操作:状态变更(如 proxy enabled 标记)使用原子操作
  • 延迟销毁:unhook 时只标记 proxy 为 disabled,延迟销毁 hub

2. 性能如何保障?

  • 无锁跳板:不需要在热路径上加锁
  • 栈缓存:前 1024 个线程使用预分配缓存,避免频繁 mmap
  • 内存池:跳板槽复用,减少系统调用
  • 哈希查找:使用 GNU hash 快速定位符号

3. 崩溃如何兜底?

  • 信号捕获BH_SIG_TRY/CATCH/EXIT 宏包裹危险操作
  • 错误标记:崩溃的 ELF 标记为 error,避免反复尝试
  • 安全函数:使用 bh_safe_* 系列函数,内部有额外检查
  • 保护属性:修改 GOT 前先检查并临时修改内存保护属性

4. 为什么需要 Inline Hook?

  • 监控 so 加载:需要 hook linker 内部函数(call_constructors)
  • 覆盖全场景:确保延迟加载的 so 也能被 hook
  • 架构完整性:PLT Hook + Inline Hook = 完整的 hook 方案

总结

bhook 是一个工程实践非常优秀的 PLT Hook 框架,其核心亮点包括:

  1. 创新的无锁跳板机制:通过 TLS + 快照解决了传统方案的性能瓶颈
  2. 完善的稳定性保障:信号捕获、延迟销毁、原子操作等多重保障
  3. 精细的内存管理:跳板池、栈缓存、引用计数等优化内存使用
  4. 全面的场景覆盖:支持单个/批量 hook、自动/手动模式、延迟加载 so 监控

从 bhook 的实现中,我们可以学到:

  • 性能优化:无锁设计、内存池、缓存策略
  • 稳定性设计:异常捕获、延迟销毁、状态机管理
  • 跨平台适配:通过宏和条件编译支持多架构
  • 工程化思维:日志、错误码、调试支持、文档完善

参考资料

  1. bhook 官方仓库
  2. bhook 原理文档
  3. ELF 文件格式规范
  4. Android Dynamic Linker 设计文档
  5. Control Flow Integrity (CFI) 介绍
  6. Linux Signal Handling
  7. ARM64 ABI 规范