Android码农C语言基础补漏(1)观ByteHook源码

238 阅读17分钟

最近几周浏览一下ByteHook源码,只是先从语法上看看,大概记录一下。

记录自己没留意过或者不了解的API,补充自己C语言的一些基础。

一些涉及比较大的内容先略过了。下文内容不确保准确性,欢迎指正。

bh_cfi.c

Control flow integrity (CFI): 控制流完整性

这个C文件里主要是Arm64架构androidO以上版本把libdl.so__cfi_slowpath__cfi_slowpath_diag两个函数的地址(他们的第一个指令)写入RET( #define BH_CFI_ARM64_RET_INST 0xd65f03c0)指令。

CFI具体作用以后再看,大概就是用来防止通过修改内存中的函数调用顺序进行攻击。

(issue里的回复: )...目前 bytehook 对 CFI 的处理只针对了 64 位设备。根据我们之前的数据观察,可能是因为开启 CFI 需要消耗比较多的内存,所以在线上的移动设备中,只发现厂商在部分 64 位设备上会开启 CFI。

Control Flow Integrity — Clang 20.0.0git documentation (llvm.org)

bh_core.c

  • 在函数内使用static关键字声明的变量, 初始化操作只会在第一次调用该函数时执行。

  • 直接使用PTHREAD_MUTEX_INITIALIZER初始化互斥锁(刚注意到,很多项目都在这样配合函数内static去用...)。

  • 原子操作__atomic_store_n(&bh_core.init_status, status, __ATOMIC_SEQ_CST);(//TODO 内存模型以后再看。)

int bh_core_init(int mode, bool debug) {
  // ...
  static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  pthread_mutex_lock(&lock);
  // ...
  pthread_mutex_unlock(&lock);
  // ...
}

bh_dl.c

这里主要是通过例如/system/bin/linker64的文件获取linker的一些.symtab.strtab等。

Auxiliary Vector:

extern __attribute((weak)) unsigned long int getauxval(unsigned long int);

其实有对应头文件#include <sys/auxv.h>,这里用extern,难道是虽然头文件有判断版本,但实际上存在API18以上的设备仍没有getauxval函数?(或许是以前版本NDK没这个函数)

#if __ANDROID_API__ >= 18
unsigned long int getauxval(unsigned long int __type) __INTRODUCED_IN(18);
#endif /* __ANDROID_API__ >= 18 */

代码中只传过AT_BASE参数, 表示获取动态链接器的基址。当这个函数不可用的时候,解析maps文件获取基址。

getauxval(3) - Linux manual page (man7.org)

ElfW

link.h的ElfW宏就可以用来适配64/32位了(而不必自己写):

#if defined(__LP64__)
#define ElfW(type) Elf64_ ## type
#else
#define ElfW(type) Elf32_ ## type
#endif

比如:

ElfW(Shdr) *shdrs = NULL;
// 展开为 
// Elf64_shdr *shdrs = NULL;  
// 或 
// Elf32_shdr *shdrs = NULL;

ANDROID_API

#include <android/api-level.h>这个头文件有关于Android版本的宏,比如__ANDROID_API_T__

__ANDROID_API__的意义相当于minSdkVersion,在构建时传入。与运行时的设备版本没什么关系。

bh_dl_iterate.c

这个文件是兼容低于Android6的dl_iterate_phdr实现。


以前没注意过int dl_iterate_phdr(int (*)(struct dl_phdr_info *, size_t, void *), void *);-的参数二。实际上就是每次参数一被调用时,这个参数二会被传递给参数一的回调函数的参数三。


if (3 != sscanf(line, "%" SCNxPTR "-%*" SCNxPTR " r%*c%cp %" SCNxPTR " ", &base, &exec, &offset))

sscanf返回值表示成功读取了多少个值。

没注意过的宏SCNxPTR,出自#include <inttypes.h>

//...
#ifdef __LP64__
#define __PRI_64_prefix  "l"
#define __PRI_PTR_prefix "l"
#else
#define __PRI_64_prefix "ll"
#define __PRI_PTR_prefix
#endif
//...
#define SCNxPTR          __PRI_PTR_prefix"x"       /* uintptr_t */

bh_dl_monitor.c

TAILQ(tail queue),实际作用就是双向链表,这里用的是额外携带的bsd/queue.h,而不是ndk目录的sys/queue.h(大概看了一下这个两个头文件内容差不多)以后再具体去尝试使用TAILQ


读写锁pthread_rwlock_t,这里主要是为了确保TAILQ线程安全。

static pthread_rwlock_t bh_dl_monitor_cbs_lock = PTHREAD_RWLOCK_INITIALIZER;
// ..
pthread_rwlock_rdlock(&bh_dl_monitor_cbs_lock);
// ..
pthread_rwlock_unlock(&bh_dl_monitor_cbs_lock);
// ..

pthread_key_create初始化声明一个线程私有的存储空间,参数二用于释放这个空间。

static pthread_key_t bh_dl_monitor_dlerror_msg_tls_key;
// ..
char *errmsg_tls = (char *)pthread_getspecific(bh_dl_monitor_dlerror_msg_tls_key);
// ..
pthread_setspecific(bh_dl_monitor_dlerror_msg_tls_key, (void *)errmsg_tls);
// ..
pthread_key_create(&bh_dl_monitor_dlerror_msg_tls_key, bh_dl_monitor_dlerror_msg_tls_dtor)

__thread是GCC对C语言扩展的关键字)


内联汇编,获取android bionic内部的tls数组,从而得到dlerror(往dlerror写入内容)。

#define LIBC_TLS_SLOT_DLERROR 6
// ...
void **libc_tls = NULL;
__asm__("mrs %0, tpidr_el0" : "=r"(libc_tls));
((char **)libc_tls)[LIBC_TLS_SLOT_DLERROR] = errmsg;

内嵌汇编语法如下:
asm(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。

输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串(约束字符)和C 语言变量组成每个输出操作数的限定字符串必须包含“=”表示他是个输出操作数

更多内容看文末参考链接。


获取当前函数返回地址(哪个地址调用的当前函数)

(uintptr_t)(__builtin_return_address(0))

bh_elf.c

这个文件有各种解析elf文件代码工具方法,包括bh_elf_sysv_hashbh_elf_gnu_hash.gnu.hash只包含导出函数),未来有需要了再来这里复制...

elf.h这个头文件(<elf.h>不是<linux/elf.h>)已经有各种便于解析elf文件的宏。

#define R_AARCH64_GLOB_DAT              1025    /* Create GOT entry.  */
#define R_AARCH64_JUMP_SLOT             1026    /* Create PLT entry.  */
//...
#define ELF32_R_SYM(x) ((x) >> 8)
#define ELF32_R_TYPE(x) ((x) & 0xff)
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
//...
#define ELF_ST_TYPE(x) ((x) & 0xf)
#define ELF_ST_INFO(b,t) (((b) << 4) + ((t) & 0xf))

SLEB128(Signed Little Endian Base 128)编码:

每个 LEB128 编码的值 由 1-5 个字节组成,合在一起表示一个 32 位的值。每个字节的有效位只有7位,最高位作为标记:最后一个字节的最高位设为0,其余设为1 。 SLEB128 对最后一个字节的最高位进行了符号扩展(高位全补1,ULEB128是高位全补0),即作为符号位。

可参考《OPPO内核工匠》的一张图(由于他发表在的某博客平台体验非常糟糕,文末不贴链接了):

对于数字-123456,二进制形式 11111111 11111110 00011101 11000000

以7bit分组 1111 1111111 1111000 0111011 1000000

对二进制数字进行编码 1111 1111111 1111000 0111011 1000000

转换为16进制 0x78 0xBB 0xC0,小端序列 0xC0 0xBB 0x78

bh_elf_manager.c

这个文件用到了红黑树相关数据结构(tree.h),从注释来看($FreeBSD: stable/9/sys/sys/tree.h 189204 2009-03-01 04:57:23Z bms $)应该是出自FreeBSD,2020年8月后它的tree.h已经改为rank-balanced树(具体看文末的参考链接)。

typedef struct bh_elf {
  // ...
  const char *pathname;
  // ...
  RB_ENTRY(bh_elf) link;
  // ...
} bh_elf_t;

// ...

static __inline__ int bh_elf_cmp(bh_elf_t *a, bh_elf_t *b) {
  return strcmp(a->pathname, b->pathname);
}
typedef RB_HEAD(bh_elf_tree, bh_elf) bh_elf_tree_t;
RB_GENERATE_STATIC(bh_elf_tree, bh_elf, link, bh_elf_cmp)

// ...

struct bh_elf_manager {
  // ...
  bh_elf_tree_t elfs;
  // ...
};

typedef struct bh_elf_manager bh_elf_manager_t;

// ...

bh_elf_manager_t *self;
if (NULL == (self = malloc(sizeof(bh_elf_manager_t)))) return NULL;
// ...
RB_INIT(&self->elfs);

// ...

bh_elf_t elf_key = {.pathname = info->dlpi_name};
bh_elf_t *elf = RB_FIND(bh_elf_tree, &self->elfs, &elf_key);

bh_hook.c

SLIST(Singly-linked List)和TAILQ一样出自sys/queue.h

这里添加节点用了__atomic_store_n(, , __ATOMIC_RELEASE),防止(SLIST_INSERT_HEAD内部的两条指令执行顺序颠倒,或者是__atomic_store_n之前的写操作对其他线程不可见)出现其他线程只能看到链表中的"running"这个节点的情况。

#define SLIST_INSERT_HEAD(head, elm, field) do {                        \
        SLIST_NEXT((elm), field) = SLIST_FIRST((head));                 \
        SLIST_FIRST((head)) = (elm);                                    \
    } while (0)
