最近几周浏览一下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_hash
、bh_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_mutex
、dlclose
之类的导出符号的,这大概就是在栈帧中寻找互斥锁的原因吧。
从代码来看,有个奇怪的地方是至少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
对应的地址。
CF7C
是L_hook_ptr
。
这里定义bh_trampo_data
是为了计算指令长度。
bh_utils.c
bh_util_starts_with
、bh_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...
sigaction64
和sigaction
名字上虽然有个数字,但这个和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/siglongjmp
和setjmp/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没有规定是否还原信号掩码)
而换成sigsetjmp
和siglongjmp
,如果sigsetjmp
的参数二不是0
,siglongjmp
之后和sigsetjmp
之前信号屏蔽字相同,而siglongjmp
和sigsetjmp
之间改变信号屏蔽字的行为,相当于被忽略掉。
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;
}
参考
ART虚拟机 | Android应用中SIGSEGV信号的处理流程SIGSEGV是信号11,其在内存访问错误时产生。信号 - 掘金 (juejin.cn)
[question] Understanding dynamic linker symbol resolution order