Bionic 兼容层 — 让两种 libc 在同一进程共存
这是 Android Translation Layer (ATL) 系列技术博客的第五篇。在前几篇中,我们介绍了 ATL 的整体架构、Java Framework 的重新实现、NDK API 的重新实现,以及 ART 虚拟机的独立化。本篇将深入
bionic_translation/子目录,解析 ATL 如何在一个进程内同时运行 Bionic 和 glibc/musl — 两套完全不同的 C 标准库。
1. 概念铺垫:Bionic 是什么
Bionic 是 Android 自己的 C 标准库实现,替代了桌面 Linux 上常见的 glibc 或 musl。Google 在 2008 年从 BSD libc 派生出 Bionic,目标是体积小、启动快、适配 Linux 内核但避免 GPL 许可证。
从功能上看,Bionic 也提供 libc、libpthread、libdl、libm 这些标准组件,但它和 glibc/musl 之间存在大量 ABI 级别 的差异。以下是主要不兼容点:
| 差异维度 | Bionic | glibc/musl |
|---|---|---|
struct dirent | 固定 256 字节 d_name | d_name 大小取决于实现 |
struct stat (32 位) | time_t 为 uint32_t | time_t 为 long |
struct sigaction | 32 位:sa_flags 在 sa_mask 之后;64 位:sa_flags 在结构体开头 | sa_handler/sa_flags/sa_mask 顺序不同 |
struct addrinfo | ai_canonname 在 ai_addr 之前 | ai_addr 在 ai_canonname 之前 |
| sysconf 常量 | _SC_OPEN_MAX = 0x000b | _SC_OPEN_MAX = 4 (glibc) |
long double (x86) | i386: 64 位; x86_64: 128 位 | i386: 80 位; x86_64: 80 位 |
| pthread 结构体 | pthread_mutex_t 为紧凑的 int32_t 数组 | pthread_mutex_t 为不透明的大结构体 |
| TLS ABI | slot 0-7 有固定含义 (SELF, DTV, APP, OPENGL…) | 由 libc 内部管理 |
| stdio FILE | 自有布局 | glibc 有 vtable; musl 有自己的布局 |
这就带来了一个根本性挑战:APK 中的 .so 文件全部是针对 Bionic 编译链接的,而 ATL 的主可执行文件以及宿主系统的所有库都使用 glibc 或 musl。两套 libc 必须在同一个进程中共存,而且每当 Android 代码调用一个看起来普通的 C 函数(比如 readdir、sigaction、pthread_mutex_lock),实际传入和传出的结构体布局都可能和宿主 libc 不一致。
2. bionic_translation 的四个组件
ATL 的解决方案是 bionic_translation/ 子目录下的四个共享库。查看 bionic_translation/meson.build,它们分别是:
libdl_bio.so— 一个 shim 动态链接器(linker/linker.c有 2888 行),负责加载 Bionic 格式的.so文件并在符号解析时进行重定向libc_bio.so— libc ABI 翻译层,对readdir、stat、sigaction、getaddrinfo、sysconf等函数逐个包装libpthread_bio.so— pthread ABI 翻译层,桥接 Bionic 和 glibc/musl 之间不同的 mutex、cond、rwlock 内存布局libstdc++_bio.so— 最小 C++ 运行时,仅包含operator new/delete、__cxa_guard_*和__cxa_pure_virtual
这组代码最初来源于 android2gnulinux 项目,ATL 在此基础上做了大量修改以支持 musl、64 位架构,以及更完整的 ABI 兼容。
关于 libstdc++_bio.so 的存在意义,bionic_translation/NOTE 中有解释:Android 的 libstdc++.so 是 Google 早期添加 C++ 支持时的最小实现,现已废弃。虽然宿主系统上的 libc++ 理论上涵盖了所有功能,但如果让 Android 应用直接使用宿主的实现,会出现随机崩溃。因此需要单独提供这个小库。
配置文件 bionic_translation/cfg.d/bionic_translation.cfg 定义了库名映射关系:
# bionic_translation/cfg.d/bionic_translation.cfg
libc.so libc_bio.so.0
libstdc++.so libstdc++_bio.so.0
当 Android .so 请求加载 libc.so 时,shim 链接器会将其重定向到 libc_bio.so.0,从而让 Bionic API 调用进入翻译层。
libdl_bio 和 libc_bio 的协作关系
这两个库的分工可以用一个类比来理解:libdl_bio 是调度员,libc_bio 是翻译官。
当 APK 的 native .so 调用一个 C 函数(比如 stat())时,调用链如下:
APK 的 .so 调用 stat()
↓
libdl_bio(调度员): "stat 有 bionic_ 版本吗?"
↓ 查找 bionic_stat
libc_bio(翻译官): bionic_stat() 被找到
↓
bionic_stat() 内部:
1. 调用 glibc 的 stat() 获取结果
2. 把 glibc 的 struct stat 逐字段转换为 Bionic 的 struct stat
(32 位上 time_t 大小不同、字段顺序不同)
3. 返回 Bionic 格式的结果给 APK
对于没有 ABI 差异的函数(如 memcpy、malloc、strlen),libdl_bio 找不到 bionic_memcpy,就直接使用 glibc 的 memcpy——零翻译开销。只有结构体布局或常量值存在差异的函数才会经过 libc_bio 的翻译层。这种”按需翻译”的设计让绝大部分函数调用保持原生性能。
3. libdl_bio:shim 动态链接器
为什么需要自己的动态链接器
要理解 libdl_bio 的作用,首先需要理解动态链接器(dynamic linker)在程序运行中扮演的角色。
当一个程序调用 dlopen("libgame.so") 时,操作系统的动态链接器(Linux 上是 ld-linux.so)需要完成以下工作:
- 加载:找到
.so文件,把它的代码和数据段映射到进程内存中 - 符号解析:
.so中调用了stat()、pthread_create()等外部函数,链接器需要找到这些函数的实际地址 - 重定位:把找到的地址填写到
.so的 GOT/PLT 表中,这样函数调用才能跳转到正确位置 - 初始化:调用
.so的构造函数(__attribute__((constructor))和.init_array)
问题在于:Linux 的 ld-linux.so 在做符号解析时,会把 stat() 解析到 glibc 的 stat()。但 APK 中的 .so 是用 Bionic 编译的——它期望 stat() 的行为符合 Bionic 的 ABI(结构体布局、常量值等)。直接用 glibc 的版本会导致数据错乱。
libdl_bio 的解决方案是自己实现一个完整的 ELF 动态链接器(linker.c,2888 行),在第 2 步”符号解析”时插入判断逻辑:
对于 .so 中的每一个外部符号 foo:
├─ 存在 bionic_foo? → 使用翻译版本(如 bionic_stat → libc_bio.so)
├─ 是 Android 内部符号? → 在已加载的 bionic 库中查找
└─ 都不是? → 直接使用 glibc 的 foo(ABI 兼容的函数,如 memcpy)
这就是”shim”的含义:它不是完全替代 ld-linux.so(ATL 主程序自身仍然由系统链接器加载),而是专门用于加载 APK 中的 Bionic .so 文件,在加载过程中把需要翻译的函数调用重定向到翻译层。
整个过程对 APK 的 .so 完全透明——它以为自己被标准的 Android 动态链接器加载,实际上是 libdl_bio 在幕后操控符号解析,决定每个函数调用最终去哪里。
3.1 入口点
bionic_translation/linker/dlfcn.c 导出了 bionic_dlopen、bionic_dlsym、bionic_dlclose 等函数,替换 Android 代码中的 dlopen/dlsym 调用。bionic_dlopen 的核心流程是:
// bionic_translation/linker/dlfcn.c
void *bionic_dlopen(const char *filename, int flag)
{
soinfo *ret;
pthread_mutex_lock(&apkenv_dl_lock);
void *glibc_handle = NULL;
ret = apkenv_find_library(filename, true, flag, &glibc_handle);
if (ret) {
apkenv_call_constructors_recursive(ret);
ret->refcount++;
} else if (glibc_handle) {
ret = glibc_handle;
} else {
set_dlerror(DL_ERR_CANNOT_LOAD_LIBRARY);
}
pthread_mutex_unlock(&apkenv_dl_lock);
return ret;
}
如果 apkenv_find_library 在 Bionic 库路径中找不到目标,它会退而使用宿主的 dlopen 去加载 — 这样,非 Android 特有的库(如 libEGL.so、libGLESv2.so)可以直接使用系统版本。
3.2 完整加载流程
在 bionic_translation/linker/linker.c 中,加载一个 Bionic .so 分三步:
apkenv_find_library— 在配置的库路径中搜索文件,先查 cfg 映射表(如libc.so->libc_bio.so.0),再查文件系统,找不到时尝试宿主dlopenapkenv_load_library— 读取 ELF 头,计算内存占用(apkenv_get_lib_extents),通过mmap分配内存区域(apkenv_alloc_mem_region),加载各 segment(apkenv_load_segments)apkenv_link_image— 解析.dynamic段,处理DT_NEEDED递归加载依赖,执行重定位
3.3 三级符号解析
重定位是整个 shim 链接器最关键的环节。linker.c 的 apkenv_reloc_library 函数(约第 1768-1806 行)对每个需要重定位的符号按以下优先级查找:
// bionic_translation/linker/linker.c (apkenv_reloc_library)
if ((sym_addr = (uintptr_t)dlsym(RTLD_DEFAULT, wrap_sym_name))) {
// 第 1 级:找到 bionic_ 前缀版本(如 bionic_readdir)
} else if ((s = apkenv__do_lookup(si, sym_name, &base))) {
// 第 2 级:在已加载的 bionic 库链中查找
} else if ((sym_addr = (uintptr_t)dlsym(RTLD_DEFAULT, sym_name))) {
// 第 3 级:直接使用宿主 libc 的同名函数
}
第一级最为关键:对于任何符号名 foo,链接器先构造 bionic_foo,然后通过宿主的 dlsym(RTLD_DEFAULT, ...) 查找。如果 libc_bio.so 或 libpthread_bio.so 导出了 bionic_foo,就优先使用翻译包装版本。这就是为什么翻译层中所有函数都以 bionic_ 为前缀 — 这是和 shim 链接器约定的命名协议。
第二级用于 Android 库之间的互相引用,走的是 Bionic 格式的符号表查找。第三级则是兜底方案:如果某个函数在 Bionic 和 glibc 之间恰好 ABI 兼容(比如大部分 <math.h> 函数),直接使用宿主版本即可。
实际上,解析过程并不止于上述三级。在三级主查找之后,还有几个额外的 fallback 阶段:
- 第 4 级:OpenGL 扩展解析 — 对于
gl*开头的符号,链接器会通过eglGetProcAddress尝试解析。这是libdl_bio依赖 EGL 的原因:许多 OpenGL 扩展函数不在标准的libGLESv2.so符号表中,必须通过 EGL 的扩展查询机制获取。 - 第 5 级:
sigsetjmp特殊处理 — 在 glibc 上,sigsetjmp的符号名经过 mangling(实际为__sigsetjmp),需要单独处理以确保信号相关的 setjmp/longjmp 正常工作。 - 第 6 级:
LINKER_DIE_AT_RUNTIMEstub 生成 — 如果所有查找都失败,行为取决于环境变量LINKER_DIE_AT_RUNTIME:如果设置了该环境变量,链接器会生成一个 stub 函数,只在实际被调用时才打印缺失符号名并退出,而不是在链接阶段就中止。这对调试非常有用。
3.4 ELF 重定位类型
shim 链接器支持多种 ELF 重定位类型:R_GENERIC_JUMP_SLOT(PLT 跳转槽)、R_GENERIC_RELATIVE(基址相对)、R_GENERIC_GLOB_DAT(全局数据)、R_GENERIC_TLS_DTPMOD(TLS 模块 ID)等。此外还支持 Android 特有的 packed relocation 格式和 RELR 格式,这些是 Android 为了减小 .so 体积而引入的压缩重定位编码。
3.5 TLS 模块注册
bionic_translation/linker/linker_tls.c 实现了一套独立的 TLS(线程本地存储)模块注册机制。当 shim 链接器遇到 TLS_DTPMOD 类型的重定位时,会调用 __tls_register_module() 将该 .so 的 TLS 模板注册到全局注册表中:
// bionic_translation/linker/linker_tls.c
size_t __tls_register_module(void *template_base, size_t template_size,
size_t size, int align)
{
// ... 线程安全地将模块描述符添加到全局数组 ...
size_t slot = global_module_registry.count++;
global_module_registry.modules[slot] = (tls_module_desc_t){
.template_base = template_base,
.template_size = template_size,
.size = size,
.align = align,
};
return slot;
}
对应的 bionic___tls_get_addr 函数在运行时根据 (module, offset) 对查找或分配当前线程的 TLS 块。在 32 位 x86 上,由于 GNU 使用了不同的调用约定(寄存器传参),该函数以 bionic____tls_get_addr(四个下划线)的名字导出,并标记为 regparm(1)。
4. libc_bio:逐函数 ABI 翻译
libc_bio.so 是工作量最大的组件,涉及上千行翻译代码。下面按类别逐一分析。
4.1 struct dirent 转换
Bionic 的 struct dirent 和 glibc/musl 的版本字段类型不同。bionic_translation/libc/libc.c 定义了转换结构体和函数:
// bionic_translation/libc/libc.c
struct bionic_dirent {
uint64_t d_ino;
int64_t d_off;
unsigned short d_reclen;
unsigned char d_type;
char d_name[256];
};
struct bionic_dirent *bionic_readdir(DIR *dirp)
{
static struct bionic_dirent bde;
struct dirent *de;
if (!(de = readdir(dirp)))
return NULL;
glibc_dirent_to_bionic_dirent(de, &bde);
return &bde;
}
转换函数 glibc_dirent_to_bionic_dirent 逐字段拷贝 d_ino、d_off、d_reclen、d_type、d_name。还有 bionic_readdir_r 的线程安全版本。
4.2 struct stat(32 位)
32 位 Bionic 使用 uint32_t 作为 time_t(2038 年会溢出),而 glibc/musl 使用 long。时间戳字段也被包装在一个 bionic_timespec 中:
// bionic_translation/libc/libc.c (32 位)
typedef uint32_t bionic_time_t;
struct bionic_stat {
unsigned long long st_dev;
unsigned int pad0;
unsigned long __st_ino;
unsigned int st_mode;
// ... 以下字段顺序和宽度都和 glibc 不同 ...
struct bionic_timespec st_atim;
struct bionic_timespec st_mtim;
struct bionic_timespec st_ctim;
unsigned long long st_ino;
};
bionic_stat() 先调用宿主的 stat(),然后将结果逐字段赋值到 bionic_stat 结构体,包括将 time_t 截断为 uint32_t。在 64 位架构上,两者布局一致,直接透传即可。
4.3 struct sigaction
信号处理是最复杂的结构体差异之一。Bionic 在 32 位和 64 位上使用不同的字段顺序:
| 字段 | Bionic 32 位 | Bionic 64 位 | glibc/musl |
|---|---|---|---|
| 第 1 个字段 | sa_handler | sa_flags | sa_handler |
| 第 2 个字段 | sa_mask | sa_handler | sa_sigaction |
| 第 3 个字段 | sa_flags | sa_mask | sa_mask |
| 第 4 个字段 | sa_restorer | sa_restorer | sa_flags + sa_restorer |
bionic_sigaction() 不仅要重新排列字段,还要处理 Android 特有的信号编号映射:Android 的 THREAD_SIGNAL(信号 33)需要映射到宿主的 SIGRTMIN。此外,信号 32-35 在 Android 内部有保留用途(POSIX 定时器、libbacktrace、libcore、debuggerd),翻译层会过滤掉 sa_mask 中对这些信号的引用。
4.4 struct addrinfo
Bionic 和 glibc/musl 的 struct addrinfo 有一个微妙但致命的差异:ai_canonname 和 ai_addr 的位置 互换 了。
// bionic_translation/libc/libc.c
struct bionic_addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
char *ai_canonname; // bionic: canonname 在前
struct sockaddr *ai_addr; // bionic: addr 在后
struct bionic_addrinfo *ai_next;
};
bionic_getaddrinfo() 先调用宿主的 getaddrinfo() 获取结果链表,然后遍历每个节点,将 glibc 布局中的 ai_canonname 和 ai_addr 交换到 Bionic 的位置。bionic_freeaddrinfo() 则在释放前将它们换回来,以确保宿主的 freeaddrinfo 能正确工作。
4.5 sysconf 常量映射
Bionic 和 glibc 中 sysconf 的常量值完全不同。例如 _SC_OPEN_MAX 在 Bionic 中是 0x000b,在 glibc 中可能是 4。bionic_translation/libc/libc-sysconf.h 包含一个约 460 行的自动生成的 switch 语句,将每一个 Bionic 常量值映射到对应的 glibc/musl 常量:
// bionic_translation/libc/libc-sysconf.h
static inline int
bionic_sysconf_to_glibc_sysconf(int name)
{
switch (name) {
case 0x0000: return _SC_ARG_MAX;
case 0x0001: return _SC_BC_BASE_MAX;
// ... 150+ 映射项 ...
}
}
bionic_sysconf() 先通过这个映射表转换常量值,再调用宿主的 sysconf()。
4.6 long double ABI
x86 架构上 Bionic 和 glibc/musl 对 long double 的定义截然不同。bionic_translation/libc/libc-math.c 定义了一个统一类型:
// bionic_translation/libc/libc-math.c
#if defined(__i386__)
typedef double b_long_double; // 64 位
#elif defined(__x86_64__)
typedef __float128 b_long_double; // 128 位
#else
typedef long double b_long_double; // ARM 上一致
#endif
在 i386 上,Bionic 的 long double 实际上只是 double(64 位),而 glibc 是 80 位扩展精度。在 x86_64 上更极端,Bionic 使用 __float128(128 位四精度),而 glibc 使用 80 位。因此 acosl、asinl、atanl 等数十个数学函数都需要包装,在 b_long_double 和宿主 long double 之间转换。
4.7 FORTIFY_SOURCE _chk 函数
Android 的 FORTIFY_SOURCE 机制会将 memcpy、strcpy 等函数替换为带缓冲区大小检查的 __memcpy_chk、__strcpy_chk 版本。musl 不提供这些 _chk 函数,因此 bionic_translation/libc/libc-chk.c 需要全部实现。对于 musl 独有的部分(用 #ifndef __GLIBC__ 包裹),共有约 15 个函数;另外还有一些 glibc 有但 ABI 不兼容的,以及 glibc 也没有的,总计约 25 个 _chk 函数:
// bionic_translation/libc/libc-chk.c
void *bionic___memcpy_chk(void *dest, const void *src,
size_t copy_amount, size_t dest_len)
{
return memcpy(dest, src, copy_amount);
}
char *bionic___strcpy_chk(char *dest, const char *src, size_t dest_len)
{
return strcpy(dest, src);
}
大部分实现直接忽略了 dest_len 参数的检查(因为在 ATL 场景下,安全检查不是主要关切),少数实现如 bionic___fgets_chk 和 bionic___fwrite_chk 保留了基本的溢出检测。
4.8 文件操作与路径覆盖
bionic_translation/libc/libc-open-overrides.c 包装了 open 和 fopen,支持路径覆盖钩子:
// bionic_translation/libc/libc-open-overrides.c
int bionic_open(char *path, int oflag, ...)
{
bool free_path = (*apply_path_overrides_func)(&path);
if (!strcmp(path, "/proc/self/status")) {
// 隐藏 TracerPid 行,防止反调试检测
// ...
}
// ... 正常 open() ...
}
这里有两个设计要点。第一,apply_path_overrides_func 是一个函数指针,默认为空操作,由 ATL 主程序在启动时替换为实际的路径重写逻辑(后面 6.4 节会详述)。第二,对 /proc/self/status 做了特殊处理:读取真实内容后删除 TracerPid 行,以应对某些 Android 应用的反调试检查。
4.9 杂项翻译
bionic_translation/libc/libc-misc.c 处理了一系列零散的兼容性问题:
-
__system_property_find/__system_property_read:Android 的系统属性机制在 Linux 上不存在,这里提供了最小 stub,对ro.build.fingerprint返回空字符串 -
bionic_setlocale:Bionic 只允许""(空字符串,映射到C.UTF-8)、C、C.UTF-8、POSIX、en_US.UTF-8几种 locale,其他一律返回NULL并设置ENOENT。翻译层模拟了这一行为,避免应用设置不兼容的 locale -
bionic_getenv:拦截特定环境变量 —HOME返回NULL(Android 上不存在 HOME 目录概念),ANDROID_ROOT返回"/system",其余透传到宿主的getenv
4.10 stdio FILE 结构体翻译
stdio 的 FILE 结构体是 libc_bio 中最大的单一 ABI 翻译面之一。bionic_translation/libc/libc-stdio.h 定义了 struct bionic___sFILE,在 32 位上占 84 字节,在 64 位上占 152 字节,与 glibc 和 musl 各自的 FILE 布局完全不同。此外,bionic___sF[3] 数组提供了 Android M(API 23)之前版本的 stdin/stdout/stderr 兼容 — 早期 Android 应用直接通过这个全局数组索引来访问标准流,而非使用函数接口。
在 musl 环境下,bionic_translation/libc/libc-musl.c(约 703 行)包装了 80 多个与 FILE 相关的函数,将 Bionic 的 FILE 指针翻译为 musl 的 FILE 指针后再调用底层实现。这涵盖了 fopen、fclose、fread、fwrite、fprintf、fscanf 等几乎所有 stdio 操作,使其成为整个翻译层中规模最大的单文件包装集合。
4.11 musl 特有的兼容包装
libc-musl.c 还处理了两个 musl 特有的 ABI 差异:
- 32 位
off_t兼容:musl 即使在 32 位架构上也使用 64 位off_t,而 Bionic 在 32 位上使用 32 位off_t。翻译层需要在调用lseek、mmap、ftruncate等函数时进行宽度转换。 *64函数别名:由于 musl 的基础函数已经是 64 位语义(如lseek等同于lseek64),翻译层将 Bionic 的lseek64、ftruncate64、mmap64等*64后缀函数直接映射到 musl 的同名基础函数,避免重复包装。
5. libpthread_bio:pthread 结构体桥接
5.1 核心问题
Bionic 的 pthread_mutex_t 在 64 位上是 40 字节(10 个 int32_t),在 32 位上仅 4 字节(1 个 int32_t)。而 glibc 的 pthread_mutex_t 在 x86_64 上是 40 字节,musl 的也有自己的大小。关键不仅是大小不同,内存布局语义完全不同 — Bionic 用位域编码 mutex 类型和状态,glibc/musl 则使用结构化字段。
5.2 union + 懒初始化模式
bionic_translation/pthread_wrapper/libpthread.c 采用了一个精巧的设计来桥接两边:
// bionic_translation/pthread_wrapper/libpthread.c
typedef struct {
union {
#if defined(__LP64__)
int32_t __private[10];
#else
int32_t __private[1];
#endif
pthread_mutex_t *glibc;
};
} bionic_mutex_t;
__private 数组占据与 Bionic pthread_mutex_t 相同的空间,而 glibc 指针与之重叠。翻译层通过 mmap 分配一块新的内存给真正的 glibc pthread_mutex_t,并将指针存储在 union 中。
判断一个 bionic_mutex_t 是否已初始化,使用了 mincore() 系统调用:
// bionic_translation/pthread_wrapper/libpthread.c
#define IS_MAPPED(x) is_mapped(x->glibc, sizeof(*x))
#define INIT_IF_NOT_MAPPED(x, init) \
do { if (!IS_MAPPED(x)) init(x); } while(0)
static bool is_mapped(void *mem, const size_t sz)
{
const size_t ps = sysconf(_SC_PAGESIZE);
unsigned char vec[(sz + ps - 1) / ps];
return !mincore(mem, sz, vec);
}
mincore() 检查给定地址是否对应一个有效的内存映射。如果 glibc 指针指向的不是 mmap 分配的内存(即它还存储着 Bionic 的静态初始化值),mincore 会返回错误,从而触发初始化。
5.3 静态初始化映射
Android 代码中大量使用 PTHREAD_MUTEX_INITIALIZER 这样的静态初始化宏。Bionic 的静态初始化值和 glibc 完全不同,因此翻译层维护了一个映射表:
// bionic_translation/pthread_wrapper/libpthread.c
static const struct {
bionic_mutex_t bionic;
pthread_mutex_t glibc;
} bionic_mutex_init_map[] = {
{ .bionic = {{{ ((PTHREAD_MUTEX_NORMAL & 3) << 14) }}},
.glibc = PTHREAD_MUTEX_INITIALIZER },
{ .bionic = {{{ ((PTHREAD_MUTEX_RECURSIVE & 3) << 14) }}},
.glibc = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP },
{ .bionic = {{{ ((PTHREAD_MUTEX_ERRORCHECK & 3) << 14) }}},
.glibc = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP },
};
当 default_pthread_mutex_init 被 INIT_IF_NOT_MAPPED 触发时,它会比对当前 mutex 的内容和映射表中的 Bionic 初始化值,找到匹配项后 mmap 分配 glibc 版本并拷贝对应的 glibc 初始化值。
5.4 同模式应用于其他同步原语
同样的 union + INIT_IF_NOT_MAPPED 模式被应用于 pthread_cond_t、pthread_rwlock_t 和 sem_t。每种类型都有 bionic_xxx_t 包装结构体,在首次使用时懒初始化一个 glibc 版本。
5.5 Bionic 特有的 pthread 扩展
Bionic 提供了一些 POSIX 标准中没有的扩展函数:
pthread_cond_timedwait_relative_np:接受相对超时时间。翻译层将其转换为绝对时间后调用标准的pthread_cond_timedwaitpthread_cond_timedwait_monotonic_np:使用CLOCK_MONOTONIC的超时等待。同样转换为绝对时间
// bionic_translation/pthread_wrapper/libpthread.c
int bionic_pthread_cond_timedwait_relative_np(
bionic_cond_t *cond, bionic_mutex_t *mutex,
const struct timespec *reltime)
{
struct timespec tv;
clock_gettime(CLOCK_REALTIME, &tv);
tv.tv_sec += reltime->tv_sec;
tv.tv_nsec += reltime->tv_nsec;
if (tv.tv_nsec >= 1000000000) {
++tv.tv_sec;
tv.tv_nsec -= 1000000000;
}
return bionic_pthread_cond_timedwait(cond, mutex, &tv);
}
5.6 Cleanup handler 兼容
pthread 的 cleanup_push/cleanup_pop 在 glibc 和 musl 上有完全不同的内部实现。glibc 使用 __pthread_unwind_buf_t,musl 使用 struct __ptcb。翻译层定义了 bionic_pthread_cleanup_t,其中 union 的一个分支是 glibc 的 unwind buffer 指针,另一个是 musl 的 ptcb 指针:
// bionic_translation/pthread_wrapper/libpthread.c
struct bionic_pthread_cleanup_t {
union {
struct bionic_pthread_cleanup_t *prev;
#ifdef __GLIBC__
__pthread_unwind_buf_t *glibc;
#else
struct __ptcb *musl;
#endif
};
void (*routine)(void*);
void *arg;
};
bionic___pthread_cleanup_push 通过 mmap 分配宿主的 cleanup 结构体,注册到宿主的 pthread cancellation 机制中。bionic___pthread_cleanup_pop 则执行清理并回收 mmap 的内存。
6. ATL 主程序中的 Bionic 兼容
除了 bionic_translation/ 中的四个共享库,ATL 的主可执行文件中也包含一些必须在主程序级别处理的兼容代码。
6.1 TLS slot 对齐
src/main-executable/bionic_compat.c 中定义了一个关键的 _Thread_local 数组:
// src/main-executable/bionic_compat.c (ARM/AArch64)
_Thread_local uintptr_t TLS[] = {
/* (tp + 2) = */ 0x5555555555555555, // TLS_SLOT_APP
/* (tp + 3) = */ 0x5555555555555555, // TLS_SLOT_OPENGL
/* (tp + 4) = */ 0x5555555555555555, // TLS_SLOT_OPENGL_API
/* (tp + 5) = */ 0x5555555555555555, // TLS_SLOT_STACK_GUARD
/* (tp + 6) = */ 0x5555555555555555, // TLS_SLOT_SANITIZER
/* (tp + 7) = */ 0x5555555555555555, // TLS_SLOT_ART_THREAD_SELF
};
为什么它必须是主可执行文件中 唯一 的 _Thread_local 变量?因为链接器会将主可执行文件的 PT_TLS 段放在线程指针(tp)的已知偏移处。如果有其他 TLS 变量,这个数组在 TLS 段中的位置就不再可预测,Bionic 代码通过 tp + N 访问特定 slot 时就会读写错误的内存。
在 x86/x86_64 上情况稍有不同,因为 Bionic 的 slot 布局恰好和 glibc/musl 的 tcbhead_t 结构体重合。特别是 tp + 5(stack guard)在两边都是栈保护值,这是一个幸运的巧合。但在 ARM 上,Google 选择了不同的 stack guard 位置,因此需要这个 TLS 数组来占位。
文件中详细注释了 glibc 64 位、glibc 32 位和 musl 各自在 tp + 0 到 tp + 10 处存储的内容,说明了哪些 slot 恰好兼容、哪些需要额外处理。
6.2 r_debug 注册
同一文件的开头,init__r_debug() 解析主可执行文件的 _DYNAMIC 段,找到 DT_DEBUG 标签以获取 r_debug 结构体的指针:
// src/main-executable/bionic_compat.c
void init__r_debug()
{
#if defined(_r_debug)
_r_debug_ptr = &_r_debug; // glibc 直接导出
#else
// musl 不导出 _r_debug,需要手动从 _DYNAMIC 中找
int i = 0;
ElfW(Dyn) current;
do {
current = _DYNAMIC[i];
if (current.d_tag == DT_DEBUG) {
_r_debug_ptr = (struct r_debug *)current.d_un.d_ptr;
break;
}
i++;
} while (current.d_tag != 0);
#endif
}
r_debug 是 GDB 用来追踪动态库加载的标准机制。shim 链接器在加载新的 Bionic .so 后,会通过这个指针通知 GDB 更新其库列表,使调试体验尽可能正常。
6.3 栈预增长
src/main-executable/main.c 中的 pregrow_stack() 解决了一个 musl 特有的问题:
// src/main-executable/main.c
static void pregrow_stack()
{
setrlimit(RLIMIT_STACK, &(struct rlimit){8 * MiB, 8 * MiB});
volatile uint8_t dummy[6 * MiB];
dummy[0] = dummy[0]; // 触发栈增长
}
Linux 内核对主线程的栈使用”按需增长”策略:初始只映射 128KiB,当访问超出当前映射的地址时自动扩展。但 ART 虚拟机会在栈底放置自己的 guard page,用于优雅地捕获 Java 层的栈溢出。问题在于:如果 guard page 被放在内核尚未扩展到的区域,它本身就会阻止内核进一步扩展栈。
glibc 的 pthread_getattr_np 报告的是 RLIMIT_STACK(大值),musl 报告的是当前实际栈大小(小值)。在 musl 上,ART 会基于较小的栈大小计算 guard page 位置,这个位置可能恰好挡住了栈的增长方向。解决方案很直接:在 ART 初始化之前,先分配一个 6MiB 的 volatile 数组强制内核把栈扩展到位。
6.4 路径覆盖注册
在 main.c 的主函数中,ATL 注册了路径覆盖回调:
// src/main-executable/main.c
libc_bio_set_apply_path_overrides_func(apply_path_overrides);
实际的覆盖逻辑在 src/main-executable/libc_bio_path_overrides.c 中:
-
/system/fonts/*— Android 的字体全在/system/fonts/目录下。翻译层使用 fontconfig 搜索宿主系统上文件名匹配的字体路径。例如/system/fonts/Roboto-Regular.ttf会被重定向到/usr/share/fonts/truetype/roboto/Roboto-Regular.ttf(如果 fontconfig 能找到的话) -
/system/etc/fonts.xml— Android 的字体配置文件。翻译层优先查找/etc/fonts.xml,其次查找 ATL 安装的$DATADIR/atl/system/etc/fonts.xml -
通用
/system/和/data/路径 — 如果应用尝试访问其他 Android 系统路径,翻译层会在 stderr 输出警告
总结
bionic_translation 的设计遵循一个清晰的原则:在 ELF 符号解析层面进行拦截,在 C ABI 层面进行翻译。shim 链接器通过 bionic_ 前缀的命名约定将函数调用重定向到翻译层;翻译层在每个函数内部完成结构体布局转换、常量值映射、以及 pthread 同步原语的代理。这种方式不需要修改 Android 应用的二进制文件,也不需要修改宿主系统的 libc — 它完全工作在两者之间的接缝处。
主要的技术挑战和对应方案:
- 结构体布局不兼容 — 逐字段拷贝转换(dirent、stat、sigaction、addrinfo)
- 常量值不兼容 — 查表映射(sysconf、fpclassify)
- 类型宽度不兼容 — 条件编译 + typedef 别名(long double、time_t)
- 同步原语布局不兼容 — union 重叠 + mmap 分配 + mincore 检测(pthread 全家族)
- TLS ABI 不兼容 — 主程序中精确放置 _Thread_local 数组 + 独立的 TLS 模块注册机制
- 文件系统路径不兼容 — 可插拔的路径覆盖钩子 + fontconfig 集成
在下一篇博客中,我们将探讨 ATL 如何将 Android 的 Looper 事件循环与 GLib/GTK 的事件循环融合到同一个线程中:事件循环融合。