// create new item
if (NULL == (running = malloc(sizeof(bh_hook_call_t)))) {
  r = BYTEHOOK_STATUS_CODE_APPEND_TRAMPO;
  goto end;
}
running->func = func;
running->enabled = true;
running->task_id = task_id;

// insert to the head of the running_list
//
// equivalent to: SLIST_INSERT_HEAD(&self->running_list, running, link);
// but: __ATOMIC_RELEASE ensures readers see only fully-constructed item
SLIST_NEXT(running, link) = SLIST_FIRST(&self->running_list);
__atomic_store_n((uintptr_t *)(&SLIST_FIRST(&self->running_list)), (uintptr_t)running, __ATOMIC_RELEASE);

bh_hook_manager.c

从这段代码看到了两个没见过的概念:

  // bypass for ifunc
  if (NULL == info.dli_sname) {
    ElfW(Sym) *sym = bh_elf_find_export_func_symbol_by_symbol_name(callee_elf, task->sym_name);
    if (NULL != sym && STT_GNU_IFUNC == ELF_ST_TYPE(sym->st_info)) {
      BH_LOG_INFO("hook chain: verify bypass ifunc: %s in %s", task->sym_name, info.dli_fname);
      r = 0;
    }
  }
  // bypass for alias-func
  else {
    void *addr = bh_elf_find_export_func_addr_by_symbol_name(callee_elf, info.dli_sname);
    if (NULL != addr && addr == *((void **)got_addr)) {
      BH_LOG_INFO("hook chain: verify bypass alias-func: %s in %s", task->sym_name, info.dli_fname);
      r = 0;
    }
  }

