Skip to content
Go back

ATL深度解析(5)Bionic 兼容层 — 让两种 libc 在同一进程共存

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 级别 的差异。以下是主要不兼容点:

差异维度Bionicglibc/musl
struct dirent固定 256 字节 d_named_name 大小取决于实现
struct stat (32 位)time_tuint32_ttime_tlong
struct sigaction32 位:sa_flags 在 sa_mask 之后;64 位:sa_flags 在结构体开头sa_handler/sa_flags/sa_mask 顺序不同
struct addrinfoai_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 ABIslot 0-7 有固定含义 (SELF, DTV, APP, OPENGL…)由 libc 内部管理
stdio FILE自有布局glibc 有 vtable; musl 有自己的布局

这就带来了一个根本性挑战:APK 中的 .so 文件全部是针对 Bionic 编译链接的,而 ATL 的主可执行文件以及宿主系统的所有库都使用 glibc 或 musl。两套 libc 必须在同一个进程中共存,而且每当 Android 代码调用一个看起来普通的 C 函数(比如 readdirsigactionpthread_mutex_lock),实际传入和传出的结构体布局都可能和宿主 libc 不一致。

2. bionic_translation 的四个组件

ATL 的解决方案是 bionic_translation/ 子目录下的四个共享库。查看 bionic_translation/meson.build,它们分别是:

  1. libdl_bio.so — 一个 shim 动态链接器(linker/linker.c 有 2888 行),负责加载 Bionic 格式的 .so 文件并在符号解析时进行重定向
  2. libc_bio.so — libc ABI 翻译层,对 readdirstatsigactiongetaddrinfosysconf 等函数逐个包装
  3. libpthread_bio.so — pthread ABI 翻译层,桥接 Bionic 和 glibc/musl 之间不同的 mutex、cond、rwlock 内存布局
  4. 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 差异的函数(如 memcpymallocstrlen),libdl_bio 找不到 bionic_memcpy,就直接使用 glibc 的 memcpy——零翻译开销。只有结构体布局或常量值存在差异的函数才会经过 libc_bio 的翻译层。这种”按需翻译”的设计让绝大部分函数调用保持原生性能。

3. libdl_bio:shim 动态链接器

为什么需要自己的动态链接器

要理解 libdl_bio 的作用,首先需要理解动态链接器(dynamic linker)在程序运行中扮演的角色。

当一个程序调用 dlopen("libgame.so") 时,操作系统的动态链接器(Linux 上是 ld-linux.so)需要完成以下工作:

  1. 加载:找到 .so 文件,把它的代码和数据段映射到进程内存中
  2. 符号解析.so 中调用了 stat()pthread_create() 等外部函数,链接器需要找到这些函数的实际地址
  3. 重定位:把找到的地址填写到 .so 的 GOT/PLT 表中,这样函数调用才能跳转到正确位置
  4. 初始化:调用 .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_dlopenbionic_dlsymbionic_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.solibGLESv2.so)可以直接使用系统版本。

3.2 完整加载流程

bionic_translation/linker/linker.c 中,加载一个 Bionic .so 分三步:

  1. apkenv_find_library — 在配置的库路径中搜索文件,先查 cfg 映射表(如 libc.so -> libc_bio.so.0),再查文件系统,找不到时尝试宿主 dlopen
  2. apkenv_load_library — 读取 ELF 头,计算内存占用(apkenv_get_lib_extents),通过 mmap 分配内存区域(apkenv_alloc_mem_region),加载各 segment(apkenv_load_segments
  3. apkenv_link_image — 解析 .dynamic 段,处理 DT_NEEDED 递归加载依赖,执行重定位

3.3 三级符号解析

重定位是整个 shim 链接器最关键的环节。linker.capkenv_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.solibpthread_bio.so 导出了 bionic_foo,就优先使用翻译包装版本。这就是为什么翻译层中所有函数都以 bionic_ 为前缀 — 这是和 shim 链接器约定的命名协议。

第二级用于 Android 库之间的互相引用,走的是 Bionic 格式的符号表查找。第三级则是兜底方案:如果某个函数在 Bionic 和 glibc 之间恰好 ABI 兼容(比如大部分 <math.h> 函数),直接使用宿主版本即可。

实际上,解析过程并不止于上述三级。在三级主查找之后,还有几个额外的 fallback 阶段:

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_inod_offd_reclend_typed_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_handlersa_flagssa_handler
第 2 个字段sa_masksa_handlersa_sigaction
第 3 个字段sa_flagssa_masksa_mask
第 4 个字段sa_restorersa_restorersa_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_canonnameai_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_canonnameai_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 位。因此 acoslasinlatanl 等数十个数学函数都需要包装,在 b_long_double 和宿主 long double 之间转换。

4.7 FORTIFY_SOURCE _chk 函数

Android 的 FORTIFY_SOURCE 机制会将 memcpystrcpy 等函数替换为带缓冲区大小检查的 __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_chkbionic___fwrite_chk 保留了基本的溢出检测。

4.8 文件操作与路径覆盖

bionic_translation/libc/libc-open-overrides.c 包装了 openfopen,支持路径覆盖钩子:

// 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 处理了一系列零散的兼容性问题:

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 指针后再调用底层实现。这涵盖了 fopenfclosefreadfwritefprintffscanf 等几乎所有 stdio 操作,使其成为整个翻译层中规模最大的单文件包装集合。

4.11 musl 特有的兼容包装

libc-musl.c 还处理了两个 musl 特有的 ABI 差异:

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_initINIT_IF_NOT_MAPPED 触发时,它会比对当前 mutex 的内容和映射表中的 Bionic 初始化值,找到匹配项后 mmap 分配 glibc 版本并拷贝对应的 glibc 初始化值。

5.4 同模式应用于其他同步原语

同样的 union + INIT_IF_NOT_MAPPED 模式被应用于 pthread_cond_tpthread_rwlock_tsem_t。每种类型都有 bionic_xxx_t 包装结构体,在首次使用时懒初始化一个 glibc 版本。

5.5 Bionic 特有的 pthread 扩展

Bionic 提供了一些 POSIX 标准中没有的扩展函数:

// 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 + 0tp + 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 中:

总结

bionic_translation 的设计遵循一个清晰的原则:在 ELF 符号解析层面进行拦截,在 C ABI 层面进行翻译。shim 链接器通过 bionic_ 前缀的命名约定将函数调用重定向到翻译层;翻译层在每个函数内部完成结构体布局转换、常量值映射、以及 pthread 同步原语的代理。这种方式不需要修改 Android 应用的二进制文件,也不需要修改宿主系统的 libc — 它完全工作在两者之间的接缝处。

主要的技术挑战和对应方案:

在下一篇博客中,我们将探讨 ATL 如何将 Android 的 Looper 事件循环与 GLib/GTK 的事件循环融合到同一个线程中:事件循环融合


Share this post on:

Previous Post
ATL深度解析(4)ART 独立化 — 把 Android 虚拟机搬到 Linux 桌面
Next Post
ATL深度解析(6)事件循环融合 — 让 Android Handler 和 GTK MainLoop 共舞