深入分析优秀的 PLT Hook 框架 bhook 的实现原理与工程实践,与 AI 合作完成。
前言 PLT Hook 在 Android 上是比较成熟稳定的 Hook 技术,其中目前最为健壮的方案主要是字节跳动开源的 bhook 。本文将深入分析其中各种优秀的工程实践,以及我们能够学习借鉴的部分。
在开始之前,让我们带着以下几个核心问题来深入学习:
线程安全 :如何保障线程安全,怎么避免同时 hook/unhook 造成的影响?
性能优化 :怎么保障性能,避免 hook 点成为线程阻塞的瓶颈?
稳定性保障 :如何尽量兜底避免可能的内存异常崩溃?
架构演进 :为什么新版本要依赖 shadowhook inline hook?
基本原理 PLT Hook 的基本原理 bhook 官方文档已经介绍得十分详细,建议先阅读:https://github.com/bytedance/bhook/blob/main/doc/overview.zh-CN.md
ELF 文件结构 后续分析会反复涉及到 ELF 文件中的各种 section 和 segment:
动态链接重定位过程 下图展示了动态链接器进行符号重定位的过程:
PLT Hook 的核心思想就是修改 GOT 表中的函数地址,将其指向我们的 hook 函数。
源码分析 分析版本 :1.1.1commit :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) { for (ElfW(Dyn) *entry = dynamic; entry && entry->d_tag != DT_NULL; entry++) { switch (entry->d_tag) { 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 ; 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 ; 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 ; case DT_SYMTAB: self->dynsym = (ElfW(Sym) *)(self->load_bias + entry->d_un.d_ptr); break ; case DT_STRTAB: self->dynstr = (const char *)(self->load_bias + entry->d_un.d_ptr); break ; 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 ; 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 ; } } 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 ; } else { #ifndef __riscv if (BH_ELF_R_GLOB_DAT != r_type && BH_ELF_R_ABS != r_type) return 0 ; #endif } if (NULL == *sym) { if (0 == strcmp (self->dynstr + self->dynsym[r_sym].st_name, sym_name)) { *sym = &self->dynsym[r_sym]; if (NULL != callee_addr && *((void **)got_addr) != callee_addr) return -1 ; if (0 != bh_array_push(gots, (uintptr_t )got_addr)) return -1 ; if (0 != bh_array_push(prots, (uintptr_t )bh_elf_get_protect(self, got_addr))) return -1 ; } } else { if (&self->dynsym[r_sym] == *sym) { if (NULL != callee_addr && *((void **)got_addr) != callee_addr) return -1 ; if (0 != bh_array_push(gots, (uintptr_t )got_addr)) return -1 ; if (0 != bh_array_push(prots, (uintptr_t )bh_elf_get_protect(self, got_addr))) return -1 ; } } return 0 ; }
这里的关键逻辑是:
检查重定位类型是否是 PLT 跳转或全局数据引用
匹配符号名
验证 GOT 地址内容是否指向预期的被调用者地址
收集所有匹配的 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 ; if (0 == (self->trampo = bh_trampo_alloc(&bh_hub_trampo_mgr))) { free (self); return NULL ; } 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 void **data = (void **)(self->trampo + code_size); *data++ = (void *)bh_hub_push_stack; *data = (void *)self; 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 使用巧妙的内存池管理机制:
Bitmap 标记 :使用位图标记每个跳板槽的使用状态
按需分配 :首次使用时 mmap 一整页内存,然后分割成多个跳板槽
延迟释放 :释放的槽不会立即复用,而是延迟一段时间(默认 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); 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)) { if (mgr->delay_sec > 0 && (now.tv_sec <= page->timestamps[i] || now.tv_sec - page->timestamps[i] <= mgr->delay_sec)) continue ; page->flags[flags_idx] |= mask; trampo = page->ptr + (mgr->trampo_size * i); memset ((void *)trampo, 0 , mgr->trampo_size); goto end; } } } 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; 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); 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 ; }
跳板代码模板 跳板代码需要完成以下任务:
保存所有寄存器状态(参数寄存器、浮点寄存器、链接寄存器)
调用 bh_hub_push_stack 获取实际要执行的 hook 函数地址
恢复寄存器状态
跳转到 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__( "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" "ldr x0, hub_ptr \n" "mov x1, lr \n" "ldr x16, push_stack \n" "blr x16 \n" "mov x16, x0 \n" "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" "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 检查。
实现原理 :
找到 linker 中的 __cfi_slowpath 符号
修改其函数入口,使其直接返回而不执行检查
针对不同架构使用不同的返回指令(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; uintptr_t orig_addr; void *return_address; } bh_hub_frame_t ;
核心流程
进入跳板 :跳板代码调用 bh_hub_push_stack(hub, return_address)
获取栈 :从 TLS 或全局缓存中获取线程私有栈
复制 proxy 列表 :将当前 hub 的 proxy 列表快照复制到栈帧中(这是关键!)
返回 hook 函数 :返回第一个 enabled 的 proxy 的函数地址
链式调用 :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 int bh_hub_add_proxy (bh_hub_t *self, uintptr_t proxy_func) { pthread_mutex_lock(&self->proxies_lock); if (proxy->func == (void *)proxy_func) { if (!proxy->enabled) __atomic_store_n((bool *)&proxy->enabled, true , __ATOMIC_SEQ_CST); goto end; } SLIST_NEXT(proxy, link) = SLIST_FIRST(&self->proxies); __atomic_store_n((uintptr_t *)(&SLIST_FIRST(&self->proxies)), (uintptr_t )proxy, __ATOMIC_RELEASE); pthread_mutex_unlock(&self->proxies_lock); } 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); deleted = true ; } } pthread_mutex_unlock(&self->proxies_lock); } static void *bh_hub_push_stack (bh_hub_t *self, void *return_address) { bh_hub_proxy_t *proxy; SLIST_FOREACH(proxy, &self->proxies, link) { if (proxy->enabled) { frame->proxies = self->proxies; return proxy->func; } } } void *bh_hub_get_prev_func (void *func) { 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 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 的缓存
为什么读取可以是普通读取?
关键在于这几点:
读取位置不同 :
bh_hub_push_stack 读取 self->proxies(全局链表)
bh_hub_get_prev_func 读取 frame->proxies(线程私有快照)
容忍旧值 :
在 bh_hub_push_stack 中,即使读到旧的 proxy->enabled 值,也是安全的
最坏情况:读到 enabled=true(已经被禁用),多执行一次 hook
或者读到 enabled=false(已经被启用),跳过这次 hook
这两种情况都不会导致崩溃或数据损坏
数据依赖保证 :
链表节点的插入使用了 __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: 普通读取(容忍旧值) └─ 遍历快照链表: 线程私有数据,无需同步
内存屏障层次 :
SEQ_CST (最强):enabled 的修改,保证全局顺序一致性
RELEASE (中等):链表节点插入,保证数据完整性
普通读取 (最弱):快照和遍历,依赖上层保证
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()
应用场景 :
解析 ELF 时 :防止读取损坏的 ELF 文件导致 SIGSEGV
填充跳板代码时 :防止写入不可写内存导致 SIGBUS
修改 GOT 表时 :防止内存保护属性问题
实现原理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int bytesig_init (int signum) ;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 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 ); 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) { LIST_REMOVE(hub, link); bh_hub_destroy_inner(hub); } } pthread_mutex_unlock(&bh_hub_delayed_destroy_lock); } 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); while (!SLIST_EMPTY(&self->proxies)) { bh_hub_proxy_t *proxy = SLIST_FIRST(&self->proxies); SLIST_REMOVE_HEAD(&self->proxies, link); free (proxy); } free (self); }
延迟销毁的目的 延迟销毁主要解决以下问题:
场景:线程 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 实际上有两层延迟销毁:
跳板延迟释放 (5 秒):
1 2 3 4 5 6 7 #define BH_HUB_TRAMPO_DELAY_SEC 5 if (mgr->delay_sec > 0 && (now.tv_sec <= page->timestamps[i] || now.tv_sec - page->timestamps[i] <= mgr->delay_sec)) continue ;
Hub/Proxy 延迟销毁 (10 秒):
1 2 3 4 5 6 #define BH_HUB_DELAY_SEC 10 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 void *my_hook_malloc (size_t size) { void *ptr = BYTEHOOK_CALL_PREV(my_hook_malloc, size); sleep(15 ); 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 节点仍然是共享的
如果 proxy 被 free(),所有持有该指针的快照都失效
问题 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) { if (!LIST_EMPTY(&bh_hub_delayed_destroy)) { } }
问题 :
没有独立的清理线程
依赖下次 unhook 操作触发清理
如果长时间没有 unhook,内存会一直占用
改进方案 虽然有这些潜在问题,但在实际使用中出现的概率很低,因为:
Hook 函数通常很快 :很少有 hook 函数会执行超过 10 秒
保守的延迟时间 :10 秒对大多数场景足够了
快照保护 :即使 proxy 被禁用,在延迟期内仍然可以安全访问
如果要做得更安全,可以考虑 :
引用计数 :
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 ; SLIST_FOREACH(proxy, &self->proxies, link) { if (proxy->enabled) { __atomic_fetch_add(&proxy->ref_count, 1 , __ATOMIC_ACQUIRE); } } __atomic_fetch_sub(&proxy->ref_count, 1 , __ATOMIC_RELEASE); if (__atomic_load_n(&proxy->ref_count, __ATOMIC_ACQUIRE) == 0 ) { free (proxy); }
后台清理线程 :
1 2 3 4 5 6 7 void *cleanup_thread (void *arg) { while (running) { sleep(5 ); bh_hub_cleanup_delayed_destroy(); } }
更长的延迟时间 :
1 #define BH_HUB_DELAY_SEC 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 __attribute__((constructor)) static void init_function (void ) { void *ptr = malloc (1024 ); }
问题 :
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 逻辑。
实现原理
Hook linker 的关键函数
bhook 使用 shadowhook(inline hook 库)hook 了 linker 中负责调用构造函数的内部函数:
注册回调
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(); shadowhook_register_dl_init_callback(bh_task_manager_dl_init_pre, NULL , NULL ); shadowhook_register_dl_fini_callback(NULL , bh_task_manager_dl_fini_post, NULL ); } #endif
前置回调:抢在 .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); bh_elf_t *elf = bh_elf_manager_add(info); if (NULL != elf) { bh_task_manager_hook_elf(elf); bh_elf_decrement_ref_count(elf); } }
完整的执行流程
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 __attribute__((constructor)) void init_lib_a () { pthread_t tid; pthread_create (&tid, NULL , worker_thread, NULL ); } 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); }
传统 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 ; #elif defined(__arm__) return bh_util_get_api_level() >= __ANDROID_API_L__; #else return false ; #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); bh_elf_manager_del(info); 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 等)
两者配合才能实现:
PLT Hook :hook 应用和库代码中的函数调用
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() ↓ 原始函数
性能优化要点
无锁设计 :TLS + 快照机制
内存池 :跳板槽复用 + 延迟释放
哈希加速 :优先使用 GNU hash 查找符号
缓存策略 :ELF 信息缓存、栈缓存
稳定性保障
信号捕获 :防止内存访问崩溃
引用计数 :防止 ELF 信息被过早释放
延迟销毁 :跳板和 hub 延迟销毁,防止野指针
原子操作 :关键状态变更使用原子操作
回答开篇问题 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 框架,其核心亮点包括:
创新的无锁跳板机制 :通过 TLS + 快照解决了传统方案的性能瓶颈
完善的稳定性保障 :信号捕获、延迟销毁、原子操作等多重保障
精细的内存管理 :跳板池、栈缓存、引用计数等优化内存使用
全面的场景覆盖 :支持单个/批量 hook、自动/手动模式、延迟加载 so 监控
从 bhook 的实现中,我们可以学到:
性能优化 :无锁设计、内存池、缓存策略
稳定性设计 :异常捕获、延迟销毁、状态机管理
跨平台适配 :通过宏和条件编译支持多架构
工程化思维 :日志、错误码、调试支持、文档完善
参考资料
bhook 官方仓库
bhook 原理文档
ELF 文件格式规范
Android Dynamic Linker 设计文档
Control Flow Integrity (CFI) 介绍
Linux Signal Handling
ARM64 ABI 规范