IFUNC(indirect function)函数:

如果函数func有多个版本,为了避免每次调用func时都进行一遍版本选择,可以将函数func的符号类型定义为STT_GNU_IFUNC,并且为func实现相应的函数解析器。之后在elf装载时,会进行函数解析,将选择的版本记录下来,以后每一次调用func都会自动跳转到对应的版本。这就是ifunc。

//todo : 据说bionic的动态链接器会在加载elf的时候直接调用func的函数解析器,或许可以起到和.init_array一样的作用?(利用这个"信息差"做so防护)

alias-func: 可以给函数起个别名,一般和弱符号一起用。比如下面这段代码,就算是弱符号f被覆盖,也可以调用__f这个函数。:

void __f () { /* Do something. */; }
void f () __attribute__ ((weak, alias ("__f")));

bh_linker.c

上来首先能看到一个互斥锁的内部定义:

typedef struct {
  // bits:     name:    description:
  // 15-14     type     mutex type, can be 0 (normal), 1 (recursive), 2 (errorcheck)
  // 13        shared   process-shared flag
  // 12-2      counter  <number of times a thread holding a recursive Non-PI mutex > - 1
  // 1-0       state    lock state, can be 0 (unlocked), 1 (locked contended), 2 (locked uncontended)
  uint16_t state;
#if defined(__LP64__)
  uint16_t pad;
  int owner_tid;
  char reserved[28];
#else
  uint16_t owner_tid;
#endif
} bh_linker_mutex_internal_t __attribute__((aligned(4)));

#define BH_LINKER_MUTEX_IS_UNLOCKED(v) (((v)&0x3) == 0)
#define BH_LINKER_MUTEX_COUNTER(v)     (((v)&0x1FFC) >> 2)

关于pthread_mutex_t这个结构体,头文件并没有透露内部的具体结构,每个字节具体的含义可能因版本而异(android6之后,/bionic/libc/bionic/pthread_mutex.cpp有结构体pthread_mutex_internal_t)。

ndk的头文件pthread_types.h:

typedef struct {
#if defined(__LP64__)
  int32_t __private[10];
#else
  int32_t __private[1];
#endif
} pthread_mutex_t;

然后BHook试探运行时的互斥锁的各个字节含义是否符合bh_linker_mutex_internal_t所"猜测"的那样:

static bool bh_linker_check_lock_compatible_helper(bh_linker_mutex_internal_t *m, bool unlocked,
                                                   uint16_t counter, int owner_tid) {
  return BH_LINKER_MUTEX_IS_UNLOCKED(m->state) == unlocked && BH_LINKER_MUTEX_COUNTER(m->state) == counter &&
         m->owner_tid == owner_tid;
}

static bool bh_linker_check_lock_compatible(void) {
  bool result = true;
  int tid = gettid();
  pthread_mutex_t mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
  bh_linker_mutex_internal_t *m = (bh_linker_mutex_internal_t *)&mutex;

  if (!bh_linker_check_lock_compatible_helper(m, true, 0, 0)) result = false;
  pthread_mutex_lock(&mutex);
  if (!bh_linker_check_lock_compatible_helper(m, false, 0, tid)) result = false;
  pthread_mutex_lock(&mutex);
  if (!bh_linker_check_lock_compatible_helper(m, false, 1, tid)) result = false;
  pthread_mutex_lock(&mutex);
  if (!bh_linker_check_lock_compatible_helper(m, false, 2, tid)) result = false;
  pthread_mutex_unlock(&mutex);
  if (!bh_linker_check_lock_compatible_helper(m, false, 1, tid)) result = false;
  pthread_mutex_unlock(&mutex);
  if (!bh_linker_check_lock_compatible_helper(m, false, 0, tid)) result = false;
  pthread_mutex_unlock(&mutex);
  if (!bh_linker_check_lock_compatible_helper(m, true, 0, 0)) result = false;

  return result;
}

在Android4.x的版本,在栈帧里面找锁,但不知道这里为什么arm以外的架构就直接创建一个线程私有空间,而arm架构在栈帧里找,找不到就直接算是初始化失败?

#if __ANDROID_API__ < __ANDROID_API_L__
static int bh_linker_init_android_4x(void) {
#if defined(__arm__)
  return NULL == (bh_linker_g_dl_mutex = bh_linker_mutex_get_by_stack()) ? -1 : 0;
#else
  return 0 != pthread_key_create(&bh_linker_g_dl_mutex_key, NULL) ? -1 : 0;
#endif
}
#endif

现在也找不到android4的arm设备了,我随便下载了个arm架构的android4.x的刷机包和android5.x的刷机包,解压对比了/system/bin/linker和/system/lib/linker,发现android4.x是没有__dl__ZL10g_dl_mutexdlclose之类的导出符号的,这大概就是在栈帧中寻找互斥锁的原因吧。

从代码来看,有个奇怪的地方是至少android5、android7需要获取g_dl_mutex,而中间的android6似乎不需要获取?现在Android8以下的手机基本也不存在了,先忽略...

bh_linker_mutex.c

这个文件就是arm架构android4.x从栈帧里获取g_dl_mutex的具体实现。

size_t begin = 0, end = 0;
// ... begin和end就是linker的有rw权限的vma区间, g_dl_mutex在里面.

void *somain = dlopen(NULL, RTLD_LOCAL);
// ... 这里dlopen仅仅是为了后面有个理由调一下dlclose.

size_t stack[MAX_COUNT_OF_STACK_CHECK];
size_t *cursp = NULL;

/* don't modify this codes, even debug >>> */
__asm__ volatile("mov %[_cur_sp], sp" : [_cur_sp] "=r"(cursp) : :);

dlclose(somain);

if (cursp == NULL) return NULL;

cursp -= 1;

for (size_t i = 0; i < MAX_COUNT_OF_STACK_CHECK; i++) {
  stack[i] = *(cursp - i);
}
/* <<< don't modify this codes, even debug */

通过内联汇编获取sp寄存器的值(栈指针寄存器,一个函数被调用之后,函数的局部变量、入参 一部分数据在寄存器中,由于寄存器数量有限,所以另一部分数据在栈上。先入栈的数据在高地址,后入栈的在低地址)。

先记录当前栈的低地址存到cursp,然后故意调一下dlclose,dlclose函数内部用到的数据如果有存到栈上,就会往cursp更低的地址存。当这里dlclose返回到上方贴出的代码时,dlclose里面有些数据还留在栈的cursp更低的地址里面,而dlclose里面是有用到g_dl_mutex的,就这样能捞到g_dl_mutex

这也就是为什么注释里不让改这些代码,哪怕是debug的时候。

bh_trampo_arm64.c

__attribute__((naked))作用是不为函数生成“额外”的指令,比如函数内部的局部变量,在汇编指令视角来看都对应着各种寄存器,所以要在函数开头生成一些指令,把一些寄存器里原本内容保存到栈。而标记这个之后就不会生成这些指令了。


      // ...
      "br    x16                      \n"

      "bh_trampo_data:"
      ".global bh_trampo_data;"
      ".L_push_stack:"
      ".quad 0;"
      ".L_hook_ptr:"
      ".quad 0;");
}

内联汇编的这个末尾: .global bh_trampo_data;这条指令将标签bh_trampo_data声明为可以被其他文件引用的全局符号。

.L_push_stack:.L_hook_ptr:定义两个标签,它们以.开头,表示仅在当前文件可见。

这里.global不会占用额外空间,而.quad 0;会占用8字节空间。

汇编视角(IDA):

.text:000000000000CF70                 BR              X16
.text:000000000000CF70 ; End of function bh_trampo_template
.text:000000000000CF70
.text:000000000000CF70 ; ---------------------------------------------------------------------------
.text:000000000000CF74 bh_trampo_data  DCQ 0                   ; DATA XREF: bh_trampo_template+30↑r
.text:000000000000CF74                                         ; .got:bh_trampo_data_ptr↓o
.text:000000000000CF7C qword_CF7C      DCQ 0                   ; DATA XREF: bh_trampo_template+28↑r
.text:000000000000CF7C ; } // starts at CF0C
.text:000000000000CF84

这里CF74地址就是L_push_stack,同时也是bh_trampo_data对应的地址。

CF7CL_hook_ptr

这里定义bh_trampo_data是为了计算指令长度。

bh_utils.c

bh_util_starts_withbh_util_ends_with之类的,现在有了GPT,这种基本的方法就不必记录了...

bh_util_localtime_r时间戳转年月日时分秒,将来需要了再来这里复制吧(这个注释风格不太一样,猜测也是从哪里复制来的。)。


bh_util_trim_ending里面的isspace函数虽然只是判断下ascii码范围,但以前不知道C语言还提供这种函数, 来自于#include <ctype.h>里面的#include <bits/ctype_inlines.h>


获取android版本号,优先用NDK里的APIandroid_get_device_api_level,头文件#include <android/api-level.h>,但是它似乎有失败的可能,所以bytehook这里判断它返回值小于0,从"/system/build.prop"文件,找ro.build.version.sdk=

bytesig.c

bionic 和 ART sigchain 在某些 AOSP 版本上存在 bug,所以我们需要优先使用 sigaction64 和 sigprocmask64,而不是 sigaction 和 sigprocmask

出自这篇作者写的文章,但我没找到具体是什么版本,具体是什么BUG...

sigaction64sigaction名字上虽然有个数字,但这个和App位数/系统位数没关系,Linux内核支持64个信号,而sigaction函数的参数sigset_t类型在32位产物中只能支持32个信号(typedef unsigned long sigset_t;)。

bionic实际对sigaction的实现就相当于把传入的参数sigset_t转为sigset64_t然后调用sigaction64


关于通过dlsym刻意从libc查找sigaction, 而不是直接调用的原因:ART虚拟机的libsigchain有个同名的sigaction,并且编译参数加了-z,global(把libsigchain这个ELF标记上DF_1_GLOBAL),会覆盖掉libc的sigaction。ART和libc的这个函数行为不一样。

The global group technically contains:

  • the main executable, followed by
  • LD_PRELOAD libraries, then
  • libraries needed by the executable (in BFS order), then
  • libraries loaded by dlopen with RTLD_GLOBAL (also in BFS order). In addition, libraries with DF_1_GLOBAL (-Wl,-z,global) are in the global group.

For looking up relocations, though, the last two bullet points aren't searched (exception: A DF_1_GLOBAL library is always searched, but DF_1_GLOBAL is broken on O-MR1). A future version of Android might add all of these libraries to the global group used for relocation.

从技术上讲,全局组包含:

  • 可执行文件
  • LD_PRELOAD 库
  • 可执行文件所需的库(按 BFS 顺序) ··
  • 由 dlopen 使用 RTLD_GLOBAL 加载的库(也按 BFS 顺序)。此外,具有 DF_1_GLOBAL(-Wl、-z、global)的库位于全局组中。

但是,对于查找重定位,不会搜索最后两个要点(例外:始终会搜索 DF_1_GLOBAL 库,但 DF_1_GLOBAL 在 O-MR1 上已损坏)。未来版本的 Android 可能会将所有这些库添加到用于重定位的全局组中。


头文件limits.h里面有一个CHAR_BIT,计算比特数量可以用它代替字面量8...

static void bytesig_sigorset64(sigset64_t *dest, sigset64_t *left, sigset64_t *right) {
  sigemptyset64(dest);
  for (size_t i = 1; i < sizeof(sigset64_t) * CHAR_BIT; i++)
    if (sigismember64(left, (int)i) == 1 || sigismember64(right, (int)i) == 1) sigaddset64(dest, (int)i);
}

sigsetjmp/siglongjmpsetjmp/longjmp区别:前者可以根据参数决定是否保存和恢复信号掩码(信号屏蔽字),

int setjmp(jmp_buf __env) __returns_twice;
__noreturn void longjmp(jmp_buf __env, int __value);

int sigsetjmp(sigjmp_buf __env, int __save_signal_mask);
__noreturn void siglongjmp(sigjmp_buf __env, int __value);

比如setjmp之前,信号掩码a,setjmp之后并且longjmp之前,设置的信号掩码b,在调用longjmp之后,当前的信号屏蔽字可能还是b。(POSIX没有规定是否还原信号掩码)

而换成sigsetjmpsiglongjmp,如果sigsetjmp的参数二不是0siglongjmp之后和sigsetjmp之前信号屏蔽字相同,而siglongjmpsigsetjmp之间改变信号屏蔽字的行为,相当于被忽略掉。

siglongjmp的参数二就是sigsetjmp的返回值。在代码中通过判断sigsetjmp返回值为0,表示执行至此时,是按正常顺序执行到这里,而不是通过调用siglongjmp直接跳到这里。


很多资料都是通过sigprocmask设置信号掩码,在Android平台,实际pthread_sigmask就是包装了它。

// /bionic/libc/bionic/signal.cpp
int pthread_sigmask(int how, const sigset_t* new_set, sigset_t* old_set) {
  ErrnoRestorer errno_restorer;
  return (sigprocmask(how, new_set, old_set) == -1) ? errno : 0;
}

int pthread_sigmask64(int how, const sigset64_t* new_set, sigset64_t* old_set) {
  ErrnoRestorer errno_restorer;
  return (sigprocmask64(how, new_set, old_set) == -1) ? errno : 0;
}

参考

(内联汇编基础) - 博客园 (cnblogs.com)

解析 dex 文件结构 - LEB128 · kiya

生活在BSD的树上 (mistivia.com)

ifunc - YWgrit's blog

ART虚拟机 | Android应用中SIGSEGV信号的处理流程SIGSEGV是信号11,其在内存访问错误时产生。信号 - 掘金 (juejin.cn)

[question] Understanding dynamic linker symbol resolution order