Android Bionic 技术调研报告(v1.8-public):体系边界、运行机制与 RISC-V64 实现现状
| 字段 | 内容 |
|---|---|
| 文档类型 | 技术调研报告 |
| 版本 | v1.8-public(机制 v1.7-r2 + benchmark 增补) |
| 状态 | 公开研究稿(源码与固定 pin 复核;未做设备实测) |
| 密级 | 公开 |
| 作者 | zhenjun.feng |
| 生成日期 | 2026-10-21 |
| 研究截点 | 固定 release 基线 android17-release(= tag android-17.0.0_r1)commit 06356e41c5ed7b12220b24c05ba4fdb873b126a7(2026-03-28);Android 16 公开基线 android-16.0.0_r4 commit f22516cbc67e81c13cf943ce02f01b8929a98726(2025-10-09);配套 external/scudo(15715589)、external/arm-optimized-routines(498b3de1)、external/llvm-libc(95e08d1d)、external/libcxx(88894a62)同为 android17-release 分支固定 commit;分支与版本身份经官方 git 协议于 2026-10-16 现场核对 |
| 目标读者 | 从事 Android 系统软件、RISC-V 平台适配与性能优化的资深工程师;需要沿原理定位 libc/链接器/线程/分配器热点的分析和优化人员 |
| 调研问题 | Bionic 由哪些部件构成、边界在哪里;进程启动到 main、动态链接、系统调用封装、线程与 TLS、原生堆、字符串例程等机制的真实实现是什么;在 RISC-V64 上这些机制各自处于"存在/受构建支持/有测试/正式 ABI"的哪一层;如何在 bionic 基准与采样数据上分辨软件路径差异与硬件差异 |
| 覆盖范围 | bionic 五部件职责与外部依赖;启动链;linker 装载/符号/重定位/命名空间;syscall 生成、vDSO、RISC-V hwprobe;pthread/TLS/futex/原子;Scudo 分配器;string/memory;libm;signal/setjmp/unwind;stdio/时间/随机/locale/dns 功能地图;FORTIFY 与构建实现选择;bionic 测试与基准方法;当前主线与 Android 16 公开基线差异及 RISC-V64 支持层级;benchmark 套件家族/注册点/harness/计时方法与未实测边界 |
| 排除范围 | 未在真实设备上运行任何基准、未采集性能数据;不含 ART/libcore 内部实现(分属独立研究);不含未公开源码、厂商分支与内部优化;不评价具体硬件优劣 |
摘要
- Bionic 是 Android 的 C 库、数学库与动态链接器,由
libc、libm、libdl、libstdc++、linker等多类产物构成(libm是独立cc_library,不是折入libc);其中libstdc++只提供 C++ ABI 支持函数(__cxa_guard_acquire、operator new/delete),并不是 GNU libstdc++,也不含 STL,STL 属于独立的 LLVMlibc++/libc++abi仓库 [S01][S48]。 - 默认目标上的真实
malloc/free实现是 Scudo,由构建期宏USE_SCUDO选择并以静态库整体链入libc.so;libc/bionic/bionic_allocator.cpp是 libc/linker 内部的小对象分配器而非 malloc,全树不存在__libc_malloc/__libc_free符号 [S26][S27]。 - RISC-V64 的字符串/内存例程是 bionic 树内手写的 RVV 汇编(
libc/arch-riscv64/string/*.S),memcpy/memset为单一 VLEN-agnostic 字节循环、无尺寸分派;对照之下 ARM64 的memcpy/memset主体来自external/arm-optimized-routines并经 IFUNC 按hwcap运行分派,这是两条软件路径差异,其倍数影响必须在目标硬件实测,报告不臆测数字 [S07][S09]。 - Bionic 已支持 RISC-V 的 IFUNC 机制:
__bionic_call_ifunc_resolver的__riscv分支把(AT_HWCAP, __riscv_hwprobe, nullptr)传给解析器;__riscv_hwprobe优先走 vDSO、否则内联ecall。但 bionic 自身没有注册任何 riscv IFUNC、也没有用 hwprobe 做 string/libm 运行分派,因此"源码里用了 RVV 指令"不等于"运行时有 RVV 可用性探测与回退" [S15][S16][S41]。 - 分支名必须按官方语义读:官方
platform/manifest的main已声明<default revision="android17-release">,而 bionic 仓库自己的main(等于master) 是冻结快照(731631f3…,2025-03-26;该 SHA 仅对 bionic 实核);据此不能把 bionic 的main当当前主线,更不能按日期把它叫 AOSP17 [S42][S02]。
背景与现状
Bionic 是 Android 一直在用的 C 运行库,也是 Android 与主流 Linux 发行版的显著差异点。按 bionic 仓库自述,它的"大件"是 libc/(libc.so/libc.a)、libm/(libm.so,历史上为省空间而独立)、libdl/(dlopen(3) 等接口的 stub,运行时被链接器替换为自身实现)、libstdc++/(libstdc++.so,仅 C++ ABI 支持函数)、linker/(/system/bin/linker{,64},由 ELF 的 PT_INTERP——程序头类型,非动态段标签 DT_*——触发)[S01]。仓库结构上,各架构在 libc/arch-<arch>/ 下放机器相关汇编,libc/kernel/uapi 承载内核 UAPI 头,BSD 源在 libc/upstream-{freebsd,netbsd,openbsd},而堆实现在 external/scudo/、部分优化字符串/内存/数学例程在 external/arm-optimized-routines/、部分函数来自 external/llvm-libc/ [S01]。这意味着"Bionic 的行为"往往不只由 bionic 一个仓库决定,定位热点时必须跨仓库检索。与 glibc 相比,Bionic 在 locale、线程取消、robust mutex 等方面主动裁剪(docs/status.md 明确列出),而它自带把测试跑到 host 的 glibc/musl 上的入口,便于对照 POSIX 行为 [S01][S02]。
版本与分支身份是本次调研首先要钉死的事实。截至 2026-10-16,官方 platform/manifest 的 main 分支在其 default.xml 中声明 <default revision="android17-release" remote="aosp"/>,并把 superproject 指向 android-latest-release [S42];而 platform/bionic 的 main(等于 master)停在 731631f3…(2025-03-26)这一冻结快照 [S02](该 SHA 是本报告实核的 bionic pin;不据此推断其它仓库——art 自己的 main 是不同 commit,本报告未对其取值)。bionic 真正在推进的公开分支是 android17-release,其 tip 与 android-17.0.0_r1 指向同一 commit 06356e41…(2026-03-28);Android 16 的公开基线可取 android-16.0.0_r4(即 android16-qpr2-release)f22516cb…(2025-10-09)。temp-android16-plus 这个分支名与 android17-release 是同一 commit,属旧名沿用;android16-release/android-16.0.0_r1 则是 09a271af…(2025-04-30),QPR1 为 81b12d08…。因此本报告"当前主线"一律指 android17-release@06356e41,"16 基线"指 android-16.0.0_r4@f22516cb,并把释放通道与冻结的 main 分开陈述,避免把分支名当版本号、把目录存在当支持完成。
RISC-V64 的话题需要先划清"存在/受构建支持/有测试/正式 ABI"四个层级。bionic 为 riscv64 提供了完整的 libc/arch-riscv64/ 汇编集合(syscall.S、__bionic_clone.S、setjmp.S、vfork.S、_exit_with_stack_teardown.S 以及 string/ 下 17 个例程),linker/arch/riscv64/ 提供 begin.S 与 tlsdesc_resolver.S,libc/Android.bp 的 riscv64: 块把这些源写入 libc [S10]。API 侧,docs/status.md 记录 __riscv_flush_icache 与 __riscv_hwprobe 包装在 API 35(V)加入,还记录了 LD_SHOW_AUXV、%#m 等行为 [S02]。这些都属"存在且受构建支持"。但 bionic 内部对 riscv64 没有 IFUNC 表、没有基于 hwprobe 的运行分派,libm 侧 riscv64 只有 fenv-riscv64.c 一项专属源 [S34]。更细的工程事实(如 SA_RESTORER 在 riscv64 的 uapi 头里不定义)也提示:riscv64 的若干行为依赖内核或 vDSO,而非 bionic 自带等价物 [S33]。这些差异不是缺陷判断,而是定位热点前必须掌握的前提。
就性能分析口径而言,bionic 自带 Google Benchmark 之上的 bionic-benchmarks 与 bionic-spawn-benchmarks,配套 benchmarks/suites/*.xml 与一组尺寸简写(AT_COMMON_SIZES 等)[S29]。理解 bionic 的运行机制,才能合理解释同一用例在不同构建/运行模式下的分数差异,并把"能改的软件路径"与"只能测的硬件差异"分开。本报告的首要目的即在于此:让读者能沿原理从"某个基准偏慢"走到"具体函数/文件/汇编入口",再提出正确实验,而不是停留在术语层面。
技术详解:从库边界到热点定位
3.1 体系构成与库边界
入口与步骤序列:确定"某个 API 由谁实现"的可复述步骤是:①读仓库根 README.md 拿到五部件清单 [S01];②确认目标符号落在哪个部件——查 libc/libc.map.txt(libc)、libm/libm.map.txt(libm)、libdl/libdl.map.txt(libdl)、libc/libstdc++.map.txt(libstdc++)[S48];③若不在 bionic 导出表,转向外部仓库——malloc 在 external/scudo、部分 memcpy/exp 在 external/arm-optimized-routines、STL 在 external/libcxx [S01][S26][S09];④回到 libc/Android.bp 的 arch: 块确认最终链接进某架构 libc 的源集 [S10]。
关键数据结构与字段:libstdc++.map.txt 的导出面是最直观的边界依据:它导出的符号是 _ZSt7nothrow、_ZdaPv/_ZdlPv(数组/对象 delete)、_Znaj/_Znwj(仅 32 位)、_Znam/_Znwm(64 位,含 riscv64)、__cxa_guard_acquire/release/abort、__cxa_pure_virtual [S48];配套头 libstdc++/include/new 声明的是全局 operator new/delete(含 std::nothrow 重载与 placement 形式)——不是"std:: 命名空间里的运算符",导出表中对应 _Znwm/_Znam(operator new)、_ZdlPv/_ZdaPv(operator delete)与变量 _ZSt7nothrow。这与 GNU libstdc++ 的符号体系完全不同——它没有 std::string 等 STL 符号,因为 bionic 的 libstdc++.so 只承担 C++ ABI 支持。libdl 的边界则体现为"导出的是 stub":README.md 直言 libdl 是"一堆运行时会被动态链接器替换为其自身实现的 stub" [S01],真实实现在 linker/dlfcn.cpp 以 __loader_* 导出(见 3.3)。
实现与接口依据:内核 UAPI 边界由 libc/kernel/uapi 承载,例如 riscv 的 syscall 号在 libc/kernel/uapi/asm-riscv/asm/unistd_64.h,hwprobe 结构在 asm-riscv/asm/hwprobe.h,而 asm-riscv/asm/signal.h 仅 #include <asm-generic/signal.h>(因而 riscv64 不定义 SA_RESTORER)[S33]。运行时部署边界是 APEX:bionic 的 apex/ 目录描述 com.android.runtime,其 linker.config.json 仅 {"visible": true},manifest 只有 {name, version} [S01];链接器按 ld.config 为进程配置命名空间(见 3.3)。
失效边界:把 STL 符号当 bionic 导出会失败——对 libstdc++.so 做 nm -D 找不到 std::string 一类符号;libm 是 bionic 独立构建的 cc_library(libm/Android.bp:491 产出真实 libm.so,配 libm.map.txt 全量导出与 stubs/llndk,并作为 native_shared_libs 打入 com.android.runtime APEX;libc 侧仅含历史遗留的 ldexp),不应因某二进制 DT_NEEDED 里没列它、或某符号恰好在 libc 里,就断言"libm 折入 libc"(用到 libm 符号的程序 readelf -d 会列出 libm.so;DT_NEEDED 只反映直接依赖,不证明实现归属);把 bionic_allocator.cpp 当 malloc 实现会在写补丁时找错文件(该文件不含 malloc 导出)[S56][S26]。判定归属应直接 nm -D /system/lib64/libc.so | grep <symbol>,再回到 libc/Android.bp 确认源。
验证条件(可复现,本报告未运行):可复制观察包括 nm -D/readelf -d 检查产物导出与依赖;readelf -d <bin> | grep NEEDED 看动态依赖;对某符号用 grep -rn "symbol" libc/Android.bp 定位源集 [S10]。这属于静态验证,无需上板。
成本与权衡:边界设计本身有成本考量——libdl 用 stub + 运行时替换换取"链接期薄、运行期统一";libstdc++ 只留 ABI 支持、把 STL 交给 libc++,换取 bionic 与编译器/STL 解耦;libm 与 libc 各自独立构建(libm/Android.bp:491),各有一份版本脚本与导出面,代价是库数量与跨库符号解析而非"折叠成一个镜像"。这些取舍直接影响启动时的库数量与重定位量(见 3.2、3.3),理解它们有助于判断"某热点到底在哪一层"。
补充:文档与构建边界。libc.map.txt 与 libm.map.txt 是两套独立版本脚本,决定哪些符号从 libc/libm 导出;libc/upstream-{freebsd,netbsd,openbsd} 下的源"尽量不改动"、通过各目录 android/include/ 里的 *-compat.h 自动 -include 来适配编译,external/llvm-libc 则按需补入个别函数 [S01]。这些外部源之所以留在 bionic 树内而非各建小仓库,README 解释为历史原因(BSD 只有单一大仓、repo 工具不便只取小子集)[S01]。apex/Android.bp 描述 com.android.runtime 的打包,运行时调试层(libc_malloc_debug/libc_malloc_hooks)不在默认路径、由属性触发经 android_dlopen_ext 装入 [S26]。一个常见误判示例:把"某二进制 DT_NEEDED 里没有 libm.so"当成"libm 被 libc 吸收"——libm 其实是独立 cc_library(libm/Android.bp:491),DT_NEEDED 只表示直接依赖,且 libm 的符号也可能因静态链接或版本化而出现在别处;另一误判是把 bionic_allocator.cpp 当 malloc 去改,而它不含 malloc 导出 [S10][S26]。把 API 归属定位下来的可复述步骤可写作一条链:nm -D <产物>() → grep <符号>() → 命中则查 libc/Android.bp() 的 arch 源集 → 未命中则到 external/{scudo,arm-optimized-routines,libcxx}() 检索 [S10]。
见图 1。
手机可横向滑动查看本图
图 1:Bionic 体系构成、外部依赖与内核/APEX 边界(来源:bionic@06356e41 源码与构建文件 [S01][S10][S26])。图中上层是 libcore/ART/framework 经 JNI/NDK 使用 bionic 的消费者,中间是五部件,下方左右分别是外部实现仓库与内核 UAPI,底部是 APEX 与运行时调试层。读图要点:其一,malloc 与部分 memcpy/exp 不在 bionic 仓库内,定位热点要跨仓库;其二,libstdc++ 与 libc++ 是两条独立产物,别把 C++ 侧开销记到 bionic;其三,libm 由独立 cc_library 构建(libm/Android.bp:491),与 libc 并列,不要把数学热点默认当作在 libc 内。
3.2 进程启动:exec 到 __libc_init 到 main
入口与调用链:动态可执行的启动链为 kernel execve(读 PT_INTERP)→ linker _start(begin.S)→ __linker_init(linker_main.cpp:724)→ __libc_init_main_thread_early(:736)→ relocate_linker()(:758)→ __linker_init_post_relocation(:784)→ __libc_init_main_thread_late()(:787)→ linker_main()(装载 DT_NEEDED、逐库 replace)→ __libc_init_main_thread_final() → call_pre_init_constructors()/call_constructors() → 跳到主镜像 _start → __libc_init(libc_init_dynamic.cpp:147)→ exit(main(...)) [S11][S39][S19]。
关键数据结构与字段:KernelArgumentBlock(libc/private/KernelArgumentBlock.h:32)从原始栈指针解析 argc、argv、envp,并在跳过环境块尾的 NULL 后得到 auxv;它还自带一个不依赖 libc 全局的 getauxval(:53)[S47]。__libc_init_main_thread_early(__libc_init_main_thread.cpp:73)把 args.auxv 存入 __libc_shared_globals(),__init_tcb 写入 TLS_SLOT_THREAD_ID,__init_tcb_dtv 指向静态零 DTV,__set_tls 设置线程指针,tid = __getpid(),并记录 stack_top = argv(:84)[S18]。主线程对象 main_thread 是 BIONIC_USED_BEFORE_LINKER_RELOCATES static pthread_internal_t(:47)[S18]。
实现与接口依据:riscv64 的 _start(linker/arch/riscv64/begin.S:31)先 .cfi_undefined ra 截断回溯,再 mv a0, sp(把原始栈指针作第一个参数),call __linker_init,返回后 jr a0 跳到主镜像入口 [S39]。__linker_init 在自重定位前运行,注释明确此时引用外部变量或 GOT 会段错误;它用 getauxval(AT_BASE) 取链接器基址(自运行链接器时为 0,退回用 AT_PHDR/AT_PHNUM 推断),relocate_linker() 完成自重定位并运行链接器自身 ifunc 解析器(此后才可安全使用字符串函数)[S11]。__libc_init_post_relocation 内 __libc_init_main_thread_late() 分配临时 bionic TLS、__set_tid_address(&main_thread.tid)、把 pthread_attr 的 guardsize 设为 0、android_reset_stack_guards() 用 __libc_arc4random_buf_or_die 播种 __stack_chk_guard [S18]。主镜像 _start 调用 __libc_init(raw_args, …, main, structors):再构造 KernelArgumentBlock,若有 fini_array 用 __cxa_atexit(__libc_fini, fini_array, nullptr) 登记,最终 exit(main(argc - Δ, argv + Δ, envp)),Δ = initial_linker_arg_count(链接器被直接运行时为 1)[S19]。libc.so 作为依赖被装载时,__libc_preinit(标记 constructor(1),优先级最高)先跑:从 TLS 取 __stack_chk_guard,再 __libc_init_globals()、__libc_init_common()(设 environ、errno=0、__libc_add_main_thread()、__system_properties_init()、__libc_init_fdsan()/fdtrack())、__libc_init_scudo()、__libc_init_malloc、__libc_init_fork_handler、netdClientInit() [S19][S20]。__libc_init_AT_SECURE 在 setuid 等场景清除不安全环境变量(含 LD_PRELOAD、LD_LIBRARY_PATH、MALLOC_CONF、SCUDO_OPTIONS、TZDIR 等)并补齐标准文件描述符 [S20]。
失效边界:若在 __linker_init 自重定位前访问非 hidden 全局或调用 string.h 函数,会因 GOT 未就绪而段错误(注释 :720-722);__libc_preinit 必须用 noinline 包裹 impl,否则 LTO 内联出的 stack protector 检查会读到尚未初始化的 __stack_chk_guard;__libc_init_main_thread_early 所在文件被强制 -fno-stack-protector/-ffreestanding,若改动其编译选项会破坏早期初始化链 [S18]。linker 被直接当解释器运行时(ldd),argv[0] 不是目标程序,需靠 initial_linker_arg_count 偏移校正 [S11]。
验证条件(可复现,本报告未运行):可复制观察包括 LD_SHOW_AUXV=1 <prog> 打印 auxv(API 35 起链接器支持)[S02];LD_DEBUG=all 或 linker 的 --list(等价 ldd)观察装载顺序 [S12];对某可执行用 readelf -d 确认 NEEDED 列表(如 libc.so、libdl.so);注意 DT_NEEDED 只反映直接依赖,不证明某实现被谁"吸收"(libm 是独立 cc_library)。静态层面可读 linker_main.cpp 的注释与调用序列核对。
成本与权衡:启动链把"最小初始化"前移到链接器自重定位之前,代价是 early 阶段能做的事极少(不能引用非 hidden 全局),收益是 TLS/errno 尽早可用,使链接器自身也能做系统调用;把 libc.so 的初始化做成最高优先级构造函数,代价是它早于任何用户构造函数运行,收益是后续模块可安全依赖 libc 全局。initial_linker_arg_count 的偏移处理让同一份 __libc_init 同时服务直接运行与被 PT_INTERP 拉起两种情形。
补充:主线程收尾与静态构建。__libc_init_main_thread_final()(__libc_init_main_thread.cpp:146)在所有 ELF 模块加载后调用 __allocate_thread_mapping(0, PTHREAD_GUARD_SIZE) 分配主线程的最终静态 TLS(该映射不含栈),再 __init_static_tls、new_tcb->copy_from_bootstrap(temp_tcb)、__set_tls(&new_tcb->tls_slot(0))、__free_temp_bionic_tls,失败路径用 async_safe_fatal("failed to mmap main thread static TLS: %m") [S18]。静态可执行的启动不同:不走链接器,由 libc_init_static.cpp 直接建立主线程并跑 __libc_init。zygote 场景下 android_reset_stack_guards() 公开给 zygote 调用,为每个子进程重置栈 cookie [S18]。这些细节决定"主线程 TLS 何时最终定型"——在 final 之前 TLS 指针指向临时区,任何缓存了旧 TLS 地址的代码在 final 后会失效。
见图 2。
手机可横向滑动查看本图
图 2:动态可执行的启动链(来源:bionic@06356e41 的 linker_main.cpp、begin.S、libc_init_dynamic.cpp [S11][S39][S19])。图把 kernel execve、链接器 _start、__linker_init 两段、库装载与构造、主镜像 _start、__libc_init 与 libc.so 的 __libc_preinit 串成一条竖链。读图要点:其一,TLS、errno、auxv 在 early 阶段就绪,因此链接器在自重定位前后都能做系统调用;其二,libc.so 的初始化以"构造函数"身份在装载期发生,早于主镜像 main;其三,链接器被直接运行时 argv 会带额外偏移(Δ)。
3.3 动态链接器:装载、符号查找、重定位与命名空间
入口与调用链:dlopen/android_dlopen_ext → __loader_dlopen / dlopen_ext(dlfcn.cpp:137,持 g_dl_mutex)→ do_dlopen(linker.cpp:2125)→ find_containing_library / get_caller_namespace → find_library(:1863)→ find_libraries(:1549,BFS)→ find_library_internal(:1462,先按 soname 命中)→ load_library(:1318→:1163,open_library + soinfo_alloc(:315)→ soinfo::soinfo)→ si->call_constructors()(:2251) [S12]。dlsym 走同一符号查找但不重复重定位(resolve_symbol_address 处理 ifunc)[S12][S43]。
关键数据结构与字段:符号查找由 SymbolLookupList(linker_soinfo.cpp:58)组织——先为 DT_SYMBOLIC 预留 slot 0,再依次加入全局组与局部组,begin_/end_ 决定遍历范围;全局组由 get_global_group()(主可执行体 + LD_PRELOAD + DF_1_GLOBAL)构成 [S43]。GNU hash 快路径 soinfo::gnu_lookup(:300)先用 Bloom filter(gnu_bloom_filter_[word_num],word_num = (hash / kBloomMaskBits) & gnu_maskwords_)过滤,再走 gnu_bucket_/gnu_chain_;慢路径 soinfo::elf_lookup(:340)用 bucket_[hash % nbucket_] 链遍历,哈希为 calculate_elf_hash [S43]。版本匹配谓词 check_symbol_version(:106)为 verneed == kVersymNotNeeded ? !(verdef & kVersymHiddenBit) : verneed == (verdef & ~kVersymHiddenBit),kVersymHiddenBit = 0x8000 [S43]。
实现与接口依据:重定位入口 soinfo::relocate(linker_relocate.cpp:593)按固定顺序:先 RELR(DT_RELR/DT_ANDROID_RELR,偶数项=下一偏移、奇数项=位图),再 Android packed relocations(要求头四字节 APS2,用 sleb128 分组解码),再常规 RELA/REL(JUMP_SLOT/ABSOLUTE/GLOB_DAT/RELATIVE/IRELATIVE(ifunc)/TLS_*/TLSDESC),最后 TLS 延迟写回 [S13]。架构映射集中在 linker_relocs.h:riscv64 把 R_GENERIC_* 映射到 R_RISCV_JUMP_SLOT、R_RISCV_64(ABS 与 GLOB_DAT 共用)、R_RISCV_RELATIVE、R_RISCV_IRELATIVE、R_RISCV_TLS_DTPMOD64/DTPREL64/TPREL64、R_RISCV_TLSDESC;R_GENERIC_COPY 分支显式拒绝,报 COPY relocations are not supported [S14][S13]。RELRO:link_image 在重定位后 protect_relro()(改 PT_GNU_RELRO 为只读),链接器自身延后到 __linker_init_post_relocation;DT_BIND_NOW 被显式忽略(注释称被 DF_BIND_NOW 取代),且 bionic 无惰性绑定,装载时一次性完成全部重定位(含 PLT)[S12]。命名空间由 ld.config 决定:init_default_namespaces(linker.cpp:3668)读 Config::read_config_for_binary 并装配,无配置退回 init_default_namespace_no_config;LD_LIBRARY_PATH/LD_PRELOAD 仅在非 setuid/setgid 时解析 [S12]。构造函数顺序由 soinfo::call_constructors(linker_soinfo.cpp:464)保证:先递归所有 children(依赖),再 DT_INIT,后 DT_INIT_ARRAY(析构相反);pre-init 仅主可执行体生效 [S43]。
失效边界:未知重定位类型会落入 default 分支报 unknown reloc type %d in "<path>"(linker_relocate.cpp:529);R_GENERIC_COPY 报 COPY relocations are not supported;Android packed relocs 头不是 APS2 时报 bad android relocation header. [S13]。RISC-V 未在通用路径枚举、且未纳入 R_GENERIC 映射的专用重定位(如 R_RISCV_32、R_RISCV_*_HI20/LO12、R_RISCV_ALIGN)若出现在动态重定位表,会落入 default 报未知类型(此为推断,未见测试直证)[S13][S14]。DT_ANDROID_REL[A] 在错误平台上被禁(RELA 平台禁 DT_ANDROID_REL),属构建期约束 [S13]。
验证条件(可复现,本报告未运行):可复制观察包括 readelf -d <lib> 看 DT_RELR/DT_ANDROID_RELA/DT_FLAGS_1;LD_DEBUG=lookup 观察符号查找(linker_debug.cpp 的合法 tag 为 calls/cfi/dynamic/lookup/props/reloc/statistics/timing/all,没有 glibc 风格的 symbols,用 symbols 只会打印合法选项);linker --list <prog>(等价 ldd)看依赖解析;对某符号用 nm -D/objdump -R 核对重定位类型;按 get_ld_config_file_path(linker.cpp:3607)的实际优先级核对命名空间配置:LD_CONFIG_FILE(仅调试构建)→ APEX 路径(get_ld_config_file_apex_path)→ /system/etc/ld.config.<abi>.txt → /linkerconfig/ld.config.txt → vndk 路径 → /system/etc/ld.config.txt(兜底),不要只查最后一个 [S12]。
成本与权衡:符号查找的成本主要由 hash 算法与库数量决定——GNU hash 的 Bloom filter 让未命中快速失败,但慢路径(SysV/ELF hash)与版本校验会显著增加单次查找开销;重定位成本与库的 RELA/RELR 数量、是否使用 APS2 压缩相关;RELRO 在装载期多做一次 mprotect 换取运行期只读(安全性收益),而"无惰性绑定"牺牲了首屏延迟的一部分换取运行期确定性。g_dl_mutex 是全局递归锁,把所有 __loader_* 入口串行化,是并发 dlopen 场景下的关键成本点 [S12]。
补充:dlclose、dlextinfo 与 16KiB 兼容。dlclose 同样在 g_dl_mutex 下运行,通过 soinfo 引用计数协调卸载 [S12]。android_dlopen_ext 允许传入 android_dlextinfo(如 ANDROID_DLEXT_USE_LIBRARY_FD、ANDROID_DLEXT_FORCE_LOAD、ANDROID_DLEXT_WRITE_RELRO/USE_RELRO),分别落入 load_library 的 fd 路径或 RELRO 序列化/映射分支 [S12]。16KiB 页兼容是近年新增的一类处理:linker_phdr_16kib_compat.cpp 在段重叠时退回 4KiB,a17 又新增了 LOAD 段重叠检测(prev_load_end > page_start(...) 则退回 4KiB)[S12]。ProtectedDataGuard(linker.cpp:470-489)用嵌套计数控制元数据内存权限:最外层构造时 protect_data(PROT_READ | PROT_WRITE)(把 soinfo/soinfo_links/namespace 等分配器内存临时置为可写,以便装载过程写元数据),最外层析构时 protect_data(PROT_READ)(恢复只读);嵌套调用只加减 ref_count_,溢出时 async_safe_fatal("Too many nested calls to dlopen()")。即平时元数据只读、装载期临时可写——这与 ELF 的 PT_GNU_RELRO(重定位后把段永久改只读)是两件不同的事 [S12]。定位"某符号为何解析到意外库"的可复述链是:LD_DEBUG=lookup() → 观察 lookup 顺序() → 核对 ld.config 命名空间() → 查 SymbolLookupList 的全局/局部组() [S12]。
见图 3。
手机可横向滑动查看本图
图 3:dlopen 装载链、符号查找与重定位关键路径(来源:bionic@06356e41 的 dlfcn.cpp、linker.cpp、linker_relocate.cpp、linker_soinfo.cpp [S12][S13][S43])。图左是装载链,图右是符号查找(GNU/SysV hash、版本校验、查找顺序),中间是 soinfo::relocate 的四步顺序与 RISC-V 映射,底部是 RELRO/绑定与构造顺序/锁。读图要点:其一,符号查找与重定位是两条独立路径,dlsym 走查找但不重复重定位;其二,RISC-V 的重定位类型都被通用映射覆盖,除 COPY 明确不支持外,未枚举的 RISC-V 专用重定位会报未知类型(推断);其三,g_dl_mutex 与 ProtectedDataGuard 共同保护装载与元数据写。
3.4 系统调用、ABI、vDSO 与 RISC-V 特征探测
入口与调用链:syscall 包装的生成链是 libc/SYSCALLS.TXT → gensyscalls.py(构建期 genrule)→ 各架构汇编 wrapper;手工部分在 libc/arch-riscv64/bionic/syscall.S:syscall()(a0=号, a1..a6=参数)→ mv a7,a0 → 参数下移 a0<-(a1) … a5<-(a6) → ecall → DO_SYSCALL_RETURN [S31][S17]。vDSO 链是 __libc_init_vdso(读 AT_SYSINFO_EHDR → SHT_DYNSYM → DT_SYMTAB/DT_STRTAB 匹配符号)→ __libc_globals->vdso[name].fn,调用时 clock_gettime/clock_getres/gettimeofday → vdso 命中则直返、未命中回落 __clock_gettime 等 syscall [S16]。RISC-V 探测链是 __riscv_hwprobe(若 vDSO 有 VDSO_RISCV_HWPROBE 则用,否则内联 ecall:a0..a4 传参、a7=__NR_riscv_hwprobe),两条路径都把内核返回的负值取反,故函数返回"0 表示成功、正整数为错误号(errno 数值)"而非设置 errno [S16][S41]。
关键数据结构与字段:SYSCALLS.TXT 的格式为 func_name[|alias][:syscall_name[:socketcall_id]](params) arch_list,arch 列表含 riscv64,脚本按参数类型计算所需寄存器(含 ILP32 的 64 位参数对)[S17]。errno 不是独立 TLS 槽,而是线程结构字段:__errno() 返回 &__get_thread()->errno_value(__errno.cpp:34),汇编 stub 用 __set_errno_internal 把负返回值转成 errno = -r; return -1 [S18]。vDSO 解析用 ELF 结构:ElfW(Ehdr)/ElfW(Shdr)/ElfW(Dyn)/ElfW(Sym),以 VDSO_END 为枚举上界逐一匹配符号名 [S16]。sys/hwprobe.h 仅在 __riscv 下生效,包含 uapi 的 <asm/hwprobe.h> 并声明 int __riscv_hwprobe(struct riscv_hwprobe*, size_t, size_t, cpu_set_t*, unsigned) [S41]。
实现与接口依据:__riscv_hwprobe 的实现直接给出寄存器约定——register long a0..a4 依次为 pairs/pair_count/cpu_set_size/cpu_set/flags,a7 = __NR_riscv_hwprobe,__asm__ volatile("ecall" ...) 后 return -a0——内核失败时 a0 为负的 -errno,取反后即得到正的错误号,成功返回 0;vDSO 路径同样是 return -vdso_riscv_hwprobe(...)。正错误号的实例是"非法 flags":自测 tests/sys_hwprobe_test.cpp:192-197 的 sys_hwprobe.__riscv_hwprobe_fail 用 __riscv_hwprobe(probes, 0, 0, nullptr, ~0) 断言得到 EINVAL。注意"未知 key"不是错误:官方 hwprobe 约定对 unknown key 清零 key=-1、value=0 以保持前向兼容(见 docs.kernel.org/arch/riscv/hwprobe.html),调用方不应把"问了个不认识的能力"当作失败 [S16][S41]。sys/hwprobe.h 的注释为 "Returns 0 on success and returns an error number on failure",与实现一致 [S16][S41]。代码注释解释了为何要内联而非用生成的 syscall 包装:IFUNC 解析器里失败无法设置 errno,而本 API 的约定就是"返回错误值、不设 errno" [S16]。IFUNC 支持在 bionic_call_ifunc_resolver.cpp:60 的 __riscv 分支:解析器类型为 (uint64_t, __riscv_hwprobe_t, void*),传入 (getauxval(AT_HWCAP), __riscv_hwprobe, nullptr)(第三参保留,与 glibc 一致)[S15]。docs/status.md 记录 API 35 加入 __riscv_hwprobe/__riscv_flush_icache 包装 [S02]。
失效边界:编译器 ISA 与运行分派的混淆是主要陷阱——riscv64 的字符串例程直接写 RVV 指令、无 hwprobe 运行分派,因此"源码里有 RVV"只说明构建期启用了向量 ISA;若目标平台没有向量扩展,这些例程不会存在替代实现(不同于 ARM64 有 hwcap 分支)[S07][S09]。__riscv_hwprobe 若 vDSO 无对应符号则走内联 ecall;无论哪条路径,它都返回正整数错误号且不设置 errno,因此"查 errno 判失败"的写法对它无效,必须直接判返回值是否非 0 [S16][S41]。vDSO 若解析失败(无 AT_SYSINFO_EHDR 或符号缺失),clock_gettime 会回落 syscall,表现为延迟升高而非错误 [S16]。
验证条件(可复现,本报告未运行):可复制观察包括 strace -e trace=clock_gettime,gettimeofday <prog> 看是否仍发系统调用(走 vDSO 则不出现);LD_SHOW_AUXV=1 <prog> 看 AT_SYSINFO_EHDR/AT_HWCAP [S02];用 getauxval/__riscv_hwprobe 写小工具打印能力位;对某 syscall 用 strace 核对实际发出的 syscall 号与参数 [S17]。
成本与权衡:vDSO 的价值在于把高频时钟读取从 syscall 路径改为纯用户态执行,省去陷内核的开销(本报告未测量具体量级);__riscv_hwprobe 的"vDSO 优先、内联 ecall 兜底"设计兼顾了首次调用(可能在链接器 early 阶段)的可用性;把 errno 放到线程结构而非独立 TLS 槽,减少了 TLS 槽数量但增加了通过 __get_thread() 的一次间接。是否引入运行分派(IFUNC)是重要权衡:分派带来按硬件择优的收益,但 IFUNC 解析在链接器自重定位前运行、约束多、且增加装载期开销 [S11][S15]。
补充:RISC-V 包装与 vDSO 符号面。SYSCALLS.TXT 为 riscv64 生成的包装覆盖常规 syscall,另有专门入口如 __riscv_flush_icache(<sys/cachectl.h>)与 __riscv_hwprobe(<sys/hwprobe.h>,API 35 起)[S02][S41]。vDSO 的解析面由 private/bionic_vdso.h 的枚举决定,包含 VDSO_CLOCK_GETTIME、VDSO_CLOCK_GETRES、VDSO_GETTIMEOFDAY,以及仅 x86 的 VDSO_TIME、仅 riscv 的 VDSO_RISCV_HWPROBE [S16]。__riscv_hwprobe 的语义是"传入一组 (key,value) 对、按 pair_count 批量探测、可用 cpu_set 限定",bionic 包装在成功时返回 0、失败时返回正的错误号(errno 数值)而非设置 errno(vdso.cpp 的 vDSO 路径与内联 ecall 路径都做了取反),调用方要区分"不支持"与"错误"须直接判返回值 [S16][S41]。具体例子:若在某 riscv 平台上 memcpy 命中 vsetvli/vle8.v,只能说明该 libc.so 构建时启用了向量 ISA;它既不代表运行时做了能力探测,也不代表没有向量扩展时存在回退——这两件事在 bionic 中都不成立 [S07][S15]。
补充:生成脚本与返回约定。SYSCALLS.TXT 的 arch_list 支持 all 或具体架构集合,脚本按参数类型计算寄存器需求(含 ILP32 上 64 位参数需寄存器对、arm32 需从偶数寄存器起)[S17]。riscv64 的 syscall() 汇编额外做了参数重排:把 syscall 号从 a0 移到 a7、把参数 a1..a6 顺次下移,再 ecall,返回经 DO_SYSCALL_RETURN——该宏把内核的负返回值翻译成 errno 与 -1(由 __set_errno_internal 完成),因此上层看到的是 POSIX 风格返回而非原始负值 [S31][S18]。__riscv_hwprobe 走的是另一条路径:它不设 errno,而是返回"0 或正错误号"(内核负值被取反),与普通 syscall 包装"失败设 errno 并返回 -1"的约定不同——同一份代码里两种约定并存,是绕 IFUNC/early 阶段限制的结果 [S16][S41]。AT_HWCAP 在 riscv 上传的是基础扩展位,配合 __riscv_hwprobe 的细粒度探测可覆盖更完整的 CPU 能力面 [S15][S16]。
3.5 线程、TLS、futex 与原子操作
入口与调用链:pthread_create → __allocate_thread_mapping(pthread_create.cpp:221,一次 mmap:stack+guard+static TLS+guard+libgen)→ __allocate_thread(定位 tcb/tls/lb → __init_static_tls → __init_tcb → __init_tcb_dtv → __init_tcb_stack_guard → __init_bionic_tls_ptrs → __init_libgen_buffers_ptr)→ clone(__pthread_start, child_stack, flags, thread, &thread->tid, tls, &thread->tid) → 子线程 __pthread_start → 用户 start_routine → pthread_exit [S21]。pthread_exit → __cxa_thread_finalize → cleanup handlers → pthread_key_clean_all → 禁用并 munmap 备用信号栈 → 置 THREAD_EXITED_NOT_JOINED → __free_dynamic_tls → detached 则 _exit_with_stack_teardown 否则 __exit(0) [S21]。pthread_join → CAS 到 THREAD_JOINED → while(*tid_ptr) __futex_wait(tid_ptr, tid, nullptr) → __pthread_internal_remove_and_free [S23]。
关键数据结构与字段:线程主映射布局自高向下为 [PTHREAD_GUARD_SIZE][libgen][static TLS][stack_size][stack_guard_size],字段 static_tls = libgen_buffers - layout.size()、stack_top = static_tls [S21]。clone 的 flags = CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID,其中 CLONE_CHILD_CLEARTID 让内核在线程退出时清零 thread->tid,正是 pthread_join 等待的地址 [S21][S23]。riscv64 用通用整数寄存器 tp(x4) 作线程指针(libc/platform/bionic/tls.h:72-82 用 mv %0, tp / mv tp, %0;tp 不是 CSR,勿与 fcsr/fflags 等真实 CSR 混淆),采用 Variant I(tp 指向 TCB 末尾之后,槽位负偏移:MIN_TLS_SLOT=-10、TLS_SLOT_DTV=-8、TLS_SLOT_THREAD_ID=-7、TLS_SLOT_STACK_GUARD=-3、MAX_TLS_SLOT=-1)[S25]。动态 TLS 走每线程 DTV:TlsDtv{count,next,generation,modules[]},快路径比对本地 __libc_tls_generation_copy 与该 DTV 的 generation [S03]。互斥量 64 位表示为 _Atomic(uint16_t) state 加 owner_tid(或 PI 版本),状态位含低 2 位锁状态、bits12-2 计数、bit13 shared、bits15-14 类型 [S22]。
实现与接口依据:锁的 fast path 是 pthread_mutex_lock(:827)先判普通锁再 NonPI::NormalMutexTryLock(:560)做单次 CAS(atomic_compare_exchange_strong_explicit(..., memory_order_acquire, memory_order_relaxed));慢路径(:585-616)先一次 NormalMutexTryLock,失败即进入 while (atomic_exchange_explicit(&mutex->state, locked_contended, acquire) != unlocked) { __futex_wait_ex(...) } 重试环——无用户态忙等,exchange 失败即 futex 等待、wait 返回后再重试;解锁用 atomic_exchange_explicit(..., memory_order_release) 原子释放;仅当返回值为 locked_contended(已有人标记争用)才 __futex_wake_ex,返回 locked_uncontended(无争用)不产生系统调用——这是"无争用解锁纯用户态"的依据 [S22]。PI 路径把状态写为 PI_MUTEX_STATE,锁分派在 :833:old_state == PI_MUTEX_STATE 走 PIMutexTryLock 否则 PIMutexTimedLock(内部优先 FUTEX_LOCK_PI2,旧内核回退 FUTEX_LOCK_PI)[S22]。futex 底层是内联 __futex()(libc/private/bionic_futex.h),成功后保存/恢复 errno(pthread 不希望被设 errno),op 为 FUTEX_WAIT_BITSET[_PRIVATE]、FUTEX_WAKE[_PRIVATE]、FUTEX_*_PI 等 [S23]。原子方面,bionic 不手写原子汇编,统一用 C11 <stdatomic.h> 的 atomic_*_explicit(获取 acquire、释放 release、计数 relaxed);对 arch-riscv64/arch-arm64 全树检索 lr.[wd]/sc.[wd]/amo*/ldxr/stlxr 均无命中 [S22]。
失效边界:对 invalid pthread_t 的用法在目标 SDK ≥26 时会 abort 并报 attempt to use invalid pthread_t;对已销毁 mutex 在目标 SDK ≥28 会 __fortify_fatal(报 called on a destroyed mutex)[S02][S22]。pthread_join 等待的是被 CLONE_CHILD_CLEARTID 清零的 tid,若线程被标记 detached 则其结构随栈一起 unmap,对它的 pthread_join/pthread_kill 不安全 [S02]。riscv64 的原子指令落点是明确的未决点:源码层无法证明具体用 AMO 还是 LR/SC(推断为编译器 lowering 结果),必须反汇编 libc.so 才能确认 [S22]。
验证条件(可复现,本报告未运行):可复制观察包括 nm -D libc.so | grep -E "pthread_(create|mutex|once)";对 pthread_mutex_lock 用 objdump -d 查看 fast path 的 CAS 序列;bionic-unit-tests --gtest_filter=*pthread*/*mutex* 做正确性回归;用 strace -e trace=futex 观察 futex op;用 /proc/self/task 与 gettid 核对线程数 [S21][S22]。riscv64 原子形态需 objdump -d /system/lib64/libc.so 找 amoadd.w/lr.w/sc.w。
成本与权衡:fast path 用单次 CAS、无系统调用,是绝大多数无争用场景的成本基线;一旦竞争,__futex_wait_ex 陷入内核,成本量级上升,因此"减少争用/缩短临界区"比"改 fast path 指令"收益更确定。TLS 采用 Variant I 与负偏移槽位,让 tp 之外的静态 TLS 访问是几次 load,代价是与 ELF TLS 通用布局不同、需专门处理;atomic 全走 C11 内建的代价是机器码形态不透明(需反汇编),收益是跨架构统一与可维护 [S22][S25]。
补充:创建握手与条件变量/读写锁细节。线程建立用一个启动握手锁同步、且方向是父线程掌握放行:父线程在 pthread_create 尾部 init(false) 后 lock() 持有它(pthread_create.cpp:459-460),子线程在 __pthread_start 用 lock() 阻塞等待(:399,子线程全程不 unlock),父线程发布 pthread_t 后 unlock(:519;失败路径 :498/:513 也 unlock)才放行子线程——屏障由子线程的 acquire 获得,起步时机由父线程决定 [S21]。条件变量的 __pthread_cond_pulse 用 atomic_fetch_add_explicit(&cond->state, COND_COUNTER_STEP, ...) 递增计数后 __futex_wake_ex,注释刻意说明不加 release/seq_cst fence;等待路径为 atomic_fetch_add_explicit(&cond->waiters,1,...) → pthread_mutex_unlock → __futex_wait_ex → pthread_mutex_lock [S22]。读写锁状态字含 bit31 writer、bits30-2 reader_count、bit1 pending_writers、bit0 pending_readers,tryrdlock/trywrlock 用乐观 CAS,超时等待用 __futex_wait_ex(&rwlock->pending_reader_wakeup_serial, ...) [S22]。pthread_once 用 0/1/2 状态与 atomic_compare_exchange_weak_explicit(..., ONCE_INITIALIZATION_UNDERWAY, memory_order_acquire, memory_order_acquire),完成后 atomic_store_explicit(..., memory_order_release) 并 __futex_wake_ex(once_control_ptr, 0, INT_MAX);末尾有 __strong_alias(call_once, pthread_once)(a17 新增,对齐 C23)[S22]。这些细节说明:同步原语的快路径是纯用户态原子、慢路径才陷入 futex,优化应优先减少争用而非常规路径指令。
补充:默认尺寸与 riscv 栈拆卸。默认线程 stack_size = PTHREAD_STACK_SIZE_DEFAULT = (1*1024*1024) - 32KiB、guard_size = PTHREAD_GUARD_SIZE = max_android_page_size();guard 尺寸会被 __builtin_align_up 对齐到页,若对齐后溢出返回 EAGAIN [S21]。线程内部链表由 g_thread_list + 读写锁 g_thread_list_lock 管理,__pthread_internal_free 对 mmap_size != 0 执行 munmap(mmap_base, mmap_size);detached 线程退出走 libc/arch-riscv64/bionic/_exit_with_stack_teardown.S,实现是 li a7, __NR_munmap; ecall; …; li a7, __NR_exit; ecall——先 unmap 自身栈所在映射再 exit,全程不触碰已释放内存 [S21][S32]。创建期由全局 g_thread_creation_lock 串行化线程创建;子线程何时可运行则由 startup_handshake_lock 的放行方向决定(父持锁→子 lock() 等待→父发布 pthread_t 后 unlock,见上),二者职责不同 [S21]。
补充:TLS 与 join 的联动。riscv64 的 StaticTlsLayout 在 bionic_elf_tls.cpp 用 reserve_tp_pair 把 bionic_tcb 放在 tp 之前、可执行文件段放在 tp 之后,并有 static_assert(MAX_TLS_SLOT == -1, "Last slot of bionic_tcb must be slot #(-1) on riscv") 固化约定;__libc_get_static_tls_bounds 据此用 __get_bionic_tcb() 减 layout.offset_bionic_tcb() 得静态 TLS 起止 [S25][S03]。pthread_join 依赖内核清零的 tid(CLONE_CHILD_CLEARTID)而非 bionic 主动通知,因此即使线程已被回收、只要 tid 未清零就会等待下去——这也解释了为何 join 一个 detached 线程是不安全的 [S23][S02]。
补充(v1.4):riscv64 原子形态的离线 lowering 观察(本报告实际执行的编译,非设备测量)。M5 的"riscv64 原子究竟是 AMO 还是 LR/SC"在 bionic 源码层不可证。按公开工具链 + 最小样例做实际离线编译并阅读 .s(样例/命令/产物与 SHA 见 离线编译记录:clang --target=riscv64-linux-gnu -march=rv64gc -O2 -S;Ubuntu clang 18.1.8,sha256(/usr/bin/clang)=15a48145…;样例 810f7ec4…、汇编 58cd9381…)。这是"编译器 lowering"形态,不等于系统 libc 的机器码(未取匹配 libc 产物、未上板)。观察结果:atomic_compare_exchange_strong_explicit(p,&e,1,acquire,relaxed) 生成 lr.w.aq/sc.w(LR/SC 序列);atomic_fetch_add_explicit(p,1,relaxed) 生成 amoadd.w(AMO);atomic_exchange_explicit(p,v,acquire) 生成 amoswap.w.aq(AMO);atomic_store_explicit(p,3,release) 生成 fence rw,w。即在该 flags 下"无比较的 RMW 用 AMO、CAS 用 LR/SC",与 bionic"不手写原子汇编、交给编译器 lowering"的取向一致。边界:本 pin 默认平台 clang(clang-r584948,见 §3.9)与 host clang 18.1.8 在具体指令、-march、是否启用 Zacas 上可能不同,故此结果只支撑"形态判断",不构成"系统 libc 机器码"的证明——后者仍需反汇编该 libc.so(本报告未上板)[S22]。
3.6 原生堆与分配器(Scudo)
入口与调用链:malloc → GetDispatchTable()(读 __libc_globals->current_dispatch_table,acquire)→ 非空走调试层 table、为空走编译期宏 Malloc(malloc)(=scudo_malloc)→ Scudo::allocate [S26];free 同理,先 MaybeUntagAndCheckPointer(mem) 再分派 [S26]。装载期:__libc_init_malloc → MallocInitImpl → MaybeInitGwpAsanFromLibc → CheckLoadMallocDebug/CheckLoadMallocHooks → HeapprofdShouldLoad [S26]。Scudo 侧 Allocator::init()(combined.h:145):initFlags() → 把 flags 映射为 OptionBit 与填充模式(如 may_return_null、dealloc_*;zero_contents 经 setFillContentsMode(ZeroFill) 映射为 FillContents 模式,而不是某个叫 zero_contents 的 OptionBit)→ Primary.init(ReleaseToOsIntervalMs)/Secondary.init(...) → 若未编译掉 quarantine 则初始化 [S27]。
关键数据结构与字段:构建期选择由 libc/Android.bp 决定——libc_defaults 硬编码 -DUSE_SCUDO,libc_native_allocator_defaults 以 whole_static_libs: ["libscudo"] + -DUSE_SCUDO 把 Scudo 整体静态链入(libscudo 的 shared.enabled=false,非独立 .so);产品变量 malloc_low_memory 打开时改 jemalloc(-UUSE_SCUDO + libjemalloc5 + libc_jemalloc_wrapper)[S26]。malloc_common.h 里 Malloc(function) 宏在 USE_SCUDO 下展开为 scudo_##function(USE_SCUDO_SVELTE 分支在本版本无定义,属死代码)[S26]。Scudo 配置由 custom_scudo_config.h 用 #if defined(SCUDO_LOW_MEMORY) 选 AndroidLowMemoryConfig/AndroidNormalConfig;普通 64 位 size-class 参数 NumBits=7、MinSizeLog=4、MidSizeLog=6、MaxSizeLog=16、SizeDelta=16,最小类 32B、最大 primary 对象约 65552B,更大走 secondary [S27]。chunk 头是 64 位紧凑 UnpackedHeader{ClassId:8, State:2, OriginOrWasZeroed:2, SizeOrUnusedBytes:20, Offset:16, Checksum:16} [S28]。
实现与接口依据:bionic_allocator.cpp 并非 malloc——它自述"提供与 malloc/free/realloc/memalign 相同的功能",但仅被 libc 与链接器内部使用(linker/linker_memory.cpp 的 g_bionic_allocator、ELF TLS 的 tls_allocator),大于 1KiB 直接 mmap,小对象按 2 的幂进小对象分配器;全树无 __libc_malloc/__libc_free 符号 [S26]。release-to-OS 有一处易错点:flags.inc 里 release_to_os_interval_ms 默认 5000,但 primary64.h:372 用配置默认覆盖 flag,而 AndroidNormalConfig 的 DefaultReleaseToOsIntervalMs 为 1000(secondary 为 0),因此普通目标 primary 实际是 1000ms [S27][S28]。quarantine 在普通 Android 目标默认被编译掉(QuarantineDisabled=true),故 flags 的三个 quarantine 值默认为 0 不参与 [S27]。内存来源统一为 mmap(MAP_PRIVATE|MAP_ANONYMOUS),Android 下用 prctl(PR_SET_VMA,...) 命名 VMA(scudo:primary 等),归还物理页用 madvise(MADV_DONTNEED)(失败则手动清零)[S28]。mallopt 选项含 M_DECAY_TIME、M_PURGE、M_PURGE_ALL、M_PURGE_FAST(API 37 新增)、M_MEMTAG_TUNING、M_BIONIC_ZERO_INIT、M_BIONIC_SET_HEAP_TAGGING_LEVEL、M_LOG_STATS;Scudo 侧 scudo_mallopt 把 M_DECAY_TIME 映射为 ReleaseInterval(1→INT32_MAX),M_PURGE* 映射为 releaseToOS 的不同强度 [S28][S49]。android_mallopt 是另一入口,按 opcode 分派;静态 libc 只支持四个 opcode:M_GET_DECAY_TIME_ENABLED、M_INITIALIZE_GWP_ASAN、M_MEMTAG_STACK_IS_ON、M_SET_ALLOCATION_LIMIT_BYTES(其后 #if defined(LIBC_STATIC) 一律 errno = ENOTSUP);M_SET_ZYGOTE_CHILD、M_GET_MALLOC_LEAK_INFO 等其余只存在于动态 libc [S49]。
失效边界:MALLOC_USE_APP_DEFAULTS 未设时命令行工具的分配器行为与应用不同(应用自 API 26 起默认不同、该变量自 API 36 起可用),直接用命令行基准比较会失真 [S05][S26]。GWP-ASan 必须最先装,否则与 heapprofd/malloc_debug 冲突 [S26]。riscv64 不参与 heap tagging:AndroidNormalConfig::MaySupportMemoryTagging 仅对 __aarch64__ 为真 [S27]。madvise(MADV_DONTNEED) 失败时 Scudo 手动 memset 清零,保证语义但成本上升。
验证条件(可复现,本报告未运行):可复制观察包括 getprop libc.debug.malloc.options 与 getprop libc.debug.malloc.program 判断调试层是否启用;mallopt(M_LOG_STATS, 0) 打印分配器统计;cat /proc/<pid>/maps | grep scudo 看 region 命名与大小 [S28];bionic-benchmarks --benchmark_filter=malloc 复现分配微基准(需锁核锁频)[S29];memory_replay64 <trace.zip> 跑真实轨迹(见 3.12)[S05]。
成本与权衡:Scudo 用 primary(小对象、按 size class 的 region)+ secondary(大对象、map)两段式设计换取分配速度与隔离性;primary release 间隔 1000ms 是"少回收省系统调用"与"控 RSS"的折中;quarantine 编译掉换来更快 free 与更低内存,代价是释放后悬挂读的检出能力下降。调试与采样层的装入方式不同:libc_malloc_debug/libc_malloc_hooks(heapprofd 亦经此路径)按配置经运行时 dlopen 装载 hook 库(未启用时不装载该库);GWP-ASan 不是 dlopen 插件——它由 gwp_asan_wrappers.cpp 编入 libc 并静态依赖 gwp_asan(libc/Android.bp:1544/1562、:1610/1620),MallocInitImpl 直接调 MaybeInitGwpAsanFromLibc(malloc_common_dynamic.cpp:383/387)并把 gwp_asan_dispatch 设为分派表(gwp_asan_wrappers.cpp:463-469),且可与 hooks 叠加(heapprofd/malloc_debug 安装时以当前分派表为 prev_dispatch,见 :330-337)。这些配置会改变分配路径;额外成本取决于启用方式、采样率与负载,应在目标系统测量——这也是"基准模式差异"的来源之一 [S26][S49]。
补充:region 布局与 secondary/调试层细节。primary 为每个 size class 预留连续 region(CHECK(ReservedMemory.create(..., RegionSize * NumClasses, "scudo:primary_reserve"))),AndroidNormalConfig 的 RegionSizeLog=28、GroupSizeLog=20 使每 class region 为 256MiB,EnableRandomOffset=true 使起始偏移随机化 [S27]。secondary(大对象)的 cache 配置为 EntriesArraySize=256、DefaultMaxEntriesCount=32、DefaultMaxEntrySize=2<<20,releaseToOS() 含 ForceFast(走 tryLock)变体;低内存配置用 MapAllocatorNoCache(无 cache)[S27]。quarantine 的批量上限为 8KiB(QuarantineBatch),普通目标整体编译掉 [S27]。调试层与采样:libc_malloc_debug/libc_malloc_hooks 经 libc.debug.malloc.options 与 libc.debug.malloc.program 触发,MallocInitImpl 优先加载它们,否则 HeapprofdShouldLoad() 时装 heapprofd;GWP-ASan 必须最先装 [S26]。jemalloc_wrapper.cpp 仅在 malloc_low_memory(Android Go)目标启用,为 jemalloc 补齐 M_DECAY_TIME/M_PURGE*/je_aligned_alloc_wrapper/je_reallocarray 等 Android 语义 [S26]。
补充:flags 解析优先级与低内存命名。Scudo 的 initFlags() 解析来源优先级为:编译期 SCUDO_DEFAULT_OPTIONS → 弱符号回调 __scudo_default_options() → 环境变量 SCUDO_OPTIONS(另有单值 SCUDO_ALLOCATION_RING_BUFFER_SIZE);Options 是运行期原子 bitmap(OptionBit 含 MayReturnNull/FillContents/Dealloc*/TrackAllocationStacks/UseOddEvenTags/UseMemoryTagging/AddLargeAllocationSlack)[S27]。命名上有一处历史沿革:低内存配置在 Android V(API 35)前用 MALLOC_SVELTE := true 于 BoardConfig.mk 开启,之后改为 MALLOC_LOW_MEMORY := true [S05]。mem_map_linux.cpp 的映射封装在 Addr 非 0 时加 MAP_FIXED、MAP_NOACCESS 时用 MAP_NORESERVE + PROT_NONE,Android 下用 prctl(ANDROID_PR_SET_VMA, ...) 命名 VMA [S28]。
跨版本核对:本次补取 external/scudo 的 Android 16 快照(android-16.0.0_r4,commit 03a49bce)与当前主线(15715589)做同仓库对比,config/custom_scudo_config.h 文件整体有差异,但本报告引用的关键常量(QuarantineDisabled = true、DefaultReleaseToOsIntervalMs 的 primary 1000 / secondary 0、MaySupportMemoryTagging 的 aarch64 与其它分支)在两者间一致;Android.bp 与 custom_scudo_tracing.h 有改动。即"分配器选择与核心分配参数"在两版本间稳定,仅周边配置演进 [S27]。
补充:释放阈值与监测入口。secondary 的释放阈值 MaxUnreleasedCachePages = 4U,primary 的释放阈值算法在 primary64.h:330 附近(含 97% 阈值与最小 size class 启发式,本报告只读注释未展开公式)[S28]。监测上,mallopt(M_LOG_STATS, 0) 触发 printStats()+printFragmentationInfo()(wrappers_c.inc:299),mallinfo 系列被 ART 使用、故评估新分配器时要求其性能与现分配器同量级 [S05][S28]。android_mallopt 的 opcode 常量表在 libc/platform/bionic/malloc.h(如 M_SET_ALLOCATION_LIMIT_BYTES 是 opcode 而非 mallopt 选项)[S49]。这些入口是排查"RSS 偏高/分配抖动"时的第一手观测点。
3.7 string/memory:通用路径与 RISC-V 向量实现
入口与调用链:调用者 memcpy(dst, src, n) → 若命中 FORTIFY 重载则 __memcpy_chk(n>dst_len 时 __memcpy_chk_fail)→ libc 导出的 memcpy →(riscv64)arch-riscv64/string/memcpy.S 的 RVV 循环 [S07]。字符串类同理:strlen/strchr/strchrnul/strrchr → vle8ff.v 循环 + vfirst.m;strcmp → mf2 起步、按 iLMUL 阶梯升到 m1/m2/m4 [S07][S08]。ARM64 侧则是 memcpy → IFUNC shim → 解析器按 hwcap 选 __memcpy_aarch64_simd / _nt / _mops / 标量 [S09]。
关键数据结构与字段:riscv64 memcpy 的实现(arch-riscv64/string/memcpy.S:74)为:
ENTRY(memcpy)
mv p, dst
L(loop):
vsetvli iVL, n, e8, m8, ta, ma
vle8.v v0, (src)
sub n, n, iVL
add src, src, iVL
vse8.v v0, (p)
add p, p, iVL
bnez n, L(loop)
ret
END(memcpy)
vsetvli iVL, n, e8, m8, ta, ma 以 8 倍 LMUL 的字节元素配置向量长度(iVL 同时是元素个数寄存器),ta/ma 为 tail/mask agnostic;无对齐处理、无短/中/长尺寸分派 [S07]。字符串类用 vsetvli iVL, zero, e8, <LMUL> 表示按 VLEN 自适应的不设界循环,并以 vle8ff.v(Unit-stride Fault-Only-First Load)处理页边界:element 0 的同步异常仍会 trap,后续元素遇异常可缩短本次 vl,从而把"后续元素落在不可读页"从故障转为短路——有效字符串借此避免把后续跨页探测变成不必要故障,但不是"永不故障";普通 memcpy 用 AVL=n 限定有效字节,未对齐的处理与代价、以及"调用越界"是两回事 [S07][S08][S64]。LMUL 汇总:memchr/memcmp/memmove=m8(#define ELEM_LMUL_SETTING m8)、strlen=m2、strcmp 为 mf2→m1→m2→m4 阶梯、其余多为 m1;memcpy 与 memset 均硬编码 m8(memset.S:78/85 的 vsetvli iVL, n, e8, m8, ta, ma 置于循环首尾两处),stpncpy 另有补零段 ZERO_FILL_ELEM_LMUL_SETTING m8(stpncpy.S:70/102/106)[S07][S08]。
实现与接口依据:ARM64 侧 bionic 只有 arch-arm64/string/__memcpy_chk.S、__memset_chk.S 与 oryon/ 非临时变体,memcpy/memset 主体来自 external/arm-optimized-routines(libc/Android.bp 引 libarm-optimized-routines-string/-mem)[S09][S10]。ARM64 通过 IFUNC 做运行分派:arch-arm64/ifuncs.cpp 在装载时按 hwcap 选择——memcpy/memmove 依 HWCAP2_MOPS、Oryon(读 MIDR_EL1 判 implementer 'Q' 且 part ≤ 15)、HWCAP_ASIMD 分派;memchr/strchr/strchrnul/strlen/strrchr 依 HWCAP2_MTE 分派 [S09]。RISC-V64 没有对应 ifuncs.cpp,其字符串例程由编译期 ISA 决定、无运行分派 [S09][S15]。两类都保留 FORTIFY 检查:riscv64 memcpy.S 的 __memcpy_chk 在 n > dst_len 时 call __memcpy_chk_fail [S07]。
失效边界:riscv64 memcpy 直接使用 RVV 指令,若目标平台不提供向量扩展则不可用(无 ARM64 式的 hwcap 分支回退)[S07][S09];按 ABI,memcpy 的调用者须提供有效 n 字节,实现也不得超出约定范围访问;本实现的 RVV 循环用 AVL=n 限定有效访问,这与是否使用 FOF 无因果;memset 同理;未对齐访问的硬件支持与代价是另一回事(实现中没有对齐快路径,其代价依微架构)[S07][S64]。vle8ff.v(fault-only-first)服务的是终止位置未知的字符串扫描(strlen/strchr/strcmp 等),使后续元素落在不可读页时短路而非必要故障;memcpy 因长度已知不需要也不使用 FOF。正确性回归应按 docs/libc_assembler.md 的边界用例(不越界读、处理未对齐、覆盖各尺寸)[S07][S50][S64]。若调用点尺寸或内容可被编译器静态判定,memcpy/strlen 可能被内联或折叠,导致"测的不是库函数"——基准需防编译器消除:benchmark::DoNotOptimize 能抑制一部分,但不保证表达式不被内联或常量折叠(Google Benchmark 官方 user_guide 明确给出反例),仍须确认真实函数调用与机器码 [S30]。
验证条件(可复现,本报告未运行):可复制观察包括 objdump -d /system/lib64/libc.so | grep -A20 '<memcpy>' 看是否出现 vsetvli/vle8.v;bionic-benchmarks --bionic_xml=string.xml --benchmark_filter=memcpy(按 AT_COMMON_SIZES/AT_ALIGNED_* 跑)[S29];正确性用 bionic-unit-tests 的字符串/buffer_tests.h 边界测试(覆盖不可读尾页、未对齐、各尺寸)[S50];nm -D libc.so | grep ' T ' 核对符号是否来自库而非内联。
成本与权衡:RVV 单循环实现的成本优势在长尺寸(每指令搬运 LMUL×VLEN/8 字节),劣势在短尺寸(固定开销占比高、无短尺寸特化);ARM64 的 AOR 实现按尺寸分派以兼顾短/长,代价是代码体量与 IFUNC 解析开销。是否补齐 riscv 的尺寸分派、是否引入 IFUNC 运行分派是两条可选路径:前者自主可控、不依赖硬件,后者收益依赖硬件多样性且 IFUNC 解析受链接器 early 阶段约束——本报告不对收益下结论,只列验证门。
补充:掩码写与重叠处理。strcpy/stpcpy 用 vmsif.m vMask1, vMask2 把"到 \0 为止"的前缀生成掩码,再 vse8.v vStr1, (pDstPtr), vMask1.t 按掩码写,避免多写;strchr(strchr.S:79)用 vmsif.m 把搜索范围限定到首个 \0,而 strchrnul 不含 vmsif,改用两次 vfirst.m(\0 掩码与 ch 掩码)+ sltz/sltu/or 偏移比较选择先行者 [S08]。memmove 通过比较 pSrc 与 pDst 判断方向,后向拷贝时从末尾减 iVL 倒着搬,保证重叠语义 [S07]。__memcpy_chk 的检查片段为 bleu n, dst_len, 1f; call __memcpy_chk_fail; 1: // Fall through to memcpy() [S07]。这些细节说明 RISC-V 侧的实现虽简短,但仍覆盖了字符串边界与重叠两类语义,优化时不能只盯吞吐而忽略这些约束。vsetvli 的第二操作数取 zero 与取 n 是两种意图:前者表示"不设元素上限、按 VLEN 自适应",用于字符串扫描;后者表示"最多处理 n 个元素",用于定长拷贝——同一宏(ELEM_LMUL_SETTING)在不同文件取不同 LMUL,是各例程按自身访存模式做的取舍 [S07][S08]。
见图 4。
手机可横向滑动查看本图
图 4:RISC-V64 与 ARM64 的 string/memcpy 实现路径对照(来源:bionic@06356e41 的 arch-riscv64/string/*.S、arch-arm64/ifuncs.cpp 与 libc/Android.bp [S07][S08][S09])。图左是 RISC-V64 的"单一 VLEN-agnostic RVV 循环、无尺寸分派、无 IFUNC、无 hwprobe 分派",图右是 ARM64 的"外部 AOR 主体 + IFUNC 按 hwcap 运行分派 + 尺寸分派"。读图要点:其一,两图差异是软件路径差异,是"小尺寸 memcpy 在 riscv 上相对偏慢"的可能成本来源(待验证假设)、而非已证解释;倍数必须在目标硬件实测;其二,RISC-V 侧没有 hwcap 回退,说明这些例程依赖构建期 ISA;其三,strcmp 的 LMUL 阶梯是 RISC-V 侧仅有的尺寸自适应。
3.8 libm、浮点环境与精度/errno 边界
入口与调用链:sin(x) → FreeBSD s_sin.c:|x|≤π/4 走 __kernel_sin;|x|<2^-26 造 inexact;Inf/NaN 走 x-x;否则 __ieee754_rem_pio2 归约后按 n=k mod 4 分派 __kernel_sin/__kernel_cos [S45];exp(x) → AOR math/exp.c:查表 T[idx] 得 2^(k/N)≈scale·(1+tail),对 exp(r) 用 4/5/6 阶多项式,返回 scale+scale*tmp [S46];riscv64 的 floor/sqrt/fma/lrint/round 等经 builtins.cpp 走编译器内建 [S34]。
关键数据结构与字段:libm/Android.bp 用多个 filegroup 拼装——libm_common_sources(FreeBSD msun 主体 + 少量 NetBSD 复数 + 自研 significandl.c/fake_long_double.c/builtins.cpp)、libm_lp64_sources(long double ld128/,仅 lib64)、libm_riscv64_sources(仅 fenv-riscv64.c);AOR 以 whole_static_libs: ["libarm-optimized-routines-math"] 整包纳入,为 arm64 与 riscv64 都定义 -DHAVE_FAST_FMA=1 [S34][S36]。fenv-riscv64.c 的 fenv_t/fexcept_t 为 __uint32_t(整个 fcsr 宽),__fe_dfl_env = 0 [S35]。riscv64 异常标志为 FE_INEXACT 0x01、FE_UNDERFLOW 0x02、FE_OVERFLOW 0x04、FE_DIVBYZERO 0x08、FE_INVALID 0x10(无 FE_DENORMAL),舍入模式 FE_TONEAREST 0x0…FE_TONEARESTFROMZERO 0x4 [S35]。
实现与接口依据:FreeBSD s_sin.c/k_sin.c 是多项式逼近族(k_sin.c 在 [-π/4,π/4] 用 13 次奇多项式,注释误差界 ≤2^-58;特殊值在位模式分支处理)[S45]。AOR exp.c 是查表+小多项式(exp(x)=2^(k/N)·exp(r),EXP_POLY_ORDER 选 4/5/6 阶;特殊值 -INF→0、+INF/NaN→1+x、大负/大正走 __math_uflow/oflow,亚正常有 specialcase 处理双舍入)[S46]。errno 边界是关键:AOR 构建 -DWANT_ERRNO=0,math_err.c 的 with_errno 关闭时不设 errno;libm 整体 -fno-math-errno——libm 不保证设 errno [S36][S34][S02]。riscv64 浮点环境用 CSR:fegetenv/fesetenv 用 frcsr/fscsr(fesetenv 先比较再写),feclearexcept/feraiseexcept/fetestexcept 用 csrc/csrs frflags,舍入用 frrm/fsrm [S35]。riscv64 没有浮点异常陷入接口:feenableexcept 恒返回 -1、fedisableexcept 恒 0、fegetexcept 恒 0,异常标志是粘滞位、不提供 SIGFPE 式触发 [S35]。
失效边界:调用者若依赖 errno 判定域错误/溢出会失败(libm 不保证设 errno)——这是"快路径不等于语义等价"的核心:AOR 与 BSD 来源在 errno 与 FENV 标志上本就不逐条等价 [S34][S36]。feenableexcept 在 riscv64 恒失败,尝试以此捕获浮点异常无效 [S35]。builtins.cpp 的某些映射仅在特定架构生效(如 round 仅 armv8/aarch64/riscv),误用条件编译会落到不同实现导致行为差异 [S34]。
验证条件(可复现,本报告未运行):可复制观察包括对 sin/exp/pow 用 bionic-benchmarks --benchmark_filter=math(MATH_COMMON 参数)复现 [S29];用 num 类测试或自写对拍核特殊值(±inf/nan/零/次正规)与错误标志;bionic-unit-tests --gtest_filter=*math* 做回归;cat /proc/cpuinfo 或 __riscv_hwprobe 确认硬件 F/D/FMA 能力以解释路径 [S16]。
成本与权衡:多项式逼近在给定精度下常数时间,代价是系数与次数;查表+多项式在宽输入域上更稳,代价是一次带 cache 的查表;riscv64 走 FMA 快速路径(HAVE_FAST_FMA=1)利用硬件融合乘加换取精度与速度,代价是依赖硬件 FMA。"不设 errno"的取舍是 bionic 基于 BSD 的历史选择与省一次分支,代价是与部分 POSIX 期望不符——这直接影响"用 errno 判错的基准/测试"是否可信。
补充:builtins 条件与 errno 关闭链。builtins.cpp 的映射带条件:无条件是 copysign/fabs/sqrt;ceil/floor/trunc/rint、fmax/fmin、round 在非 armv7 时用内建;fma/fmaf 仅在 armv8-arm32/aarch64/__riscv;lrint/llrint 在 aarch64/riscv/x86;lround/llround 仅 aarch64/riscv [S34]。__ILP32__ 下用 __weak_reference 让 ceill/floorl 等指向双精度实现 [S34]。AOR 的全局 cflags 为 -DWANT_ERRNO=0、-DWANT_VMATH=0、-DWANT_MOPS=1;math_err.c 的 __math_invalid 形如 isnan(x)?y:with_errno(y,EDOM),但 with_errno 在 WANT_ERRNO 关闭时直返(check_oflow/check_uflow 在 math_config.h 于 WANT_ERRNO==0 时返回 x)[S36]。libm/Android.bp 的 cflags 含 -fno-math-errno 与 -fno-builtin,进一步确保不依赖 errno 与内建语义 [S34]。附加事实是 ldexp() 属 libc(libc_ldexp_srcs = FreeBSD s_scalbn.c),而 ldexpf/ldexpl 在 libm——这提醒"归属"要按导出表而非直觉 [S34]。
补充:算法细节与特殊值落点。FreeBSD s_sin.c 头注释自述算法:把 x 归约为 y1+y2 = x - k·π/2 ∈ [-π/4,π/4],n = k mod 4,按 n 选 __kernel_sin/__kernel_cos/±;主函数体先判 |x| ≤ π/4(位模式 0x3fe921fb)走 __kernel_sin,|x| < 2^-26 时 if((int)x==0) return x 制造 inexact,Inf/NaN 走 return x-x 产 NaN,否则 __ieee754_rem_pio2 归约后分派 [S45]。k_sin.c 在 [-π/4,π/4] 用 sin(x)=x+S1·x^3+…+S6·x^13 的 Horner 形式,注释误差界 ≤2^-58 [S45]。AOR exp.c 的 specialcase(L33)显式 force_eval 触发 underflow 并处理双舍入,-INFINITY→0、+INF/NaN→1+x、越界走 __math_uflow(0)/__math_oflow(0);math_err.c 的 __math_invalid 在 isnan(x) 时返回 y、否则 with_errno(y, EDOM),但在 AOR 构建下 with_errno 不设 errno [S46]。这些落点说明"特殊值处理"是分散在归约、内核、错误辅助三处的,不能只看主函数。
补充:fenv 冗余写优化与舍入上界。fesetenv 先 fegetenv 比较,值相同则不写 fcsr(省一次 CSR 写);feholdexcept = fegetenv + feclearexcept(FE_ALL_EXCEPT);feupdateenv 先暂存 fetestexcept、fesetenv 后再 feraiseexcept 复位 [S35]。riscv64 的 rm 是指令编码里的三位字段(与 CSR frm 区分:fesetround 写的是 frm),其编码 7 为 dynamic、5/6 保留,故 a17 把 fesetround 的上界从 FE_UPWARD 放宽到 FE_TONEARESTFROMZERO 并加注释,避免把合法的 FE_TONEARESTFROMZERO(0x4) 误判为非法——不应据此理解为"可以给 fesetround 传 7" [S35]。这些细节对"用 FENV 标志判定浮点行为的测试"有直接影响:在 riscv64 上既不能靠 feenableexcept 捕获异常,也要注意舍入/标志是粘滞状态。
跨版本核对(AOR):补取 external/arm-optimized-routines 的 Android 16 快照(commit 1a2b3aea)与当前主线(498b3de1)对比,Android.bp 的 riscv64: 块以及 HAVE_FAST_FMA、WANT_ERRNO 相关行在两版本间未变(行号仅整体偏移),说明 riscv64 走 FMA 快速路径、且 AOR 不设 errno 的构建取向在 a16 与 a17 一致 [S36]。
3.9 信号、setjmp/longjmp 与栈回溯
入口与调用链:sigaction(sig, act, old) → 把 struct sigaction 拷入 __kernel_sigaction → __rt_sigaction(sig, new?, old?, sizeof(sigset_t))(riscv64 不设 sa_restorer)[S33];sigprocmask → 校验 how、filter_reserved_signals → __rt_sigprocmask [S33];备用栈随线程建立:pthread_create → __init_alternate_signal_stack(mmap(SIGNAL_STACK_SIZE) → 底部 mprotect(PTHREAD_GUARD_SIZE, PROT_NONE) → sigaltstack(&ss) → prctl(PR_SET_VMA, "thread signal stack")) [S21];线程退出时置 SS_DISABLE 后 munmap [S21]。setjmp/_setjmp → sigsetjmp(env, do_mask);longjmp/_longjmp → siglongjmp(弱别名)[S24]。
关键数据结构与字段:sigsetjmp 的 jmp_buf 私有布局:word0 = cookie(高 63 位)| signal-mask 标志(低 1 位),word1 = 信号掩码,word2 ra,word3 sp,word4 被掩码的 gp(影子调用栈 SCS 只用低位),word5–16 = s0..s11,word17–28 = fs0..fs11,word29 = checksum [S24]。保存序列(sigsetjmp)关键指令:
mv a2, sp
m_mangle_registers a1, sp_reg=a2 // 对 s0..s11、a4、sp 逐个 xor cookie
sd ra, _JB_RA(a0)
sd a4, _JB_GP(a0) // a4 = 掩码后的 gp(x3),供 SCS
sd a2, _JB_SP(a0)
sd s0, _JB_S0(a0) // …… 至 s11
m_unmangle_registers a1, sp_reg=a2 // 还原寄存器本身
fsd fs0, _JB_FS0(a0) // …… 至 fs11
m_calculate_checksum t0, a0, t1
sd t0, _JB_CHECKSUM(a0)
实现与接口依据:真正的 sigaction/sigprocmask 在 libc/bionic/sigaction.cpp 与 sigprocmask.cpp(不在 signal.cpp),syscall wrapper 由 SYSCALLS.TXT 生成(__rt_sigaction/__rt_sigprocmask)[S33][S17]。riscv64 的一项 ABI 差异是没有 SA_RESTORER:sigaction.cpp 注释说明 riscv64 不设 sa_restorer,"让内核从 vdso 提供默认 restorer",而 x86_64/arm32 才需要 __restore_rt;asm-riscv/asm/signal.h 仅转发 asm-generic/signal.h、未定义 SA_RESTORER(信号返回跳板位于 vDSO:公开内核源码 arch/riscv/kernel/vdso/rt_sigreturn.S 以 SYM_FUNC_START(__vdso_rt_sigreturn) 定义函数/跳板符号(不是数据符号)并用 ecall 发 rt_sigreturn,由内核在投递信号时把该地址填入返回地址)[S33][S54]。注意这里 riscv64 的符号名是 __vdso_rt_sigreturn,与 arm64 的 __kernel_rt_sigreturn 命名不同,不可混用。signal.cpp 主要是 sigset 位操作模板与 sigpending/sigqueue/sigtimedwait/sigwait/sigsuspend 转发;sigblock/sigsetmask 在 signal.cpp:86/93 无条件定义,且在 libc/libc.map.txt:961/974 无架构注解(生成器规则为"有架构 tag 且不含本架构才剔除"),故 riscv64 与 arm64/x86_64 一样导出这两符号——signal.cpp 里"除 riscv64 外"的注释已过时(提交 a3d607a8a,2024-07-30,"libc.map.txt: remove the two riscv64 special cases." 已删除 map 中的 riscv64 特例)[S33]。setjmp.S 的寄存器混淆(m_mangle_registers 逐个 xor cookie,返回前 m_unmangle_registers 还原)目的是让 cookie 不泄入内存;SCS 只保存 gp 低位以避免泄露影子栈地址;checksum 是 word0..28 的异或,siglongjmp 首步校验、不匹配 call __bionic_setjmp_checksum_mismatch;cookie 校验在恢复寄存器后、返回前执行,不匹配 async_safe_fatal;longjmp 返回值在 setjmp.S:285-288 为 li a0, 1; maxu a0, a0, a1,配合其注释 "Return 1 if value (in a1) is 0, value otherwise"——即 v==0 时返回 1、其它(含负非零值)原样返回 v,不是 max(1,v) [S24]。回溯边界:libc 不实现 unwinder,只导出/调用 _Unwind_*,真正实现由 libunwind-exported 一类独立模块提供;libc 内的消费点是 execinfo.cpp 的 backtrace() → _Unwind_Backtrace(riscv64 分支把 IP 回退 2 字节,注释称因 C 扩展指令最短 2 字节)[S01]。
失效边界:setjmp 校验失败会触发 async_safe_fatal(checksum 不符报 checksum mismatch、cookie 不符报 setjmp cookie mismatch)——若有人破坏 jmp_buf 或跨线程使用,会直接 abort [S24]。sigprocmask64 标 __noinline__,因为 libsigchain 只拦截 sigprocmask,一旦被编译器内联则拦截失效 [S33]。riscv64 上若照搬 x86 的 sa_restorer 用法会得到未定义行为(该架构无此约定)[S33]。
验证条件(可复现,本报告未运行):可复制观察包括 bionic-unit-tests --gtest_filter=*setjmp*:*signal*;对 sigsetjmp 用 objdump -d 查看保存序列与 _JB_CHECKSUM 偏移;strace -e trace=rt_sigaction,rt_sigprocmask,rt_sigreturn 观察系统调用;用 sigaltstack(NULL,&ss) 查询是否在备用栈(SS_ONSTACK)[S33][S21]。
成本与权衡:sigaltstack 随线程建立、退出释放,代价是一次 mmap 与 mprotect,收益是信号处理栈与主栈隔离;setjmp 的 cookie/checksum/SCS 处理增加了少量指令,收益是抗 cookie 泄露与栈破坏;__noinline__ 的 sigprocmask 牺牲内联优化换取拦截可靠性。信号与异常路径若进入计时区间会显著污染基准——这是"正确性测试与基准要分开环境"的原因(方法建议,非测量结论)。
补充:altstack 失败路径与 unwind 导出面。__init_alternate_signal_stack 的 mmap 失败处理在 a17 由静默 return 改为 async_safe_fatal(pthread_create.cpp:108-127),即建栈失败会让进程直接终止而非悄悄继续 [S21];线程退出时置 ss.ss_flags = SS_DISABLE 再 munmap [S21]。查询是否运行在备用栈上的惯用法是 sigaltstack(nullptr, &ss) == 0 && (ss.ss_flags & SS_ONSTACK)(android_unsafe_frame_pointer_chase.cpp)[S21]。unwind 的导出面见 libc/libc.map.txt:_Unwind_Backtrace、_Unwind_RaiseException、_Unwind_Resume、_Unwind_GetIP 等;arm 另有 _Unwind_VRS_* 与 __gnu_Unwind_Find_exidx;libc.so 的 shared.whole_static_libs 含 libunwind-exported,linker/Android.bp 依赖 libunwind——即真正实现由独立模块提供 [S01]。execinfo.cpp 的 backtrace() 在 riscv64 分支把 IP 回退 2 字节(注释:C 扩展指令最短 2 字节、反向识别非平凡)[S01]。
补充:sigaction64 别名与信号集转换。LP64 下 __strong_alias(sigaction64, sigaction)、__strong_alias(sigprocmask, sigprocmask64),因为 sigset64_t 等于 sigset_t;ILP32 用 SigSetConverter 适配 32/64 位信号集差异 [S33]。sigprocmask64 会先校验 how(仅当 new_set 非空)再 filter_reserved_signals(*new_set, how) 过滤内部保留信号,最后 __rt_sigprocmask(how, filtered?, old_set, sizeof(*new_set)) [S33]。signal.cpp 主要提供 SigAddSet/SigDelSet/SigEmptySet/SigFillSet/SigIsMember 模板与 sigpending/sigqueue/sigtimedwait/sigwait/sigsuspend 转发,_signal/signal→sigaction64;sigblock/sigsetmask 在 riscv64 与 arm64/x86_64 一样导出(见上,旧注释不成立)[S33]。即 riscv64 的信号面并非"少两个 BSD 遗留符号"的子集,其真实 ABI 差异是"无 restorer"(不设 sa_restorer,由内核/vDSO 提供跳板)——按"符号缺失"假设做移植清单会出错。
补充:altstack 命名与回溯边界。备用信号栈建立后会 prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ..., "thread signal stack") 命名,便于在 /proc/<pid>/maps 中辨认;arm64 若启用 memtag 会给该 mmap 加 PROT_MTE,riscv64 无此路径 [S21]。回溯方面,libc 只提供 _Unwind_* 的导出与 backtrace() 的驱动,真正的 FDE 查找由外部 unwinder 完成;riscv64 无 ARM EHABI 的 exidx(dl_unwind_find_exidx 仅 arm32 有意义),因此其回溯依赖 DWARF .eh_frame 与 LLVM 的 EH 机制(此点为边界陈述,实现源不在 bionic 内)[S01]。
补充:unwinder 实现来源链(v1.3 追证)。v1.2 曾记"定义 libunwind-exported 的仓库不在可检索公开镜像集合",那只是中途结论。按 Android LLVM 工具链构建链继续追证:toolchain/llvm_android 的 LibUnwindBuilder(src/llvm_android/builders.py:583)以 LLVM_ENABLE_RUNTIMES='libunwind' 从 toolchain/llvm-project(paths.LLVM_PATH / 'runtimes')构建 LLVM libunwind,install_config()(:652)把 output_dir/lib/libunwind.a 安装为 libunwind-exported.a;注释明确 "Bionic's libc.so exports the unwinder, so it needs a copy with exported symbols"。riscv64 另建一份 is_exported=False 的平台隐藏符号副本(:599-606),因为 riscv64 没有 NDK sysroot。导出副本用 LIBUNWIND_HIDE_SYMBOLS=FALSE 且 LIBUNWIND_USE_FRAME_HEADER_CACHE=TRUE(后者需 dlpi_adds/dlpi_subs,Android R 起;见注释 llvm.org/pr46743)。据此已确证:libc.so(shared.whole_static_libs 含 libunwind-exported)静态链入该导出副本并对外导出 _Unwind_*;linker/Android.bp:371 依赖的 libunwind 是隐藏符号副本;_Unwind_* 的实际实现在 LLVM 的 libunwind 子项目(不是 compiler-rt,二者不混)。为对照说明此前的"镜像缺失"结论,附已核查清单:system/unwinding=libunwindstack(debuggerd 用,含 RegsRiscv64.cpp,非 _Unwind_*);external/libunwind_llvm 与 external/libunwind 公开镜像冻结于 2021;external/llvm 只含编译器。riscv64 寄存器/状态链(可得部分):bionic 自身 setjmp.S 保存 ra/sp/s0..s11/fs0..fs11 及 cookie/checksum/SCS-gp(见前文),是 bionic 侧对 riscv64 上下文的最小可移植捕获;平台 debuggerd 的 libunwindstack(system/unwinding)在 RegsRiscv64 中枚举 pc/ra/sp/gp/tp/t0..t6/s0..s11/a0..a7 等,并支持经 PTRACE_GETREGSET 读取 NT_RISCV_VECTOR(向量状态)——这是 riscv64 寄存器捕获的一个公开可得实现,但属 debuggerd 栈回溯,与 _Unwind_* C++ unwinder 是不同机制,不可混为一谈 [S61]。
riscv64 上下文布局与恢复链(LLVM libunwind,上游机制参考)。按 LLVM 官方 pin a60110bb 的 Registers.hpp/UnwindRegistersSave.S/UnwindRegistersRestore.S(见 上游源码,PROVENANCE 含三文件 SHA):
- 布局:
Registers_riscv(Registers.hpp:4305)私有状态为reg_t _registers[32]——_registers[0]存 pc(:4336 注释;getIP()=_registers[0]、getSP()=_registers[2],:4330-4333)+ 启用浮点时的fp_t _floats[32];64 位下static_assert(sizeof(_registers) == 0x100)固定浮点区偏移为 256 字节(:4350-4352)。 - 保存:
__unw_getcontext(UnwindRegistersSave.S的__riscv段)先ISTORE x1, (RISCV_ISIZE*0)(a0)把返回地址(ra)作为 pc 存入 slot 0,再.irp i,1..31: ISTORE x\i, (RISCV_ISIZE*i)(a0)保存 x1..x31(__riscv_32e只到 x15);启用浮点时.irp 0..31: FSTORE f\i, (RISCV_FOFFSET + RISCV_FSIZE*i)(a0);最后li a0, 0(UNW_ESUCCESS)+ret。 - 恢复与控制转移:
Registers_riscv::jumpto(UnwindRegistersRestore.S的__riscv段)先FLOAD f0..f31,再ILOAD x1, (RISCV_ISIZE*0)(a0)把 slot 0 的 pc 装回ra,随后ILOAD x2..x9、跳过 a0、ILOAD x11..x31,最后ILOAD x10, (RISCV_ISIZE*10)(a0)恢复 a0(它一直充当上下文指针),再ret——跳转目标即恢复出的ra(=原 pc)。 - 寄存器号映射:
getRegister(:4385)把UNW_REG_IP→_registers[0]、UNW_REG_SP→_registers[2]、UNW_RISCV_X0恒 0(硬连线)、1..31直接索引_registers[regNum],与 RISC-V 常规 DWARF 编号(x0..x31=0..31)一致;UNW_RISCV_VLENB经csrr 0xC22读vlenb(:4394-4397)——这是本节唯一出现 CSR 的地方,也是"向量长度"与 unwinder 的接口。
CFI 步进链(DWARF,同一上游 pin):UnwindCursor::stepWithDwarfFDE(UnwindCursor.hpp:1071)调 DwarfInstructions<A,R>::stepWithDwarf(...)(DwarfInstructions.hpp:36,传入 _isSignalFrame);getCFA(:66)在 prolog.cfaRegister != 0 时取 registers.getRegister(cfaRegister) + cfaRegisterOffset,否则求值 cfaExpression;按 CIE/FDE 的寄存器规则用 getSavedRegister(定义在 :99;addressSpace.getRegister(cfa + savedReg.value))取回各寄存器,再写回 setIP/setSP——riscv 上即写回 _registers[0](pc) 与 _registers[2](sp)。需要真正跳回目标帧时由 Registers_riscv::jumpto() 完成控制转移(见上)。信号帧由 isSignalFrame()/_isSignalFrame 区分;riscv64 的信号返回经 vDSO __vdso_rt_sigreturn(§3.9 前文)[S62][S68]。
工具链与编译器的精确绑定(同一 pin):prebuilts/clang/host/linux-x86(pin 29182889…)的 Android.bp 以 cc_defaults libunwind-android-default(:351)定义 llvm_prebuilt_library_static 模块 libunwind(:370-377)与 libunwind-exported(:379-391,visibility 仅 //bionic:__subpackages__ 与 native_bridge_support 的 libc,注释明确 "only intended for use with libc.so, which needs to export the unwinder API");defaults 里 system_shared_libs: [] 并注释说明"libc.so links against libunwind*.a"的循环依赖关系 [S65]。该 pin 的 prebuilt 目录含 clang-r547379/clang-r563880c/clang-r584948/clang-r584948b/clang-r596125/clang-stable 等;Soong(android17-release,commit 6722dd88)cc/config/global.go:436 的 ClangDefaultVersion = "clang-r584948",ClangVersion() 即 ReleaseBuildClangVersion(ClangDefaultVersion)(:631-632),另有 LLVM_PREBUILTS_BASE/LLVM_PREBUILTS_VERSION 环境覆盖路径(:617-624)[S66]。clang-r584948/AndroidVersion.txt 自述 "22.0.0 / based on r584948",其 clang_source_info.md 给出 Base revision 6d4a0935c850ec3ddfc70c4ba97b98adc35c676e 与来自 llvm_android d71d2816 的 cherry-picks 清单 [S67]。重要修正:llvm_android main 的常量 CLANG_PREBUILT_VERSION='clang-r547379' 不是本 pin 的默认编译器——该 pin 的 Soong 默认是 clang-r584948(源 = LLVM 6d4a0935 + 上述 pick);且这是默认值,特定产品可用 release flag/环境覆盖,故"某产品实际用了哪个 clang"仍不能仅凭默认断言。
base 快照对照与 picks 影响(v1.6 补):本 pin 默认 clang-r584948 的源 = LLVM base 6d4a0935c850ec3ddfc70c4ba97b98adc35c676e + 40 条 cherry-picks(clang_source_info.md)。为验证机制是否随快照漂移,本报告另取 base 的 libunwind src/ 五文件(上游源码,含 provenance 与 SHA):其 riscv64 save(UnwindRegistersSave.S:1153 ISTORE x1,(RISCV_ISIZE*0)、:1164 FSTORE 循环)与 restore(UnwindRegistersRestore.S:1181 ILOAD x1,(RISCV_ISIZE*0)、a0 最后恢复)序列,以及 Registers_riscv(Registers.hpp:4042,reg_t _registers[32]、getIP()=_registers[0])与 [S62] 的 a60110bb 快照机制一致(两者行号不同、其余 diff 属不同上游版本)。picks 中仅一条涉及 libunwind——"[libunwind] Add CMake option to enable execute-only code"(cherry 4508f44a,经 llvm_android d71d2816),是构建选项而非 riscv 保存/恢复逻辑改动。因此机制陈述可同时锚定 base 与上游快照;但 Android 发布二进制 = base + 40 picks,本报告未取得该 fork 的成套源,故仍按三层身份陈述,不把 base 或 a60110bb 单独当作产品二进制源码 [S62][S67][S69]。
身份分层(务必区分):① bionic 固定 pin 已核的构建输入(libc.so 链入 libunwind-exported、导出 _Unwind_*;模块定义见同 pin prebuilt Android.bp)[S57][S58][S65];② 该 pin 的默认工具链绑定(Soong ClangDefaultVersion=clang-r584948 → 源 6d4a0935 + picks;含覆盖机制)[S66][S67];③ 上游 LLVM a60110bb 的实现机制(本节 save/restore/寄存器映射/CFI 步进)[S62][S68]。仍未证明的是"某具体产品构建实物选了哪一版 clang"——需发布 build 记录,不属于本节可核范围。
3.10 stdio、时间、随机数、locale 与网络解析功能地图
入口与调用链:printf 家族 → __vfprintf(vfprintf.cpp);fopen → __sfp 找空闲 FILE → __smakebuf 建缓冲 → 读写经 __sread/__swrite 等 cookie 原语 [S01];clock_gettime → vDSO 命中直返、未命中 __clock_gettime syscall [S16];getentropy → getrandom(GRND_NONBLOCK),失败回落 /dev/urandom 并恢复原 errno [S44];getaddrinfo → __netdClientDispatch.dnsOpenProxy() 拿代理 FILE* → 组帧 "getaddrinfo <host> <serv> <flags> <fam> <socktype> <proto> <netid>" → fputc(0)+fflush → 读回 DnsProxyQueryResult 与结果 [S38]。
关键数据结构与字段:stdio 核心是 FILE 与缓冲(stdio.cpp 的 __sfp/_fwalk/__srefill/__smakebuf,vfprintf.cpp 的 __vfprintf 宏 CHAR_TYPE char);parsefloat.c 负责 strtod/strtof 解析 [S01]。时间:tzcode/localtime.c 的 localtime_r/mktime/tzset 与 tzfile 解析(tzloadbody/tzparse),关键 cflags 含 -DALL_STATE、-DTHREAD_SAFE=1、-DTZDIR="/system/usr/share/zoneinfo" [S01]。随机数:OpenBSD ChaCha20(arc4random.c 的 _rs_stir 用 getentropy 播种),Android 侧 _rs_allocate 用单次 mmap + madvise(MADV_WIPEONFORK) 使 fork 子进程清零 [S44]。locale:struct __locale_t { size_t mb_cur_max; },仅存 MB_CUR_MAX [S37]。
实现与接口依据:libc/stdio/ 被 README 称为"来源可疑的遗留代码",方向是迁入 libc/bionic;a16→a17 把 refill.c 合入 stdio.cpp [S01]。libc/bionic/locale.cpp 明确只支持 "C"(=POSIX) 与 "C.UTF-8"(=en_US.UTF-8),newlocale/setlocale 对其它名字返回 ENOENT,localeconv 的货币/分组/符号多为空串或 CHAR_MAX [S37];docs/status.md 也建议 i18n 走 icu4c [S02]。网络解析:libc/dns/ 是 NetBSD resolver 的修改版,但 getaddrinfo 不再自行发 DNS 查询,而是经 netd 的 DnsProxyListener 代理;__netdClientDispatch 在 netdClientInit() 初始化(见 3.2)[S38][S19]。bionic_arc4random.cpp 提供 __libc_arc4random_buf_or_die(熵未就绪时从 getauxval(AT_RANDOM) 取值,最多 16 字节,超出 async_safe_fatal)与 __libc_arc4random_uniform_or_zero [S44]。
失效边界:getentropy 的 buffer_size > GETENTROPY_MAX 报 EINVAL;getrandom 失败(EAGAIN/ENOSYS/EINVAL/EFAULT)时回落 /dev/urandom,回落路径依赖文件系统可用 [S44]。newlocale 对不支持的名字返回 ENOENT,把 bionic 当完整 locale 实现会失败 [S37]。getaddrinfo 依赖 netd 代理解码器,若在 netd 不可用的极早阶段调用会失败(协议帧格式见上)[S38]。
验证条件(可复现,本报告未运行):可复制观察包括 strace -e trace=getrandom,openat <prog> 看随机数来源;locale -a 或 newlocale 试探返回;strace -e trace=connect <prog> 观察域名解析是否走 netd 代理(本地 socket)而非直连 DNS;bionic-benchmarks --benchmark_filter=stdio(printf/stdio 微基准)[S29];date/zdump 核对时区数据来源。
成本与权衡:stdio 的遗留结构庞大但稳定,迁移成本高;时间走 vDSO 换取高频调用的低开销;随机数用 ChaCha20 + MADV_WIPEONFORK 换取 fork 安全,代价是一次 mmap;把 DNS 事务移出 libc、放进 netd 换来 per-netid 路由与集中策略,代价是每次解析多一次本地 IPC 编解码。这些取舍说明"功能地图完整"是定位热点的前提——不能因与目标性能关系弱就整块忽略。
补充:stdio 结构、随机数就绪探测与 dns netid。stdio.cpp 覆盖 FILE 生命周期与缓冲的核心(__sfp、_fwalk、__srefill、__srget、__smakebuf、__swhatbuf、fopen/fdopen/freopen/fclose/__sflush、cookie 原语 __sread/__swrite/__sseek64/__sclose、funopen、asprintf/vasprintf 等,其中 refill.c 在 a17 合入、__stdio_mutex 改名 __sglue_mutex)[S01]。vfprintf.cpp 定义宏 CHAR_TYPE char/FUNCTION_NAME __vfprintf,printf 家族最终都到它;vfscanf.cpp、vfwprintf.cpp、vfwscanf.cpp 为对应核心 [S01]。getentropy 成功后恢复原 errno,buffer_size > GETENTROPY_MAX 报 EINVAL,getrandom 失败(EAGAIN/ENOSYS/EINVAL/EFAULT)回落 /dev/urandom [S44]。bionic_arc4random.cpp 的 __libc_arc4random_ready() 在首阶段 init(pid==1 且 /proc/self/exe 不存在)时用 getrandom(GRND_NONBLOCK) 探测熵源是否可用,未就绪则从 AT_RANDOM 取值(最多 16 字节,超出 async_safe_fatal)[S44]。DNS 侧 getaddrinfo 的帧格式与 DnsProxyQueryResult=222 回包在 getaddrinfo.c 中,netid 由 __netdClientDispatch.netIdForResolv 解析 [S38]。
补充:locale 接受名与时间解析细节。__is_supported_locale 接受 ""、"C"、"C.UTF-8"、"en_US.UTF-8"、"POSIX",其余返回 ENOENT;__is_utf8_locale 判定名称含 "UTF-8" 或为空串;uselocale 把当前 locale 存 TLS(__get_bionic_tls().locale),首次返回 LC_GLOBAL_LOCALE;localeconv 用 pthread_once 初始化单例 lconv g_locale,货币与分组符号全为空串或 CHAR_MAX [S37]。时间侧 tzcode 的 tzloadbody/tzparse 负责 tzfile 解析,关键 cflags 含 -DALL_STATE(历史兼容)、-DTHREAD_SAFE=1、-DTZDIR="/system/usr/share/zoneinfo"、-DTZDEFAULT=NULL(Android 用系统属性而非 /etc/localtime);tzdata 由 libcore 团队随 ICU 同步维护 [S01]。此外 time() 仅 x86/x86_64 有 vDSO 符号,其它架构用 gettimeofday 折算,因为多架构上 time(2) 系统调用并不存在 [S16]。把这些边界与 3.4 的 vDSO 连起来,才能判断"时间读取为何在某架构走 syscall"。
补充:locale 对象与 uselocale 语义。struct __locale_t 仅含 mb_cur_max(UTF-8 为 4、C 为 1),duplocale/freelocale 只做简单复制/释放,newlocale 对不支持的名字返回 ENOENT,setlocale 同理;uselocale 把当前 locale 存 TLS、首次返回 LC_GLOBAL_LOCALE [S37]。这意味着 bionic 的 locale 只承载 MB_CUR_MAX 与 C/POSIX(S) 行为,真正的本地化(货币、排序、多语言)由 icu4c 承担,docs/status.md 也这样建议 [S02]。网络解析侧,除 getaddrinfo 外还可能涉及 __netdClientDispatch 的其它回调(如 netIdForResolv),其初始化都在 netdClientInit()` 内完成,因此"极早阶段调用 getaddrinfo"可能因 dispatch 未就绪而异常 [S38][S19]。把这些功能地图补齐,才不会把"网络解析慢"简单归到 libc 的 resolver 代码上。
3.11 FORTIFY、stack protector 与构建实现选择
入口与调用链:memcpy/open/poll 直接调用 → 因 __attribute__((overloadable)) + pass_object_size 落到 FORTIFY 重载 → diagnose_if 编译期报错;若运行时才可知 → __*_chk(__mempcpy_chk/__open_2)→ 失败 __fortify_fatal/__check_buffer_access,成功回落 __builtin_* [S04]。stack protector 链(编译器插桩,并非每个函数无条件做):受保护函数的序言取 guard 存入栈帧、尾声再与保存值比较,不等则调 __stack_chk_fail;是否插桩由编译器与选项(如 -fstack-protector-strong)决定。取 guard 的方式由目标 ABI 决定,必须写清 target。本报告用 Ubuntu clang 18.1.8(sha256(/usr/bin/clang)=15a48145…)以 -fstack-protector-strong -O2 离线编译同一样例(见 离线编译记录):--target=riscv64-linux-android24 经 GOT 全局 __stack_chk_guard(auipc %got_pcrel_hi(__stack_chk_guard));--target=aarch64-linux-android24 经 TLS(mrs x21, TPIDR_EL0);而在 *-linux-gnu 目标下两者都经 GOT(该结果不可上推为 Android 结论)。即"序言从 TLS 加载"只在部分目标成立,不能一概而论。边界:这是编译器示例,不等于本 pin 系统 libc 的机器码。bionic 侧只负责提供 cookie:__libc_preinit(libc_init_dynamic.cpp:136)把 TLS TLS_SLOT_STACK_GUARD 的值赋给全局 __stack_chk_guard,该 cookie 由 android_reset_stack_guards 用 arc4random 播种 [S19][S18][S03]。
关键数据结构与字段:FORTIFY 依赖的编译器特性是 __attribute__((overloadable))、pass_object_size(N) 与内建 __builtin_object_size(p,N);(N&1) 为真时需要类型信息,只能由前端提供,故 pass_object_size 把求值"抬升"到调用者(等价于把 __builtin_object_size(&k.i,1) 作为隐藏参数传入)[S04]。stack protector 的 cookie 存在 TLS_SLOT_STACK_GUARD(word#5),x86 的 slot 0 需自指,riscv64 采用 Variant I 负偏移槽位 [S03][S25]。构建期实现选择看 libc/Android.bp 的 arch: 块(riscv64 列 arch-riscv64/**/*.S;arm64 经 AOR)与 libc_native_allocator_defaults(USE_SCUDO)[S10][S26]。
实现与接口依据:平台默认 -D_FORTIFY_SOURCE=2,NDK r21+ 默认开启 [S02]。clang_fortify_anatomy.md 详解三点:其一,overloadable 让直接调用落到 FORTIFY 版重载、&func 指向 __builtin_func(因 pass_object_size 参数不可取址);其二,diagnose_if + __builtin_object_size 在编译期报错,运行时 __*_chk 在被拦截调用上检查;其三,"零开销"是相对的——在 LLVM 把 @llvm.objectsize 折叠为常量前,额外分支会干扰内联与 memcpy 合并等机会 [S04]。docs/defines.md 讲清 __BIONIC__(由 sys/cdefs.h 定义)、__ANDROID_API__(多数时间构建为 10000,定档后回落真实级别)、__NDK_MAJOR__ 的选择,并指出"Android 只支持 riscv64,故 __riscv 对 Android-only 代码是充分判断" [S06]。docs/status.md 按 API level 记录新增函数(API 35 加 __riscv_hwprobe/__riscv_flush_icache,API 36 加 mseal,API 37 加 sched_getattr/sched_setattr)并区分"行为变更"与"新增函数" [S02]。
失效边界:FORTIFY 的编译期诊断只能覆盖"无需优化即可判定"的情形——指针经中间变量后 __builtin_object_size 返回 -1,诊断与运行检查都不再插入 [S04]。_FORTIFY_SOURCE 在 -O0 下基本失效 [S04]。__ANDROID_API__ 在年内多数时间等于 10000,若据此做条件编译会以为目标 API 很高(定档后才回落)——这是"构建实现选择"容易踩的坑 [S06]。
验证条件(可复现,本报告未运行):可复制观察包括对某源文件预处理看 FORTIFY 宏展开(clang -E);readelf -s libc.so | grep '__.*_chk' 看运行时检查符号;用刻意越界的样例观察编译期报错 vs 运行期 __fortify_fatal;确认"该目标是否真启用了 FORTIFY"应查构建输入(该模块 Android.bp/Soong 的 cflags 或预处理 clang -E 看 _FORTIFY_SOURCE 实际取值)并用反汇编确认运行时 __*_chk 落点;ro.build.fortify 没有官方 producer,不作为启用依据 [S02][S04]。
成本与权衡:FORTIFY 在"能静态判定"时零运行开销、在"只能运行期判定"时插入 __*_chk 分支,代价是可能干扰周围优化(影响内联与合并)——这解释了为什么平台默认开 _FORTIFY_SOURCE=2 仍需关注性能敏感路径;stack protector 的成本是每函数一次 TLS cookie 比对,收益是栈溢出检出;用 arch: 分派实现是"同一 API、不同架构不同实现"的机制,理解它才能判断某优化是否进入产物。
补充:FORTIFY 三段示例与门槛。clang_fortify_anatomy.md 以 mempcpy、open、poll 讲清机制:mempcpy 用 pass_object_size0 加 diagnose_if(__bos_unevaluated_lt(...), "'mempcpy' called with size bigger than buffer"),运行时在不满足 __bos_trivially_ge(bos_dst, copy_amount) 时调 __builtin___mempcpy_chk(dst, src, copy_amount, bos_dst),否则 __builtin_mempcpy [S04]。open 用两个重载分别处理"参数过多"(diagnose_if(1, "error", "too many arguments"))与"缺 mode"(diagnose_if(__open_modes_useful(flags), "error", "'open' called with O_CREAT or O_TMPFILE, but missing mode")),运行时 __open_2 在 needs_mode(flags) 时 __fortify_fatal("open: called with O_CREAT/O_TMPFILE but no mode") [S04]。poll 用 pass_object_size(1) 与 __bos_fd_count_trivially_safe,不可达时才 __poll_chk [S04]。较新的 Clang 用 diagnose_as_builtin 取代部分 diagnose_if,其诊断能带上缓冲区大小信息 [S04]。门槛方面:_FORTIFY_SOURCE=2 平台默认、NDK r21+ 默认;FORTIFY 在 -O0 下基本失效;一批检查自 API 28 起即使未启用 FORTIFY 也生效(如对已销毁 mutex、null FILE*、%n)[S02][S04]。cpu_target_features/ 提供了运行期特性探测的小工具(main.cpp/generate_printer.py),与"用 build flags 选择实现"是两件事 [S10]。
补充:符号可用性标注与释放通道。libc/libc.map.txt 里新函数带 # introduced=<API> 注释,README 说明历史上曾用 LIBC_Q 一类分节、后来改为"新增符号不再分节、只加 # introduced="(起因是发行版命名从字母转向 2025Q4 类季度命名)[S01]。a17 引入 RELEASE_DEPRECATE_RUNTIME_APEX 构建宏,影响链接器 bootstrap 路径与 debuggerd 回调的条件编译 [S12]。这些属于"平台/NDK/API 兼容约束"的落点:同一份源码在不同目标 API level 与不同产品变量下会得到不同产物,判断"某优化是否生效"必须连构建配置一起核。
3.12 测试与基准:口径、污染与 ARM64/RV64 差异分类
入口与调用链:bionic-benchmarks --bionic_xml=string.xml --benchmark_filter=memcpy → 按 suites 展开参数 → Google Benchmark 逐迭代 → 报告 Time/CPU/Iterations [S29];benchmarks/util.h 的 GetAlignedPtr(filled) → 控制对齐与初值 → MakeAllocationResident 逐页写 → 计时循环用 benchmark::DoNotOptimize 包裹被测表达式 [S30];进程启动用 bionic-spawn-benchmarks(CPU 时间只计 posix_spawn 调用线程)[S29]。
关键数据结构与字段:尺寸集合为 kCommonSizes={8,16,32,64,512,1K,8K,16K,32K,64K,128K}、kSmallSizes={1..16, 24..144(+8), 160..256(+16)}、kMediumSizes={512,1K,8K,16K,32K,64K,128K}、kLargeSizes={256K,512K,1024K,2048K};简写含 AT_COMMON_SIZES、AT_ALIGNED_ONEBUF/TWOBUF、AT_SUB_PAGE_SIZES、AT_MULTI_PAGE_SIZES [S29]。bench_opts_t 含 cpu_to_lock、num_iterations、xmlpath、extra_benchmarks;BIONIC_TRIVIAL_BENCHMARK 用 benchmark::DoNotOptimize(__expression) 防消除 [S30]。
实现与接口依据:bionic-benchmarks 构建在 Google Benchmark 之上,支持 --benchmark_repetitions 与 --benchmark_report_aggregates_only 给出中位/均值/标准差 [S29][S50]。污染控制上,MakeAllocationResident 在计时前逐页写使缺页发生、LockToCPU(--bionic_cpu/taskset)绑定核心、README 与 libc_assembler.md 建议把 governor 设为 performance 抑制告警,部分设备有 perf-setup.sh 锁频 [S30][S29][S50]。把墙钟时间(Google Benchmark 的 "Time")与周期(PMU cycles)区分开是方法底线:跨频率比较必须先固定频率或按频率归一化;PMU 采样受多路复用限制(可用计数器数量有限、事件组可能被时间分割),需注意多路复用比率 [S29]。
失效边界:不设 MALLOC_USE_APP_DEFAULTS 时命令行工具的分配器行为与真实应用不同,分配类基准会失真 [S05][S26];不锁核会让线程在大小核间迁移导致分数剧烈波动,不锁频会因 governor 未升频而抖动 [S29][S50];完全不设防时编译器可能把被测表达式消除;但 DoNotOptimize 也不保证不内联/不常量折叠,需配合 nm/反汇编确认真实调用 [S30];不在计时前 resident 化首轮缺页会污染结果 [S30];把 ns/op 与 cycles 混算、或在多路复用未标明的采样上直接下结论,都是错误口径。
验证条件(可复现,本报告未运行):可复制观察包括 bionic-benchmarks --benchmark_filter=BM_string_memcpy --benchmark_repetitions=10 --benchmark_report_aggregates_only=true --bionic_cpu=3;taskset f <bench> 与 cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 核对锁核与 governor;对某 cell 用 perf stat -e cycles,instructions 采周期(注意多路复用提示);用 nm/objdump 确认被测符号确实来自库而非内联。ARM64 与 RV64 的差异应分三类看:软件路径差异(3.7 的 RVV 单循环 vs AOR+IFUNC;3.8 的 libm 来源与 errno 行为)、硬件差异(向量宽度、FMA 吞吐、内存子系统——需 PMU/实测)、工具链差异(编译期 ISA 与优化选项)[S07][S09][S34]。
成本与权衡:微基准(bionic-benchmarks)快且可控,但只测分配速度、忽略 RSS 与 VA;轨迹基准(memory_replay)更贴近真实分配画像,成本是准备 trace 且结果方差大 [S05]。本报告未执行任何测量,上述均为方法说明与可复制命令,不含实测数字;据此形成"从测试→符号→机器码→采样/正确性→候选 A/B"的通用流程,用于后续把"能改的软件路径"与"只能测的硬件差异"分开。
补充:基准族与轨迹基准。除 bionic-benchmarks 外,benchmarks/ 下另有 malloc_benchmark.cpp/malloc_rss_benchmark.cpp/malloc_map_benchmark.cpp/malloc_sql_benchmark.cpp,分别测分配速度、RSS、VA 与 SQLite 风格轨迹;spawn/ 与 suites/ 承载进程启动与 XML 套件 [S29][S05]。docs/native_allocator.md 给出分配器评估的完整方法:stdlib_malloc_free_default(M_DECAY_TIME,0)与 stdlib_malloc_free_decay1(M_DECAY_TIME,1)对比、stdlib_malloc_multiple_8192_allocs_*、stdlib_malloc_forty_*(40 个变尺寸分配),以及轨迹基准 memory_replay64 <trace.zip>(逐 100000 次操作 dump RSS/VA)与 trace_benchmark(把轨迹当单线程跑)[S05]。评估三轴是速度、RSS、VA(VA 约束主要用于 32 位)[S05]。把这些口径与 3.6 的分配器参数连起来,才能解释"同一分配器在不同运行模式/不同 trace 下表现差异"。本报告未运行上述任何基准,仅登记方法。
补充:迭代与进程启动口径。Google Benchmark 会先跑若干轮确定"足够大"的迭代数,--benchmark_repetitions 让同一基准重复若干次再报中位/均值/标准差,--benchmark_report_aggregates_only 抑制单轮输出 [S29]。进程启动基准(bionic-spawn-benchmarks)的 CPU 测量只计 posix_spawn 调用线程、不含子进程时间,README 明确"实时更可用、且用它决定迭代数",并建议锁频以降低方差 [S29]。MakeAllocationResident 以 pagesize 为步长逐页写,使分配页在计时前 resident;bench_opts_t 的 cpu_to_lock 与 num_iterations 决定绑定核与迭代覆盖 [S30]。把这些与"锁核/锁频/DoNotOptimize"合起来,构成"可复现、可比"的最小基准纪律。
3.13 版本演进与 RISC-V64 支持层级
入口与调用链(演进核实步骤):核对某机制在两版本间的差异的可复述步骤是:①在含两 commit 的仓库里 git diff --stat <a16> <a17> -- <路径>;②对新增文件用 git log --diff-filter=A --format='%h %ci %s' -1 <a17> -- <文件> 定位引入 commit;③对某路径用 git log <a17> -- <路径> 列全史;④对未被 Git 关联的独立快照用目录 diff -ru [S02][S07]。
关键数据结构与字段:Android 16 公开基线到当前主线(f22516cb… → 06356e41…)共 556 个文件变化(+17027/−5438),其中 libc 447、tests 68、linker 16、benchmarks 11、libm 3 [S02]。riscv64 侧的具体演进可定位到文件:libc/arch-riscv64/string/strncpy.S 重命名为 stpncpy.S;strchr.S 修改;新增 strchrnul.S 与 strrchr.S——strchrnul.S 的首个引入 commit 为 63e9530f4(2025-10-08,提交信息 "riscv64: add optimized strchrnul().");libm/fenv-riscv64.c 与 FreeBSD 的 e_remainder.c 也有改动 [S07][S35][S45]。
实现与接口依据:linker 侧的 16 个变化文件里没有 riscv64 相关:变更为熵源可用性判定重构(删除 is_first_stage_init(),改用 __libc_arc4random_ready())、GnuPropertySection 改为 shared_ptr 并存到 soinfo、16KiB 兼容代码保护重构(成员方法化并新增 LOAD 段重叠检测)、dlsym 的 EPAN 可读段检查、RELEASE_DEPRECATE_RUNTIME_APEX 构建宏,以及 arm64 汇编新增 NOTE_GNU_PROPERTY()(riscv64 无对应改动)[S12]。据此可判定:riscv64 的链接器与 TLS 解析路径(begin.S、tlsdesc_resolver.S)在 a16 与 a17 之间一致 [S12][S40]。RISC-V64 的支持状态按四层分述,不跳级:①源码落地(libc/arch-riscv64/ 汇编、linker/arch/riscv64/、libm/fenv-riscv64.c 均在该 commit 中存在)[S10][S07];②构建/测试验证(libc/Android.bp 的 riscv64: 块把这些源纳入构建、SYSCALLS.TXT 的 arch 列表含 riscv64;tests/ 含字符串边界测试与 buffer_tests.h,CTS 的 CtsBionicTestCases 覆盖多数单元测试——但本报告未运行这些测试,属"测试存在"而非"本人验证通过")[S10][S17][S01];③发布承诺(docs/status.md 记录 __riscv_hwprobe 等进入 API 35,属 release 侧记录)[S02];④产品可用(本报告不作判断,指出不对具体商业设备或 CTS 通过率作承诺)。四层任一层缺失都不应宣称"支持完成"。
失效边界:跨独立快照直接 git diff 会失败(对象不可达),须用同一仓库的多个 commit 或目录 diff -ru;git log --diff-filter=A 在无 --follow 时可能误判"重命名+新建"的首个引入点(如 memcpy.S 会先命中一次清理提交而非最初引入)——定年时需结合重命名检测 [S07][S12]。把"目录存在"当"支持完成"、把"patch 邮件/开放 review"当"已合并",都是本报告明确避免的错误。
验证条件(可复现,本报告未运行):可复制观察包括 git ls-remote 取官方 HEAD 并核对 tag 与 main 分离;git diff --name-only <a16> <a17> -- libc/arch-riscv64/ 列 RISC-V 侧改动;git log --format='%h %ci %s' <a17> -- libc/arch-riscv64/string/ 看提交历史;对某新增例程 git show <commit> -- <file> 看完整改动 [S07]。
成本与权衡:维持多架构例程(如为 riscv 逐条补 strchrnul/strrchr)的代价是维护面扩大、测试矩阵变宽;收益是各 ABI 的独立优化空间。选择"补齐例程"而非"引入运行分派"意味着收益不依赖硬件多样性,但也放弃了对不同微架构的择优。理解演进路径有助于判断后续优化应落在"补例程"还是"沿 IFUNC/hwprobe 分派"——两条路径的验证门都在 3.12 给出。
补充:本版本净变化与核实链。a17 相对 a16 在 libc/arch-riscv64/string/ 的净变化是"重命名 + 修改 + 新增两例程",其余为其它子系统的重构;libc/arch-riscv64/bionic/ 的 setjmp.S/syscall.S/vfork.S 在两版本间为等价重构(风格、label 重命名、maxu、注释位宽修正),寄存器保存集合与 syscall 约定不变;而 __bionic_clone.S 是功能性新增——加入 __bionic_clone3(__NR_clone3)与公共 clone3() 包装(libc/libc.map.txt:278 clone3; # introduced=38;引入提交 31c741872,2026-03-27,"Add a wrapper for clone3";子线程改寄存器直传、不经子栈,提交 db4d3587b,2025-12-04)[S07][S31][S24]。版本核实步骤可复述为一条链:git diff --stat <a16> <a17> -- <路径>() → git log --diff-filter=A -1 <a17> -- <文件>() → git log <a17> -- <路径>() → diff -ru <两快照>() [S07][S12]。
补充:分支别名与 16 通道。temp-android16-plus 与 android17-release 指向同一 commit(06356e41),属旧名沿用;android-16.0.0_r4 与 android16-qpr2-release 同为 f22516cb,android16-release/android-16.0.0_r1/r2 同为 09a271af(2025-04-30),android16-qpr1-release 为 81b12d08(2025-06-08)[S02]。把"Android 17 轨道"的当前 tip 与"16 各 QPR 通道"分开列举,是为了避免把某一分支名当版本号、或把 QPR 通道的 tip 误当主线。对 RISC-V64 的"支持层级"结论在任何一条通道上都要以对应 commit 重新核实,因为例程集合在版本间是动态增长的 [S07][S10]。
补充:把"存在/受支持/有测试/正式 ABI"落到检查动作。核"存在"读源文件;核"受构建支持"读 libc/Android.bp 的 riscv64: 块与 SYSCALLS.TXT 的 arch 列表;核"有测试"查 tests/ 中对应头文件的测试(如字符串边界测试与 buffer_tests.h)与 CTS 的 CtsBionicTestCases;核"正式 ABI"查 docs/status.md 的 API level 记录与发布说明 [S10][S17][S01][S02]。四步都要以同一 commit 为准,任一步缺失都不能宣称"支持完成"。
本章为 benchmark 增补,不覆盖既有机制正文。范围为 Bionic 原生 benchmark(C/C++,libc/libm/linker/allocator),不把 ART/libcore/Java 基准混入同一账本。全部源码固定在公开 commit;本章不含实测分数、设备地址或未公开路径。文中“离线编译观察”仅表示在离线用 clang 18.1.8 对 pin 源码做编译/反汇编,不是设备上的系统 libc 机器码,也不是性能测量。
3.14 范围、pin 与真实清点
Bionic 自带的 benchmark 不是单个 memcpy 用例,而是一套建立在 Google Benchmark 之上的多家族套件,入口包括 bionic-benchmarks(动态链接设备 libc)、bionic-benchmarks-static(静态可执行)、bionic-benchmarks-glibc(host glibc 对照)、bionic-spawn-benchmarks(进程启动)、linker-reloc-bench(重定位)、malloc-rss-benchmark(RSS 取样),以及 libc/system_properties/context_lookup_benchmark、linker/linker_gnu_hash_benchmark、libc/memory/replay/TraceBenchmark 等独立 harness [S29][S70]。
本章的源范围固定为:
| 仓库 | tag | commit | 用途 |
|---|---|---|---|
| platform/bionic | android-17.0.0_r1 | 06356e41c5ed7b12220b24c05ba4fdb873b126a7 | 当前基准源码 |
| platform/bionic | android-16.0.0_r4 | f22516cbc67e81c13cf943ce02f01b8929a98726 | 版本对照 |
| platform/external/google-benchmark | android-17.0.0_r1 | c42745dfc2b140fde975369e41a097b0ccddc346 | 计时/统计底座 |
| platform/external/arm-optimized-routines | android-17.0.0_r1 | 498b3de1a06e8cd87b2f9d49d1d8a23ebba5cc5a | arm64 字符串/数学来源 |
| platform/external/scudo | android-17.0.0_r1 | 15715589eebe155c799ce6fe27005bc8775b86ad | 分配器实现 |
对上述 bionic pin 逐文件清点(只统计 benchmark 相关源码,不把 tests/ 自测和 test_suites/ 输入算作被测家族),A17 共有 25 个注册文件、333 个注册点;A16 为 285 个注册点;A16→A17 净增 48。宏分布为:BIONIC_BENCHMARK 114、BIONIC_BENCHMARK_WITH_ARG 107、BIONIC_TRIVIAL_BENCHMARK 18、CTYPE_BENCHMARK 17、WCTYPE_BENCHMARK 28、SPAWN_BENCHMARK 11、直接 BENCHMARK 29、BM_MALLOC_THREADS_THROUGHPUT 9。这里的“333”是注册点,不是默认运行实例数:带 AT_* 简写的注册点会按尺寸/对齐集合展开成多实例,实例数还依赖 getpagesize() 与 XML 选择,不能拿 333 当历史总数或跨版本分母 [S29][S70]。同时要区分三层范围:L1 历史账本(例如旧包的历史 Bionic 行数,只是历史输入,已独立复算也不等于新范围)、L2 公开固定 pin 源码枚举(本章的 333 个注册点属于这一层)、L3 未来目标平台新 run 的 eligible 清单(当前 unknown,不能用历史行数填充)。本章只做 L2 与测量方法,不宣称任何新硬件范围的 slot 数或成绩。
| 家族 | 文件 | 注册点(A17) |
|---|---|---|
| string/memory + wchar | string_benchmark.cpp、wchar_benchmark.cpp | 37 |
| ctype | ctype_benchmark.cpp | 17 |
| wctype | wctype_benchmark.cpp | 28 |
| allocator/memory | malloc_benchmark.cpp、malloc_map_benchmark.cpp、malloc_sql_benchmark.cpp、get_heap_size_benchmark.cpp、TraceBenchmark.cpp | 53 |
| math | math_benchmark.cpp | 46 |
| stdio | stdio_benchmark.cpp | 24 |
| pthread/semaphore | pthread_benchmark.cpp、semaphore_benchmark.cpp | 19 |
| atomic | atomic_benchmark.cpp | 10 |
| syscall/time | syscall_mm_benchmark.cpp、unistd_benchmark.cpp、time_benchmark.cpp | 48 |
| property | property_benchmark.cpp、context_lookup_benchmark.cpp | 9 |
| linker/dynamic | dlfcn_benchmark.cpp、linker_reloc_bench.cpp、linker_gnu_hash_benchmark.cpp、spawn_benchmarks.cpp | 18 |
| stdlib/inttypes | stdlib_benchmark.cpp、inttypes_benchmark.cpp | 24 |
| 合计 | 25 个注册文件 | 333 |
A16→A17 的 48 个净增点不是均匀分布:string_benchmark.cpp +18(新增 memchr、memrchr、memccpy、strnlen、strrchr、strcspn/strpbrk/strsep/strspn 的 common/medium/rare 变体、__strchr_chk/__strrchr_chk 等),stdio_benchmark.cpp 净 +4(printf/scanf 系列改名为 snprintf/sscanf 并增加 maps 解析与基线),新增 wchar_benchmark.cpp 4 项,新增 libc/memory/replay/TraceBenchmark.cpp 22 项;A17 的 AT_ALL_PAGE_SIZES 还增加了 page×128/256/512/1024 四档,syscall 套件的实例数会随之增加 [S29][S70][S75]。
清点方法本身也需要可复核:bionic 文件从任务本地缓存的 tag commit 用 git archive 抽取,A17/A16 两棵树各自生成逐文件 SHA256 清单;取源只读,未触碰共享 Android 树。统计脚本跳过 #define 宏体内的注册行,再单独展开 CTYPE_BENCHMARK、WCTYPE_BENCHMARK、SPAWN_BENCHMARK、BM_MALLOC_THREADS_THROUGHPUT 生成的名字,因此 333 不是“文本里出现多少次 BIONIC_BENCHMARK”的简单匹配数。注册名按源码标识符捕获(包括 GNU/Clang 扩展的 $,如 BM_stdio_snprintf_1$s),并对注释、字符串字面量和 #define 宏体做过滤,避免把伪文本当注册;A17/A16 各有一个此类 $ 名称,所以修正后的总数是 333/285。A16/A17 的 249/252 个文件、48 个净增点也可以用同一脚本复算;如果换一个 pin,家族表必须重跑,不能把本表的数字当历史常数。保留 A16 对照的目的不是“证明 17 比 16 多”,而是给跨版本实验一个可解释的边界:新增用例、改名用例和页尺寸档位都会改变默认实例集合,跨版本比较前必须先把集合对齐;如果只比较“总耗时”或“总分数”,版本差异会被误读成性能差异。对 RISC-V64 尤其如此:A16/A17 的 string/stdio 用例集合不同,某些同名函数的默认参数也可能不同,必须按 cell 而不是按二进制文件名对齐。
包含/排除必须说清楚:本章包含 bionic/benchmarks/ 下的 cc_benchmark 与独立 harness,包含 libc/system_properties/context_lookup_benchmark.cpp、linker/linker_gnu_hash_benchmark.cpp、libc/memory/replay/TraceBenchmark.cpp;不把 benchmarks/tests/*(harness 自测)和 test_suites/*.xml(测试输入)当作被测家族;malloc_rss_benchmark.cpp 是独立 cc_binary,按 RSS 取样而非 Google Benchmark 计时,单列;libc/kernel/uapi/linux/map_benchmark.h 是 UAPI 头文件,不是 harness;ART/libcore/Java 基准不在本章范围,它们在另一条账本中单独计量。这样做的目的不是“凑家族”,而是让每个结论都能落回具体文件、具体注册点和具体运行方式。还有一个常见误区是把注册点数当作“覆盖度”:333 只说明源码里有多少个入口,不说明这些入口在目标设备上都可运行、都走同一条实现、都有合格 harness;真正的覆盖度要由 L3 eligible manifest 与实跑结果决定。
调用链:源码树抽取 → 注册点统计 → 覆盖矩阵。 实现与接口依据:benchmarks/Android.bp 与 benchmarks/bionic_benchmarks.cpp 给出构建/注册接口;pin commit 06356e41/f22516cb 是版本依据;inventory_benchmarks.py 的宏解析是清点实现。 失效边界:注册点不等于实例数;pin 变更、AT_* 简写或 getpagesize() 变化都会改变集合;L2 清点不能替代 L3 eligible。 验证条件:sha256sum -c source-pin/SHA256SUMS-a17.txt;重跑 inventory_benchmarks.py 与 build_coverage.py,核对 333/285/48;抽查 BIONIC_BENCHMARK 宏与宏生成用例。 成本与权衡:源码清点成本低、可重复,但不含设备可运行性;完整 L3 需要镜像/eligible manifest,成本更高 [S29][S70][S75]。
3.15 Harness:从注册到一次运行
bionic-benchmarks 的核心是一个静态注册表:各 benchmark 文件用 BIONIC_BENCHMARK(name) 或 BIONIC_BENCHMARK_WITH_ARG(name, "AT_...") 把函数指针和默认参数写入 g_str_to_func,静态初始化阶段完成注册;main 解析 --bionic_cpu、--bionic_xml、--bionic_iterations、--bionic_extra,然后把其余参数交给 Google Benchmark 的 Initialize/RunSpecifiedBenchmarks [S29][S30]。BIONIC_TRIVIAL_BENCHMARK(name, expr) 把表达式包在 benchmark::DoNotOptimize(expr) 里,专门用于 getpid、isfinite、dladdr 这类“返回值可能被优化掉”的调用 [S30]。
XML 套件的格式是 <fn><name>BM_...</name><iterations>N</iterations><cpu>K</cpu><args>...</args></fn>;args 可以是空格分隔的整数(如 32 8 8),也可以是 AT_COMMON_SIZES、AT_MANY_ALIGNED_TWOBUF 等简写,还可以用 AT_ONEBUF_MANUAL_ALIGN_XX_SIZE_YY/AT_TWOBUF_MANUAL_ALIGN1_XX_ALIGN2_YY_SIZE_ZZ 手工指定对齐与尺寸。RegisterXmlBenchmarks() 对每个 <fn> 解析 name/args/iterations/cpu,XML 的 iterations/cpu 优先于命令行;同一个函数可以在 XML 里出现多次,用不同 args 展开成多个实例 [S29]。--bionic_extra="BM_string_memcmp 32 8 8" 则是 CLI 侧的等价入口;它和 XML 可以同时使用,最终注册表按 name+args 逐项进入 Google Benchmark。理解这一点,才能解释“为什么同一个 BM_string_memcpy 在报告里有很多行、每行 range 不同”。注册表本身是 std::map<std::string, pair<func,args>>,按名字排序,所以默认运行顺序是按 benchmark 名字而不是按文件/家族;LockAndRun 通过函数指针的强制转换把 void(*)(benchmark::State&) 适配成 Google Benchmark 的回调,并在调用前完成可选的绑核。这个设计让新增 benchmark 只需要一个宏,但也意味着“注册名字”就是长期约定:改名/改默认 args 会让历史结果无法按名字对齐,跨版本比较必须用 (name, args) 而不是只看前缀。
| 目标 | 类型/链接 | 关键入口 |
|---|---|---|
bionic-benchmarks | cc_benchmark,动态链接设备 libc | benchmarks/Android.bp:102;设备 /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks |
bionic-benchmarks-static | 静态可执行 | benchmarks/Android.bp:116;用于跨版本/非 Android OS,符号路径在 symbols/data/... |
bionic-benchmarks-glibc | host glibc 对照 | benchmarks/Android.bp:130;run-on-host.sh glibc |
bionic-spawn-benchmarks | 独立 cc_benchmark | benchmarks/spawn/Android.bp:58;测 posix_spawn/进程启动 |
linker-reloc-bench | 独立 cc_benchmark | benchmarks/linker_relocation/Android.bp:45;169 个生成共享库 + 1 个 main(共 170 个 .S 文件) |
malloc-rss-benchmark | cc_binary | benchmarks/Android.bp:173;RSS 取样,不属于 Google Benchmark 计时 |
context_lookup_benchmark | 独立 Google Benchmark 二进制 | libc/system_properties/context_lookup_benchmark.cpp |
linker_gnu_hash_benchmark | 独立 Google Benchmark 二进制 | linker/linker_gnu_hash_benchmark.cpp |
TraceBenchmark | trace 回放,manual time | libc/memory/replay/TraceBenchmark.cpp |
构建参数直接决定“测到的是不是库函数”:bionic-benchmarks-defaults 使用 -O2、-fno-builtin、-ftrivial-auto-var-init=uninitialized、stl: libc++_static,并链接 liblog、libbase、libBionicBenchmarksUtils、libtinyxml2 [S70]。-fno-builtin 的作用不是“让代码更慢”,而是禁止编译器把 memcpy/strlen/memset 等调用替换成内建序列或常量折叠,目的是让 benchmark 走到 libc.so 的导出符号,仍需反汇编确认。-ftrivial-auto-var-init=uninitialized 则避免自动变量初始化给计时循环引入额外写入。即使有这两个选项,仍应通过 nm/objdump 或反汇编确认目标符号没有被内联——这是本章反复强调的“源码有 intrinsic/内建,不等于本轮调用点已经 intrinsify”。
运行侧的污染控制分散在几处:bionic_benchmarks.cpp 在 main 里调用 setpriority(PRIO_PROCESS, 0, -20) 提高进程优先级,失败只 perror 不终止;--bionic_cpu 通过 LockAndRun 在每次 benchmark 调用前执行 sched_setaffinity,--bionic_iterations 作为全局 CLI 默认,通过 registration->Iterations(n) 被每个 registration 应用;XML 里的 <iterations>/<cpu> 逐 benchmark 覆盖命令行 [S29][S30]。README 还建议把 governor 设为 performance,否则 Google Benchmark 会打印 CPU scaling 警告;部分设备提供 perf-setup.sh 锁频 [S29]。这些动作只降低方差,不改变被测函数本身;如果目标是跨平台对照,还必须把频率、hart、affinity、ISA 和内核 vDSO 状态一并记录,否则 ns/op 无法解释。
一个容易忽略的文档漂移是:README 声称未指定 XML 时默认使用 suites/full.xml(host 用 host.xml),但 A17 的 suites/ 目录只有 example.xml、memcmp.xml、strcmp.xml、string.xml、strncmp.xml、syscall.xml、vdso.xml,并不存在 full.xml/host.xml;代码在无 XML 且无 --bionic_extra 时调用 RegisterAllBenchmarks,即“注册全部” [S29][S70]。实际运行应以代码为准:不指定 XML 会跑全部注册点(及其简写展开),而不是某个不存在的默认套件。做对照实验时最好显式传 --bionic_xml 或 --bionic_extra,避免“同一条命令在不同版本跑出不同集合”。因此,复现实验的完整命令应包含:二进制 SHA、XML/extra、--bionic_cpu、--bionic_iterations(若有)、Google Benchmark 的 min_time/repetitions/aggregates 选项,以及 setpriority/绑核是否成功;只写“跑了 bionic-benchmarks”不足以复现。
调用链:main() → RegisterAllBenchmarks() / RegisterXmlBenchmarks() → benchmark::Initialize() → benchmark::RunSpecifiedBenchmarks()。 实现与接口依据:benchmarks/bionic_benchmarks.cpp 的 CLI/XML 解析与 benchmarks/Android.bp 的 cc_benchmark 定义;--bionic_xml/--bionic_extra 是公开接口。 失效边界:README 的 full.xml/host.xml 默认套件并不存在;setpriority()/sched_setaffinity() 失败只打印;XML 与 CLI 的优先级相反会改变集合。 验证条件:显式传 --bionic_xml=string.xml 或 --bionic_extra="BM_string_memcpy AT_COMMON_SIZES",核对输出中的 name/args;用 --benchmark_filter 限定 cell。 成本与权衡:XML 可复现但需维护;注册全部方便但集合大、耗时长;绑核/提优先级降低方差但不改变被测代码 [S29][S70]。
3.16 计时底座:Google Benchmark 的 0.5 秒、迭代校准与两条时钟
本章的计时语义以 A17 pin 的 Google Benchmark c42745df 为准。默认 --benchmark_min_time=0.5s、--benchmark_min_warmup_time=0、--benchmark_repetitions=1;迭代数不是预先写死的,而是先跑一轮,再按“目标时间 × 1.4”估算下一轮迭代数;如果上一轮不足目标时间的 10%,单次最多扩大 10 倍;有 repetitions 时只有第一轮计算迭代数,后续轮直接复用 [S71]。这解释了为什么同一个 benchmark 在不同机器上报告的 Iterations 可能差一个数量级:它是在追 0.5 秒的目标,而不是固定工作量。--benchmark_min_time 还有 <integer>x 形式(如 1000x),表示固定迭代数而不是时间目标;它与 --bionic_iterations(全局 CLI 默认值,由每个 registration 应用;XML <iterations> 才逐 benchmark 覆盖)以及默认的 0.5s 时间目标不是同一模式。Fixed-N 研究数据与原生自适应数据必须按执行方式记录,不能只凭 Nx 或 Iterations 数值抹掉 harness 身份 [S29][S71]。
warmup 默认关闭(min_warmup_time=0),这不是“预热不重要”,而是把选择权留给实验者:--benchmark_min_warmup_time 或单个 benchmark 的 MinWarmUpTime() 可以开启 warmup,开启后仍走同一套 Setup/Run/Teardown 与迭代校准,只是 warmup 轮不计入最终报告;RunWarmUp() 还会在结束时恢复正式阶段的迭代数,避免把 warmup 的较慢/较快结果带进正式轮 [S71]。对 page fault、allocator cache、JIT/loader 这类一次性效应明显的用例,warmup 是有意义的;对纯循环微基准,warmup 可能只增加运行时间。报告里必须写清是否开启,否则“同一条命令”的可比性不成立。参数选择上,先用默认 min_time=0.5s 看一次结果,再根据用例稳定性决定是否提高 min_time 或增加 repetitions:min_time 拉长单轮、降低启动/调度噪声占比;repetitions 增加独立轮次、用于估计轮间波动。两者不能互相替代,也不要把单进程内的多轮当作多进程样本。对 UseManualTime 的 TraceBenchmark,MinTime(15.0) 与 Repetitions(4) 是 benchmark 自己写死的,命令行再传 --benchmark_min_time 不会改变它的语义,必须按源码记录。
计时循环的边界也要说清楚。Google Benchmark 在每次 trial 前调用 benchmark 的 Setup()、结束后调用 Teardown();计时器在 State::KeepRunning() 首次进入时启动,因此循环外的 setup(构造 vector、打开文件、创建临时属性区)通常不计入主计时;如果计时已经开始、循环内还要做额外 setup,才需要在合法的 PauseTiming/ResumeTiming 之间把它排除(暂停区内不计),而不是用它把循环外准备包进计时 [S71]。DoNIterations() 会为多线程 benchmark 启动多个线程,各线程分别累计 real/CPU 时间,最后在 ThreadManager 的锁下汇总;report.iterations 是全部 harness 线程累计;DoNIterations() 里的 results.iterations / b.threads() 只是下一次每线程迭代数的校准值,不是最终报告的 Iterations [S71]。
两条时钟是分开的:real time 用 ChronoClockNow()(单调时钟),CPU time 用 CLOCK_THREAD_CPUTIME_ID(可用 MeasureProcessCPUTime() 改成进程 CPU);ThreadTimer 分别累计 real_time_used、cpu_time_used、manual_time_used [S72]。UseRealTime() 主要改变迭代校准与所选指标口径;CreateRunReport 仍然保存 real 与 CPU 两列,UseManualTime() 只是把 benchmark 自己 SetIterationTime() 的 manual 值填入 real_accumulated_time,不是“选了 real 就不报 CPU” [S71][S72]。对休眠、调度、子进程等待类用例,real time 与 CPU time 可能差很远;对纯计算循环,两者通常接近但仍受抢占影响。报告中的 “Time” 和 “CPU” 两列不能混用。
统计侧,--benchmark_repetitions=N 会让同一 benchmark 重复 N 轮,并输出 mean/median/stddev(以及 _cv 等聚合);--benchmark_report_aggregates_only 可只保留聚合值 [S71][S73]。这与 Bionic 的 --bionic_iterations 不是一回事:前者是“多轮独立 trial”,后者是全局 CLI 默认的迭代数请求(每个 registration 应用,XML <iterations> 可逐 benchmark 覆盖)。做不确定性评估时应使用 repetitions + 多进程,而不是把单进程内的多次 trial 当作独立样本;这一点在 3.24 的设备方案里会再次展开。报告里的 Iterations 也要读懂:它是 GB 线程累计,KeepRunningBatch 可能让实际迭代超过请求值,--bionic_iterations 只是请求/强制值;二者不等不能单独证明走了自适应校准;可能原因包括 XML <iterations> 逐 benchmark 覆盖、harness 线程数/批量模式等,先核对来源再判断。跨平台比较时,Iterations 不同是正常的,关键是工作量语义(size/args/线程/输入)一致,而不是强求相同的迭代数。
调用链:Initialize() → RunSpecifiedBenchmarks() → BenchmarkRunner::DoOneRepetition() → DoNIterations() → CreateRunReport()。 实现与接口依据:src/benchmark_runner.cc 的迭代校准、src/thread_timer.h 的两条时钟、src/timers.cc 的 CLOCK_THREAD_CPUTIME_ID;src/statistics.cc 给出 mean/median/stddev。 失效边界:warmup 默认关闭;--benchmark_min_time=<time>(时间形式)只影响校准目标;<N>x 是固定迭代模式,二者不同;频率/调度/页缺页会污染 real time;UseManualTime 绕过自动计时。 验证条件:同一 cell 用 --benchmark_min_time=0.5s 与 2s 各跑一次,比较 Iterations/Time/CPU;再加一次 --benchmark_min_time=1000x 固定迭代对照,确认时间目标模式与固定迭代模式不混;用 --benchmark_repetitions=4 看轮间波动。 成本与权衡:更长 min_time/repetitions 降低噪声但增加机时;两种时钟回答不同问题,不能只报一列 [S71][S72][S73]。
3.17 编译期污染:DCE、内建替换与 setup 污染
Benchmark 最常见的假象是“测到的不是你以为的函数”。Bionic 套件用了四类防护:-fno-builtin 阻止内建替换;benchmark::DoNotOptimize 用编译器屏障阻止结果被删除;volatile sink(math)或函数指针(pthread_self_fp)阻止纯函数被折叠;PauseTiming/ResumeTiming 把准备/清理移出计时区间 [S29][S30][S70]。这些防护各有边界,不能互相替代。
本报告做了一组离线编译对照,直接用 pin 源码和本机 clang 18.1.8(target riscv64-linux-android24,-march=rv64gcv)观察 -fno-builtin 的实际效果。对“固定 16 字节 memcpy”:
- 默认
-O2:编译器把memcpy(d,s,16)内联成vsetivli zero,16,e8,m1,ta,ma; vle8.v v8,(a1); vse8.v v8,(a0),即一段 RVV 拷贝序列,不会调用 libc 的 memcpy; -O2 -fno-builtin:同一函数变成li a2,16; tail memcpy,真正走到导出符号。
对“运行时长度 memcpy”和 strlen,两种编译都生成 tail memcpy/tail strlen 调用,但常量折叠场景已经说明:没有 -fno-builtin,benchmark 可能测的是编译器内联序列而不是 Bionic 实现。这组探针还暴露了两个边界:其一,-fno-builtin 只关闭“库函数到内建”的替换,不阻止 LTO/内联器跨模块把真正的 memcpy 实现内联进来;要排除这种可能,需要检查最终二进制里是否存在对 memcpy 的调用或符号,或者对 benchmark 目标做 LTO-off 构建。其二,DoNotOptimize 在不同编译器/参数下生成的屏障不同(空 asm、内存 clobber 或 asm volatile),它可能阻止删除结果,却不会阻止参数计算被提前/延后;对“测一次调用”的微基准,最稳妥的是在反汇编里找到调用指令与返回路径,而不是只相信源码里的宏。
Setup 污染同样是“计时包含什么”的问题:malloc 的 purge 用例把批量分配/逐页写/释放放在 PauseTiming 内,只计时 mallopt;stdio 在循环外 fopen/setvbuf/分配 buffer;property 在循环外建临时属性区;linker_reloc 的子进程启动本身就是被测对象。若把这些准备误算进每次迭代,得到的不是“函数成本”,而是“函数 + 一次性初始化”。反过来,有些 benchmark 故意把页驻留放进计时(stdlib 的 MakeAllocationResident),这时报告必须说明它测的是组合成本,不能与“纯 malloc 元数据”直接比较。因此,任何“libc 函数快/慢”的结论都必须回到目标文件或反汇编,确认调用点。一个实用的检查清单是:① 构建参数里是否有 -fno-builtin/-fno-lto 或等价隔离;② 目标函数是否出现在最终二进制的调用/符号表里;③ benchmark 的循环前后是否有 PauseTiming 边界不清;④ 被测输入是否在循环外准备好、是否被编译器提升为常量;⑤ 结果是否被 DoNotOptimize/sink 消费;⑥ 失败/异常路径是否会把一次性初始化算进计时。六项里任何一项含糊,性能数字都可能是在测“编译器/准备代码”而不是“libc”。这也是本章把离线反汇编与源码锚点并列的原因:源码说明意图,机器码说明实际。如果只能做一项检查,优先检查“目标函数是否真的被调用”:在最终二进制里找到调用点,比在源码里看到 BIONIC_BENCHMARK 更能说明问题。其次检查“计时区间是否干净”,最后才看 ns/op 的数值;顺序反过来,容易把编译器/准备代码的差异误读成 libc 的差异。
调用链:-fno-builtin → memcpy() 调用 → DoNotOptimize() → KeepRunning() 迭代。 实现与接口依据:benchmarks/Android.bp 的构建选项、benchmarks/util.h 的 BIONIC_TRIVIAL_BENCHMARK;probe.O2.s/probe.O2_nobuiltin.s 是离线对照。 失效边界:LTO/内联仍可能把实现拉进调用点;常量折叠可能绕过库函数;PauseTiming 边界不清会把准备算进计时。 验证条件:对最终二进制做 nm/objdump,确认存在对 memcpy/strlen 的调用;比较 copy16 的两种汇编;跑 bionic-unit-tests 的正确性用例。 成本与权衡:-fno-builtin 牺牲部分编译优化换取“测的是库”;开销检查增加构建/分析成本,但避免结论失真 [S29][S30][S70]。
3.18 深链一:memcpy/string —— 一个 RVV 字节循环对 AOR+IFUNC
Benchmark 调用侧。 BM_string_memcpy 接收 state.range(0)=nbytes、range(1)=src_alignment、range(2)=dst_alignment,用 GetAlignedPtrFilled/GetAlignedPtr 构造两个独立 buffer,在 KeepRunning() 循环里调用 memcpy(dst,src,nbytes),循环后用 SetBytesProcessed(iterations*nbytes) 报告吞吐 [S29]。默认参数是 AT_ALIGNED_TWOBUF,即 kCommonSizes(8、16、32、64、512、1K、8K、16K、32K、64K、128K)与对齐 0 的组合;AT_MANY_ALIGNED_TWOBUF 会进一步展开到 1..32 的 2 的幂对齐。memcmp/memmove/memset/strlen/strcpy/strcmp 等同族用例结构类似;memmove 另有“1 字节最坏重叠”的两个方向用例 [S29]。
实现侧。 A17 的 riscv64 memcpy.S 只有一个主循环:
memcpy:
mv p, dst
L(loop):
vsetvli iVL, n, e8, m8, ta, ma
vle8.v v0, (src)
sub n, n, iVL
add src, src, iVL
vse8.v v0, (p)
add p, p, iVL
bnez n, L(loop)
ret
它没有对齐预处理、没有按尺寸切换标量/向量路径、没有多倍展开、没有 NT store;vsetvli 每次按剩余 n 设置 VL,尾块由 VL 自然缩短,m8 表示使用最大 LMUL。__memcpy_chk 只做 n <= dst_len 检查后落入同一实现 [S07]。这与 arm64 侧形成鲜明对照:arm64 的 libc.so 通过 ifuncs.cpp 在加载时按 hwcap 选择实现,字符串/内存函数主要来自 external/arm-optimized-routines(AOR),存在多档实现与运行时分派 [S09][S36]。riscv64 侧没有等价的 IFUNC/hwprobe 分派,benchmark 调到哪个符号,就是构建时选定的那一个。
机器码/观测。 本报告用 clang 18.1.8、--target=riscv64-linux-android24 -march=rv64gcv 对上述 memcpy.S 做离线编译:生成的汇编中 memcpy 主循环为 7 条指令(vsetvli、vle8.v、sub、add、vse8.v、add、bnez;不含入口 mv p,dst 与 ret),对象中对应字节为 d7 76 36 0c(vsetvli)、07 80 05 02(vle8.v)、27 00 07 02(vse8.v);本机 llvm-objdump 未识别这三条向量指令的助记符(显示 <unknown>),但 clang -S 的汇编文本与编码一致。需要强调:这是对 pin 源码的离线编译观察,不是设备 libc.so 的机器码;设备差异可能来自所选源/构建/汇编器/链接与配置;这段手写 .S 不由 C/C++ 优化器重新调度,不能把差异归因于 clang 对这段 .S 的调度优化,必须以设备产物反汇编为准。
执行模式与正确性。 benchmark 是单线程、动态链接;-fno-builtin 用于阻止内建替换(见 3.17 的对照),但常量/小尺寸是否仍被内联、变量尺寸是否走到符号调用,都要以反汇编确认。memcpy 用例没有校验拷贝内容,只报吞吐;memcmp 只把返回值喂给 DoNotOptimize;strstr/strchr/memchr 会检查“是否找到”,失败则 errx;memmove 只测重叠方向,不校验结果 [S29]。因此性能结论不能替代正确性结论:正确性应依赖 bionic-unit-tests 的边界用例(不可读尾页、未对齐、各尺寸、重叠),而不是 benchmark 的返回值。
成本口径。 SetBytesProcessed 让报告给出 bytes/s,但“处理字节数”是按 iterations*nbytes 计算的;对 strcat 这类既有 seek 又有 copy 的用例,nbytes 可能只是名义工作量,不能直接横向比较。memcpy 的源/目的 buffer 是分离的 vector,测不到真实程序中的 cache 复用/别名;大小跨 8B 到 128KB(默认简写),但没有显式 working-set 控制;是否覆盖 L1/L2/LLC 边界取决于实际 cache 尺寸与工作集,未测不声称覆盖;对齐只到 32B,且 GetAlignedPtr 通过 or_mask 保证“不会比指定对齐更对齐”,这是有意为之。跨平台比较时必须固定 size、alignment、线程、编译器与 libc 构建;否则 ns/op 的差异可能来自分派方式、页驻留或频率,而不是“RVV 好不好”。
优化候选(事实/假设分栏)。 事实:riscv64 memcpy 是单一 RVV 循环,无对齐/尺寸分派;arm64 走 AOR+IFUNC;两者在 benchmark 中的调用路径不同 [S07][S09]。假设(需实测):对小尺寸加标量/定长快路径、对中大尺寸做多向量展开或软件流水、对超大尺寸考虑 NT store、对未对齐首尾做对齐预处理,可能降低特定 size/alignment 的成本;这些都不能从源码直接推出收益,必须在目标微架构上用同一 cell 做 A/B,并同时看 ns/op、bytes/s、cycles/instructions 与代码尺寸。另一个假设是 VLEN 越大,m8 循环的每次搬运越多、循环开销占比越低;但 VLEN 对尾块、未对齐跨页和频率的影响必须实测,不能用“向量宽度”直接承诺性能。
调用链:BM_string_memcpy() → memcpy() → memcpy.S 的 L(loop) → vsetvli/vle8.v/vse8.v → ret。 实现与接口依据:benchmarks/string_benchmark.cpp 的调用与参数,libc/arch-riscv64/string/memcpy.S 的实现;对比 libc/arch-arm64/ifuncs.cpp 与 AOR 的分派。 失效边界:benchmark 不校验拷贝内容;没有对齐/尺寸分派;无 -fno-builtin 或 LTO 时可能测到内联序列;VLEN/ISA 与页跨界会改变成本。 验证条件:离线编译 memcpy.S 并检查 vsetvli/vle8.v/vse8.v;设备上用 objdump 确认符号;用 bionic-unit-tests 的字符串边界用例做正确性。 成本与权衡:单一 RVV 循环实现简单、代码小,但缺少对齐/尺寸特化;任何特化都要用同 cell 的 A/B 与正确性测试验证 [S07][S09][S29][S36]。
3.19 深链二:malloc/Scudo —— 分配路径与“页驻留”必须拆开
Benchmark 家族。 allocator 家族不是一个用例,而是四类互补观测:malloc_benchmark.cpp 测 mallopt(M_PURGE/M_PURGE_ALL) 的释放延迟,以及 64/512/8192 字节 × 2/4/8 个 benchmark 自建 worker 的分配/释放吞吐(不是 GB harness threads;宏生成 9 个用例);stdlib_benchmark.cpp 测 malloc/free、calloc/free、40 个变尺寸分配、8192 字节多次分配,并分 M_DECAY_TIME=0/1 两档;malloc_map_benchmark.cpp 用 std::map/std::unordered_map 插入观察容器分配与 RSS;malloc_sql_benchmark.cpp 回放 SQLite benchmark 的真实分配轨迹;get_heap_size_benchmark.cpp 测 mallinfo/read_statm;TraceBenchmark.cpp 回放应用 trace(manual time)[S29][S70]。malloc_rss_benchmark.cpp 是独立 RSS 工具,不在这套计时账本里。
调用链。 以 malloc 为例:libc/bionic/malloc_common.cpp 的 malloc() 先取 GetDispatchTable()(libc.debug.malloc hook 是否装载),有 hook 则走 hook,否则调用 Malloc(malloc),最后经 MaybeTagPointer() 返回;free() 先 MaybeUntagAndCheckPointer(),再走 dispatch 或 Malloc(free);mallopt(M_DECAY_TIME) 还会把状态写入 __libc_globals 的 decay_time_enabled [S26]。Scudo 侧,combined.h::allocate() 依次做 initThreadMaybe()、对齐检查、GWP-ASan 采样、FillContents 选择、NeededSize 取整、primary size class 分配(失败则逐级放大 class)、secondary 兜底、initChunk()/内存标记 [S27][S28]。deallocate() 则按 class 归还 primary 或 secondary,并处理 quarantine/内存标记 [S28]。
成本分解。 这是本章最容易被误读的地方。MallocFree 在每次迭代里做 malloc(nbytes)、MakeAllocationResident(ptr,nbytes,pagesize)、free(ptr),三步都在计时内;MakeAllocationResident 以 pagesize 为步长写一个字节,因此对大于一页的分配会触发逐页首次触碰/缺页,测到的是“分配元数据 + 页驻留 + 释放”,不是纯 malloc 元数据 [S29][S70]。calloc 还多一次清零;MallocMultiple 把 N 次 malloc + MakeAllocationResident 放在计时内,free 放在 PauseTiming 区内不计,测的是“分配+页驻留路径(不含 free)”;malloc_threads_throughput 则把线程创建放在暂停区,计时包含多线程 malloc/free 交错与 Scudo 的 thread cache/central 竞争 [S29]。mallopt_purge 把批量分配/触碰/释放都放在暂停区,只计时 M_PURGE*,因此它回答的是“释放后归还/回收的延迟”,不是“malloc 快不快” [S29]。
线程口径(必须与 GB harness 线程分开)。 malloc_threads_throughput 的 2/4/8 是 benchmark 自己 new std::thread 的 worker 数,不是 Google Benchmark 的 b.threads():该 benchmark 在 GB 侧仍只占一个 harness 线程,report.iterations 是 GB 的迭代数,不能拿 worker 数去除它。RunThreadsThroughput 用 worker 数乘 kMaxBytes * kMinRounds 计算 SetBytesProcessed,并用 worker 数决定分配/释放交错模式;这与 GB 的多线程 benchmark(->Threads(n))是两套机制。results.iterations / b.threads() 只用于下一轮每线程校准,不是最终报告 Iterations。
正确性与失败路径。 这些用例基本不校验分配内容;malloc_threads_throughput 不检查 malloc 返回空(空指针会进入 free(nullptr),不会立刻报错);purge 用例在 malloc 为空时调用 SkipWithError,但源码没有 break/return,随后仍调用 MakeAllocationResident(ptr, size, pagesize) 与 ptrs.push_back(ptr),ptr 为空会继续解引用。这是 benchmark 源的控制流缺口,本轮只报告、不修改源;修复需同时清理已分配指针并处理 PauseTiming 状态,属于另建的修复候选,不能把“无清理 return”当作已正确修复。M_DECAY_TIME 通过 ScopedDecayTimeRestorer 在析构时恢复,避免污染后续 benchmark [S29][S70]。TraceBenchmark 依赖外部 traces/*.zip,源码树里没有 trace 本体;它用 mallopt(M_DECAY_TIME,1/0) 切换 decay,UseManualTime 记录 RunSingleThread() 返回的 ns,MinTime(15.0)、Repetitions(4)、只报聚合 [S70]。没有 trace 就不能复现,不能把它的结果与普通 malloc 微基准混为一谈。
测量边界。 分配器配置由 libc 构建决定(AndroidNormalConfig、quarantine、fill contents 等),benchmark 源码只通过 mallopt 改 decay/purge;libc.debug.malloc hook、GWP-ASan 采样、HWASan/MTE 会改变路径,必须在实验元数据里记录,不能把“开了 hook 的分配”与“release 配置的分配”放在同一列 [S26][S27]。Gather() 使用 android::meminfo::ProcMemInfo 的 MapsWithoutUsageStats() 取 VMA 列表,按 [anon:libc_malloc]/scudo:/GWP-ASan 名称筛选,再 FillInVmaStats(update_vma) 读取 usage.rss 并累加;maps 区间本身不提供 RSS,本稿只写到该 API,不推断 libmeminfo 的底层文件/页统计实现。MapBenchmark 在两次 PauseTiming 区内分别 M_PURGE_ALL+采 RSS,第二次读取时 map 仍活着;ResumeTiming 后作用域结束,map 析构仍计入时间,所以报告的时间不仅是 insert,还含析构/清理。
分配器家族还有三条不能混的观测轴:时间(malloc/free、purge、thread throughput)、RSS(malloc_map 通过 ProcMemInfo::MapsWithoutUsageStats() + FillInVmaStats() 的 usage.rss 累加,再用 M_PURGE_ALL 前后差;不是直接读 maps 区间)、VA(32 位地址空间约束,64 位通常不是瓶颈)。get_heap_size 的 mallinfo/read_statm 是另一条“读统计”路径;TraceBenchmark 是回放轨迹的 manual time。它们各自回答不同问题,不能用一个“malloc 分数”概括。另一个边界是 Scudo 的 GWP-ASan 采样:combined.h 在分配路径上先看 GuardedAlloc.shouldSample(),命中时走 guarded allocation,这会使采样命中的迭代远离普通路径;benchmark 若不记录采样配置,结果不可复现 [S27]。
优化候选。 事实:malloc/free 走 libc 薄层 + Scudo primary/secondary;benchmark 的 MallocFree 包含页驻留,malloc_threads 包含多线程竞争;TraceBenchmark 是另一条轨迹轨 [S26][S27][S28]。假设(需实测):size class 布局、thread cache/batch、quarantine 策略、decay/purge 时机会影响不同 size/线程数的成本;要验证这些,必须把“元数据成本”和“页驻留成本”拆成至少两个 cell(例如把 MakeAllocationResident 移出计时做对照),否则无法判断优化是否只是把缺页挪到了别处。
调用链:malloc() → Malloc(malloc) → Combined::allocate() → SizeClassAllocator.allocate() 或 Secondary.allocate() → initChunk()。 实现与接口依据:libc/bionic/malloc_common.cpp 的薄层、external/scudo/standalone/combined.h 与 primary64.h 的分配路径;mallopt(M_DECAY_TIME) 是公开接口。 失效边界:MakeAllocationResident 把页驻留算进计时;debug malloc/GWP-ASan/decay 改变路径;TraceBenchmark 依赖外部 traces;线程吞吐不检查 malloc 失败。 验证条件:分别跑 stdlib_malloc_free_*、mallopt_purge*、malloc_threads_throughput_*;用 mallinfo//proc/self/maps 交叉核对;记录 Scudo 配置。 成本与权衡:时间、RSS、VA 三轴不能合并;页驻留/竞争/回收各有成本,优化一个轴可能移动另一个轴的成本 [S26][S27][S28][S29]。
3.20 深链三:clock_gettime —— vDSO 与 syscall 的对照
Benchmark 调用侧。 time_benchmark.cpp 提供 BM_time_clock_gettime(CLOCK_MONOTONIC)、_MONOTONIC_COARSE、_MONOTONIC_RAW、_REALTIME、_REALTIME_COARSE、_BOOTTIME,以及对应的 clock_getres;每个“libc 调用”都有一个 _syscall 对照,直接 syscall(__NR_clock_gettime, ...)/syscall(__NR_clock_getres, ...);另有 gettimeofday/gettimeofday_syscall、time、localtime/localtime_r、strftime [S29]。所有用例都是单线程、循环内调用、无返回值校验;localtime 系列依赖 TZ/locale,strftime 的格式固定为 %Y-%m-%d %H:%M:%S。
实现侧。 libc/bionic/vdso.cpp 在 __libc_init_vdso() 里用 getauxval(AT_SYSINFO_EHDR) 解析 vDSO 的 ELF,按名字填 __libc_globals->vdso[] 的函数指针;clock_gettime() 取 VDSO_CLOCK_GETTIME 指针,有则调用并把负 errno 转成 -1+errno,没有则回退到 __clock_gettime()(syscall wrapper)[S16]。gettimeofday()、clock_getres() 同理;time() 在 riscv64 上没有 VDSO_TIME 符号,直接走 gettimeofday();__riscv_hwprobe() 也优先走 vDSO,失败再用内联 ecall [S16]。因此,clock_gettime 的成本不是一个常数,而是“vDSO 符号是否存在 + 对应 clock 是否在 vDSO 实现 + 内核 clocksource/时间数据页状态”的函数。
机器码/观测。 vDSO 是内核映射的用户态代码,不在 libc.so 里;要确认某台设备是否真的走了 vDSO,应同时看:getauxval(AT_SYSINFO_EHDR) 是否非零、vDSO 动态符号表里是否有 __vdso_clock_gettime、以及 strace -e trace=clock_gettime 是否没有系统调用(或 perf 里 syscall 计数为 0)。本章不假设所有 riscv64 内核都提供同样的 vDSO 符号;不同硬件配置/不同内核版本可能不同,必须逐镜像记录。
具体到 clock id,CLOCK_MONOTONIC/CLOCK_REALTIME/两个 _COARSE 通常在 vDSO 覆盖范围内,CLOCK_BOOTTIME 是“可选支持”,CLOCK_MONOTONIC_RAW 是否走 vDSO 取决于内核版本;同一个 clock_gettime 名字在不同 clock id 上可能有完全不同的路径。源码注释把“required in vdso”和“optionally supported in vdso”分开写,正是提醒读者不要把某个 id 的结果外推到全部 [S29]。验证时应至少取一个 required id、一个 optional id、一个 _syscall 对照;如果 optional id 在本镜像上回退 syscall,把它标成“镜像差异”,不要写成“libc 退化”。同理,time() 在 riscv64 上没有 VDSO 符号,它内部转 gettimeofday() 的成本要与 gettimeofday() 用例一起看,不能把 time() 的 ns/op 当作独立 syscall 成本。
成本口径。 clock_gettime 的 ns/op 主要反映 vDSO 读时间页 + 时钟换算,而 _syscall 对照包含 ecall 进入/退出、参数检查、内核时间读取;两者的差值可用来判断“vDSO 是否生效”,但不能直接外推为“所有 syscall 都快/慢”。clock_getres 通常更轻,localtime 还包含时区/locale 查找,strftime 包含格式化;这些不能与 clock_gettime 放在同一列比较。Google Benchmark 的 CPU time 在这里尤其有用:vDSO 路径的 CPU 时间应接近 real time,而 syscall 路径的 CPU 时间会包含内核态时间(具体取决于计时器实现)。
正确性与反例。 用例不校验时间单调性、不检查 clock_getres 返回值是否合理、不验证 localtime 的时区结果;失败路径(vDSO 缺失、clock id 不支持)只在实现里回退/返回错误,benchmark 不检查。一个常见反例是“在容器/模拟环境里 vDSO 符号存在但 clock 不支持”,此时 clock_gettime 可能返回 EINVAL,benchmark 仍会继续循环;因此 perf 结论必须与返回值检查/正确性测试一起看。另一个反例是把 _syscall 对照当成“无 vDSO 时的成本”:它绕过 libc 包装,不能代表 libc 回退路径的完整成本。
优化候选。 事实:libc 通过函数指针选择 vDSO,缺失时回退 syscall;riscv64 time() 无 vDSO 符号 [S16]。假设(需实测):内核 vDSO 的 clocksource 选择、时间数据页的 seqlock 重试、clock_gettime 的 clock id 分支会影响 ns/op;若目标镜像的 vDSO 缺失或退化,软件侧可做的是确保镜像/内核配置正确,而不是在 libc 里“再优化一次”。这条链的优化候选应以“确认 vDSO 生效 + 选择合适 clock id + 减少不必要的时间调用”为主,不能承诺用户态代码能消除内核时间读取的物理成本。
调用链:clock_gettime() → __libc_globals->vdso[VDSO_CLOCK_GETTIME].fn → __vdso_clock_gettime();缺失时 → __clock_gettime() → ecall。 实现与接口依据:libc/bionic/vdso.cpp 的 ELF 解析与函数指针、benchmarks/time_benchmark.cpp 的 libc/syscall 对照;AT_SYSINFO_EHDR 是 vDSO 接口。 失效边界:vDSO 符号可能缺失;CLOCK_BOOTTIME 等 optional id 可能回退;time() 在 riscv64 无 VDSO 符号;benchmark 不校验返回值/单调性。 验证条件:读 getauxval(AT_SYSINFO_EHDR)、查 vDSO 动态符号表、用 strace -e clock_gettime 或 perf 确认 syscall 计数;与 _syscall 对照。 成本与权衡:vDSO 省去 ecall 但依赖内核/镜像;syscall 通用但成本高;逐镜像记录比“统一假设”更可靠 [S16][S29][S54]。
3.21 深链四:pthread mutex —— 无竞争快路径与 futex 唤醒条件
Benchmark 调用侧。 pthread_benchmark.cpp 共 17 项:pthread_self(通过全局函数指针防止纯函数折叠)、getspecific/setspecific、once、普通/ERRORCHECK/RECURSIVE mutex 的 lock+unlock、三种 PTHREAD_PRIO_INHERIT 变体、rwlock 读/写、pthread_create(创建后 PauseTiming 再 join)、create_and_run(含 join)、exit_and_join、key_create/key_delete [S29]。锁相关用例(mutex/rwlock/once/key/self/getspecific/setspecific)是无竞争、没有第二个业务线程:mutex 从来没有第二个线程去抢,rwlock 也没有并发读者/写者,因此测的是快路径,不是 futex 竞争/唤醒。pthread_create/create_and_run/exit_and_join 不同:它们会创建子线程,包含 clone、栈/TLS、调度、join 或子线程退出的成本;不能把 17 项统称为“单线程”。
实现侧。 pthread_mutex_lock() 先读 mutex->state;普通 mutex 走 NonPI::NormalMutexTryLock(),后者在 old_state == unlocked 时用一次 atomic_compare_exchange_strong_explicit 把状态从 unlocked 改成 locked_uncontended,成功后直接 return 0,不写 owner_tid [S22]。失败后 MutexLockWithTimeout() 识别普通类型并转 NormalMutexLock(),用 atomic_exchange 竞争;普通 NonPI 的 fast/slow 路径都不写 owner_tid(pthread_mutex.cpp:454 注释、:585-620 NormalMutexLock、:716-724 普通类型委派)。pthread_mutex_unlock() 对普通 mutex 走 NormalMutexUnlock(),用 atomic_exchange_explicit(&state, unlocked, release) 释放;只有当 exchange 返回的值是 locked_contended 时才 futex wake,如果返回 locked_uncontended 则不唤醒 [S22]。这解释了无竞争 benchmark 的成本构成:一次 CAS + 一次 exchange;普通 fast path 不写 owner_tid,也没有 futex 系统调用。
机器码/观测。 本报告用 __atomic_* 内置对同一组操作做离线 lowering(clang 18.1.8,riscv64-linux-android24 -march=rv64gcv):load(acquire) 生成 lw → fence r,rw;store(release) 生成 fence rw,w → sw;store(seq_cst) 也生成 fence rw,w → sw;fetch_add(relaxed) 生成 amoadd.w;fetch_add(seq_cst) 生成 amoadd.w.aqrl;CAS 生成 lr.w.aq/sc.w.rl。这与 atomic_benchmark.cpp 覆盖的操作一一对应 [S29]。同样要强调:这是编译器 lowering 形态,不是设备 libc 的机器码;实际 bionic 的 atomic_compare_exchange_strong_explicit 可能因编译选项/目标 ISA 产生不同指令序列,必须以设备反汇编为准。
普通 mutex 的 state 是 16 位打包值,包含 type/shared/contention/counter 等位;NormalMutexTryLock 的 CAS 只改 state 并直接返回,普通 fast path 不碰 owner_tid;owner_tid 只在 recursive/errorcheck NonPI 的 MutexLockWithTimeout 路径维护(:747/:766);PI 使用独立的 PIMutex 结构;其自身 owner_tid 是含 futex 状态位、由用户态/内核协同维护的字段(pthread_mutex.cpp:137-145),与 NonPI owner_tid 路径不同;NonPI 的 owner_tid 与 pi_mutex 在 :459-465 为 union。普通 NonPI fast/slow 均不写 owner_tid。ERRORCHECK/RECURSIVE 在 fast path 失败后要走 MutexLockWithTimeout 并维护 counter;PI mutex 则使用另一套 PIMutex 结构(PIMutexTryLock/PIMutexTimedLock),涉及优先级继承/等待链,不能与普通 mutex 的 ns/op 直接横比。rwlock 的读/写路径又不同:读锁可能允许多个读者并发,写锁需要排他;benchmark 只测单线程 rdlock/unlock 与 wrlock/unlock,测不到读者并发/写者饥饿。把这些差异写进 cell 标识,才能避免“都是 pthread 锁”式的误合并 [S22]。
成本口径。 mutex lock/unlock 的 ns/op 是无竞争快路径成本;pthread_create 包含 clone、栈/TLS 建立、调度与 join(其中 BM_pthread_create 把 join 放在暂停区,只计创建;create_and_run/exit_and_join 把 join 计入)。pthread_self 走 TLS 读取(riscv64 的 tp 是通用寄存器,见已发表稿 §3.5),getspecific/setspecific 走 TLS key 数组;这些与 mutex 不是同一成本层。rwlock 的读/写快路径与 mutex 不同,不能合并。atomic_benchmark 是单线程 best-case:作者在源码里明确说“没有 coherence misses”“假设编译器不会优化掉 fence”;它用 volatile 计数器在原子操作之间制造未完成内存访问,使 fence 有实际工作可做 [S29]。因此它不能代表多核竞争下的原子成本。
正确性与反例。 这些用例不检查锁的公平性、不测优先级继承的实际效果、不验证 rwlock 的并发语义;pthread_create 的失败路径没有检查(创建失败会继续 join 未初始化 tid)。反例:把无竞争 mutex 的 ns/op 当作“锁竞争成本”会严重低估 futex 唤醒/调度;把 BM_atomic_fetch_add_seq_cst 与 BM_atomic_fetch_add_relaxed 的差值直接当作“fence 成本”也可能混入编译器调度差异。正确做法是增加双线程竞争 cell(现有套件没有)、用 perf 看 futex syscall/调度事件,并把无竞争与有竞争分栏。
优化候选。 事实:普通 NonPI fast/slow 均不写 owner_tid,无竞争 fast path 是单 CAS + exchange,unlock 只在 locked_contended 时唤醒;recursive/errorcheck 才维护 owner_tid,PI 用独立结构;RISC-V 原子 lowering 使用 AMO/LR-SC + fence [S22]。假设(需实测):在目标微架构上,LR/SC 与 AMO 的选择、acquire/release fence 的强度、cache line 对齐、recursive/errorcheck/PI 的 owner_tid/状态维护会影响相应路径成本;竞争场景则取决于 futex 唤醒策略与调度。任何“换成 AMO 就更快”或“去掉 fence 就更快”的说法都必须同时证明正确性(内存序)与竞争场景下的退化,不能用单线程基准代替。
调用链:pthread_mutex_lock() → NonPI::NormalMutexTryLock() → CAS;失败 → MutexLockWithTimeout() → __futex_wait_ex();pthread_mutex_unlock() → NormalMutexUnlock() → atomic_exchange →(仅 contended)futex wake。 实现与接口依据:libc/bionic/pthread_mutex.cpp 的 state 位与 fast path、benchmarks/pthread_benchmark.cpp 的用例;PTHREAD_PRIO_INHERIT 走 PIMutex。 失效边界:benchmark 全部无竞争;PI/rwlock 路径不同;不检查锁公平性/优先级继承;原子 lowering 依赖编译器/ISA。 验证条件:离线 atomic_probe.s 看 CAS/fence;设备 perf 看 futex syscall;补双线程竞争 harness 后再谈竞争成本。 成本与权衡:无竞争快路径成本低,但竞争/唤醒成本未覆盖;优化快路径不能代表竞争场景 [S22][S29]。
3.22 深链五:linker relocation 与进程启动 —— 进程级 real time
Benchmark 调用侧。 linker-reloc-bench 的 BM_linker_relocation 先拼出 nativetest64/linker-reloc-bench 目录,设置 LD_LIBRARY_PATH,然后调用 BM_spawn_test() 启动 linker_reloc_bench_main;benchmark 用 UseRealTime()、单位微秒 [S73]。linker_relocation/README.md 说明它“测量动态链接器加载大量共享库(大量符号与重定位)的时间”,生成器把目标 ELF 的重定位 dump 成 169 个共享库 + 1 个 main(本 pin 的 gen/ 下 169 个 liblinker_reloc_bench_*.S + linker_reloc_bench_main.S,共 170 个 .S 文件、7 个 .map),模拟加载一个固定版本 libandroid_servers.so 的工作量 [S73]。spawn/ 的 11 个用例(noop、noop_nostl、noop_static、bench_cxa_atexit、bin_true、sh_true、system/vendor 的 true/sh 等)都通过 BM_spawn_test():posix_spawn + waitpid,UseRealTime(),失败/退出码 127 时 SkipWithError [S73]。
实现侧。 父进程侧是 posix_spawn(bionic 的 spawn 实现最终走 clone/execve),子进程启动后由动态链接器完成装载、符号查找、重定位、构造函数,再执行目标程序;重定位类型在 riscv64 上映射到 R_RISCV_JUMP_SLOT、R_RISCV_64、R_RISCV_RELATIVE、R_RISCV_IRELATIVE、R_RISCV_COPY、R_RISCV_TLS_* 等,链接器还支持 RELR 与 APS2/sleb128 [S12][S13][S14]。linker-reloc-bench 的子进程通过依赖/加载 169 个生成共享库(另有一个 main),把这一整套重定位路径跑一遍;它不是对 linker.cpp 某个函数的微基准。
spawn 的 11 个用例可以分成三组:noop/noop_nostl/noop_static 是“最小进程 + 不同链接方式”,用来看静态/动态链接与 C++ 运行时的启动差;bench_cxa_atexit/bench_cxa_atexit_full 用 100000 次 __cxa_atexit 放大析构注册/执行成本,分别以 _Exit 与 exit 结束;bin_true/sh_true/system_bin_true/vendor_bin_true/system_sh_true/vendor_sh_true 则覆盖 /bin 与 /system/bin、/vendor/bin 的搜索路径与 shell 启动差异。它们全是 posix_spawn + waitpid,但子进程做了什么、退出方式是什么不同,不能合并成一个“启动分数”[S73]。linker_reloc 的 169 个共享库 + 1 个 main 是生成的合成负载,gen/ 里是 liblinker_reloc_bench_000..168.S(169 个)加 linker_reloc_bench_main.S,共 170 个 .S,另有少量 .map;regen/ 的 dump_relocs.py/gen_bench.py 说明如何从目标 ELF 重新生成,换一个目标库就要重新生成并重新记录 SHA。
成本口径。 这是进程级 real time:包含 fork/clone、execve、内核调度、动态链接器装载/重定位、构造函数、子进程退出与 waitpid 唤醒;Google Benchmark 的 CPU time 来自 ThreadTimer 的线程 CPU 时钟,只覆盖调用 posix_spawn 的线程,不含子进程 CPU;spawn benchmark 用 UseRealTime() 以墙钟作为校准/所选指标口径,real 与 CPU 两列仍都保留;CPU 列只覆盖调用线程,所以在这类用例上会显著低于“Time”列 [S72][S73]。linker_reloc 用 UseRealTime 让迭代校准也基于 real time;如果系统负载/调度抖动大,迭代数会被拉长或结果方差变大。要归因到“重定位本身”,需要在子进程里用 perf/PMU 或链接器统计(如镜像支持 LD_DEBUG=statistics;该选项属于源码支持的类别,本轮未实跑)分离装载、查找、重定位、构造;仅靠父进程 real time 无法区分。
正确性与反例。 BM_spawn_test 检查 posix_spawn 返回值、waitpid 结果、子进程退出码 127(exec 失败),但不会验证 169 个共享库 + main 的符号/重定位结果是否全部正确;linker-reloc-bench 的 .S 是生成的合成负载,和真实 libandroid_servers.so 的重定位分布不一定一致。反例:把 linker_reloc 的 real time 当作“重定位算法成本”,或把 spawn 的 real time 当作“execve 系统调用成本”,都会把调度/页缺页/构造函数混进来。另一个反例是 LD_LIBRARY_PATH 只影响子进程;父进程自身的库搜索路径不变,必须确保子进程确实加载了生成库(首选子进程 dl_iterate_phdr 核对已加载对象;LD_DEBUG 的合法类别见 linker/linker_debug.cpp,libs 不是合法值且会触发错误退出,本轮未实跑)。
优化候选。 事实:这是 169 个共享库 + 1 个 main 的合成重定位负载,父进程只测 real time;arm64/riscv64 的重定位类型映射和 RELR/APS2 支持在链接器里 [S12][S13][S14]。假设(需实测):RELR 覆盖比例、重定位类型分布、符号查找的 GNU/SysV hash、页缺页与 COW、构造函数数量会影响启动时间;优化应分别测“装载/查找/重定位/构造”,并用子进程 PMU 验证,而不是用父进程 real time 直接归因。对 RISC-V64 而言,重定位写入的指令/数据 cache 一致性、页表与 TLB 行为也可能是变量,需在目标硬件上实测。
调用链:BM_linker_relocation() → BM_spawn_test() → posix_spawn() → 子进程 linker_reloc_bench_main → 动态链接器装载/重定位 → waitpid()。 实现与接口依据:benchmarks/linker_relocation/README.md、linker_reloc_bench.cpp、benchmarks/spawn/spawn_benchmark.h;链接器侧见 linker/linker_relocate.cpp 与 linker/linker_relocs.h。 失效边界:父进程 real time 含子进程调度/退出;169 个共享库 + 1 个 main 是合成负载;不校验重定位正确性;CPU 列不含子进程。 验证条件:子进程 dl_iterate_phdr 确认已加载对象;如需 linker 调试输出,按 linker/linker_debug.cpp 的合法类别(如 dynamic/lookup/reloc/statistics)另行验证,本轮未实跑;子进程 perf 采 PMU;检查退出码 127/失败路径。 成本与权衡:进程级测量贴近真实启动,但归因难;需要子进程 PMU/链接器统计才能拆开装载、查找、重定位、构造 [S12][S13][S29][S73]。
3.23 其余家族:math、stdio、syscall、property、atomic 与字符分类
math(46 项)。 math_benchmark.cpp 覆盖 sqrt/log10/logb/sin/cos/sincos/fabs、isfinite/isinf/isnan/isnormal/signbit/fpclassify 的宏与函数形式,以及 SPEC CPU 2006/2017 的 exp/expf/exp2/exp2f/pow/powf/log/logf/log2/log2f/sincosf 输入向量;SPEC 用例分“throughput”(顺序读输入数组)与“latency”(f = f*0 + *cin 制造依赖链)两版 [S29]。输入数组是固定的(expf_input.cpp 约 2.4k 行、powf_input.cpp 约 2.2k 行等),因此跨平台 workload 可比;但 benchmark 不校验 ULP/errno/fenv,精度正确性要另用 math 测试。实现上,expf/logf/powf/sincosf 等来自 AOR 的查表+多项式(expf.c 用 5-bit 表 + 3 阶多项式,powf.c 用 4-bit log2 表 + 5 阶多项式),libm/Android.bp 以 whole_static_libs: ["libarm-optimized-routines-math"] 引入;其余函数来自 FreeBSD/NetBSD msun,sqrt/fabs/fma 等由 builtins.cpp 映射到硬件指令 [S34][S36][S74]。因此 math 的跨架构差异既可能来自 AOR 代码的编译结果(FMA、转换、分支),也可能来自 HAVE_FAST_ROUND/TOINT_INTRINSICS 等构建宏,不能仅凭“同一份 AOR 源码”断定同性能。
stdio(24 项)。 stdio_benchmark.cpp 的读写用例统一用 /dev/zero(读零、写丢弃),目的是测 stdio 缓冲/锁定开销而不是内核/文件系统;分 buffered/unbuffered、r/w/r+/a+、fread/fwrite,以及 fopen+fgetln/fgets/getline/fgetc+fclose 的 locking 与 __fsetlocking(BYCALLER) 对照;格式化用例覆盖 snprintf 的 literal/%s/%d/%1$s 与 sscanf 的 %s/%d/maps,并用硬编码 ParseMap 作为 baseline [S29]。循环外 fopen/分配 buffer,循环内只做传输/解析;SetBytesProcessed 按 chunk size 计。它不校验输出内容(除返回值/abort),也不模拟真实文件系统;/dev/zero 的写路径与磁盘写不同,不能把结果外推为存储性能。
syscall/time(48 项)。 syscall_mm_benchmark.cpp 覆盖匿名/文件 mmap、munmap、mprotect、madvise(DONTNEED/PAGEOUT/FREE),并区分 kBenchmarkAll、kBenchmarkMmapOnly、kBenchmarkMunmapOnly:mmap_only 在 mmap 后暂停计时再做逐页触碰和 munmap;munmap_only 先暂停,再 mmap/触碰,ResumeTiming 后计时 munmap;可写映射用 MakeAllocationResident 逐页触碰,失败 SkipWithError [S29]。unistd_benchmark.cpp 用 BIONIC_TRIVIAL_BENCHMARK 对比 getpid/gettid 的 libc 包装与裸 syscall,并测 fork(child usleep 后 _exit,parent 在暂停区 waitpid);time_benchmark.cpp 的 vDSO/syscall 对照见 3.20。syscall 家族的关键不是“libc 算法”,而是包装层/内核边界/vDSO 是否生效;跨平台比较必须固定内核、vDSO、页大小和映射策略。
property(9 项)。 property_benchmark.cpp 用 TemporaryDir + SystemProperties::AreaInit 建一个本地临时属性区,随机生成 1..512 个属性名/值,测 Get/Find/Read/__system_property_serial;BM_property_find_real 则遍历真实系统属性并逐个 __system_property_find,源码明确说“不可跨设备/跨启动比较,只用于理解端到端速度(含 /dev/__properties__ 的文件查找)” [S29]。context_lookup_benchmark.cpp 用 AOSP S 的 property_contexts 数据,比较 legacy ContextsSplit 与 trie PropertyInfoArea 的查找 [S76]。因此 property 家族有两条完全不同的口径:隔离数据结构 vs 真实系统状态;不能把 find_real 的数字与临时区数字混用。
math 还包含 sin_fesetenv/sin_feupdateenv 两项:它们在每次调用前切换浮点环境,用来观察异常标志/舍入模式变化对 sin 路径的影响;这两个用例的成本包含 fenv 操作本身,不能与普通 sin 直接比较。stdio 的 /dev/zero 选择是刻意的:读永远有数据、写永远成功且被丢弃,这样测到的主要是 stdio 缓冲/锁定/格式化,而不是磁盘或文件系统;但 fwrite 到 /dev/zero 与真实文件写仍有内核路径差异,跨设备比较时要标注。syscall 的 mmap_only/munmap_only/all 三种模式也说明:同一个 BM_syscall_mmap_* 名字下,计时可能只含 mmap、只含 munmap,或含“mmap+逐页触碰+munmap”;报告必须把模式写进 cell,否则同名不同义 [S29]。
atomic 与字符分类。 atomic_benchmark.cpp 的 10 项在 3.21 已述:单线程、volatile 计数器、empty 基线、relaxed/acquire/release/seq_cst、fetch_add、fence、mutex 临界区对照;它明确是 best-case,多核 coherence/竞争不在覆盖内 [S29]。ctype/wctype 用随机 ASCII/非 ASCII 字符数组,逐字符 DoNotOptimize,SetBytesProcessed=iterations*chars.size();wchar 只测对齐指针(源码明确说不测未对齐,因为“极其罕见”);inttypes 是 strtoimax/strtoumax 的 trivial 用例 [S29]。这些家族适合做“同 workload 的跨平台相对比较”,不适合单独承担“库整体快慢”的结论。字符分类与 wchar 用例的输入是随机/固定字符,跨平台比较时要确认字符集与 locale 一致;wchar 只测对齐指针,未对齐场景不在覆盖内,不能把它当作全场景结论。
调用链:BM_math_* → libm/AOR;BM_stdio_* → fread()/vfprintf();BM_syscall_mmap_* → mmap()/munmap() → 内核;BM_property_* → SystemProperties::Find()。 实现与接口依据:benchmarks/math_benchmark.cpp、benchmarks/stdio_benchmark.cpp、benchmarks/syscall_mm_benchmark.cpp、benchmarks/property_benchmark.cpp;libm/Android.bp 引入 AOR math。 失效边界:math 不校验 ULP/errno;stdio 用 /dev/zero 不代表磁盘;syscall 模式不同名同义;property 的 find_real 不可跨设备比较。 验证条件:math 用 ULP/特殊值对拍;stdio 检查返回值;syscall 失败看 SkipWithError;property 区分临时区与真实区。 成本与权衡:微基准可控但离端到端远;/dev/zero/临时属性区降低噪声但改变语义;跨设备比较必须固定输入与镜像 [S29][S34][S74][S76]。
3.24 面向 RISC-V64 与 Arm64 的测量方案
把上述机制用于“RISC-V64 目标平台 vs Arm64 对照平台”时,最小可比单元不是“某个 benchmark 名字”,而是 cell:同一个注册点、同一组参数(size/alignment/threads/clock id/文件/属性数量)、同一输入与 checksum、同一构建模式(动态/静态/glibc 对照不能混)、同一 libc/编译器/内核/vDSO 状态。Bionic harness 的 AT_* 简写会展开多实例,报告时必须把展开后的参数写进 cell 标识,不能只写 BM_string_memcpy。A16/A17 的 AT_ALL_PAGE_SIZES 差异也说明:跨版本比较前要先固定“实例集合”,否则分母不同。
原生 ns/op 或 throughput 与 Fixed-N 研发归因必须分账。正式 native 必须来自实际 native harness 记录,允许两端自适应 N 不同;同一平台内 signature/params/sink 稳定,跨端工作量语义匹配;Bionic 原生基准的独立样本单位是独立 OS 进程(不是 VM),Java/libcore 才是独立 VM,不能把 Java 的 VM 规则套到 Bionic。Fixed-N 的要求分两类:Bionic native 用 benchmark name + args(size/alignment/clock id/文件/属性数等)+ 线程/绑定方式 + N 作为 cell key,不能使用 Java 的 class/method/params;Java/libcore 才用 class/method/params/thread/N。至少 3 个独立进程,保存 elapsed/checksum/frequency/logs/AOT 记录;1p 只探索,3p 用全观测范围;是否追加第 4–6 个进程按预先登记的研发方法示例(比较目标/误差/波动阈值)决定,不把历史 90%/95% 阈值或旧频率/mask 当作新目标平台的统一标准;仍跨界则转 native 时间轨并保留 uncertain。历史 3p/6p 报告值只是历史 Fixed-N,不是新目标平台的正式 native,也不能由 plan 直接复算;历史账本同样不是新范围或新成绩。执行方式也要记录:原生自适应(min_time 时间目标)、--benchmark_min_time=<N>x 固定迭代、--bionic_iterations 全局 CLI 默认(每个 registration 应用;XML <iterations> 逐 benchmark 覆盖)是不同模式;不能只凭 Nx 或 Iterations 数值抹去 harness/执行方式身份。report.iterations、malloc 自建 worker 数、GB harness threads 是三个不同量,必须分开记录。本节采用共同方法 R1 作为方法学参考(原版协议已作废);R1 只提供历史输入/方法学边界,不提供新 目标平台 成绩或权威 scope。
ns/op × live GHz 只是频率归一化的周期代理(GHz 必须来自本轮现场记录),cycles/instructions 才是 CPI 类指标;二者都不是实测频率。PMU 的目标是避免 multiplex,而不是所有事件强制单独运行:无缩放、完整 runtime 时 cycles+instructions 可以同采;cache/TLB/branch 等按假设一次增加一个,驱动证明完整 runtime 才并采;empty baseline subtraction 不是通用必做,先验证对照可加性并保留 raw,不能自动扣出假因果。可重构平台频率归一化尤其要谨慎:它可以把不同 硬件配置/频率下的结果放到同一坐标上比较,但不等于真实同频硬件,也不能承诺“归一化后全面达标”;归一化只处理一阶频率差异,不处理微架构、cache、内存、调度和 vDSO/内核差异。历史阈值、频率与 affinity mask 只是旧包档案,不设为未来统一协议;未来目标需在实验前预先规定比较目标/误差与追加独立进程策略。
实验元数据至少记录:两端镜像/构建 ID、内核版本与 vDSO 符号、CPU/hart/affinity、频率与 governor、ISA/扩展(含 V、F/D、Zicbom 等)、编译器与 -march/-mcpu、libc/libm/Scudo 配置、是否开 debug malloc/GWP-ASan/HWASan、native 本身不需要 odex;若同设备跑 Java/libcore,另记 AOT/odex/ART 状态、PMU 事件组/valid ratio/raw。比较层必须对齐 image/Android/kernel/runtime/compile/GC,或显式标为混杂;历史对照平台(较旧 Android)与目标平台(较新 Android)之间的差异不能直接归因 CPU 架构。跨 ISA 对齐的是源码/版本/配置(同一 benchmark 源、同一参数/输入、可适用的构建配置),不要求两端二进制镜像相同——要求二进制相同既不可行,也会把工具链差异误当硬件差异。对 可重构平台 还要记录 硬件配置、可重构平台 频率、CPU 模式、内存/总线配置;这些字段不进入公开正文的原始值,但必须进入私有实验台账。缺镜像身份或 Java/libcore 的 odex 时如实写“未知”,不能用“应该相同”代替。
调用链:cell 定义 → 3 个独立 OS 进程 → 中位数/区间 → 跨界升级 4–6 进程 → native/Fixed-N 交叉核对。 实现与接口依据:Bionic harness 的 benchmarks/bionic_benchmarks.cpp(RegisterGoogleBenchmarks())与 src/benchmark_runner.cc(DoNIterations())给出 cell 注册/运行接口;cell 的 raw 目录与 metadata 字段见 配套设备实验计划。方法学采用 共同方法 R1 方法学参考;Java/libcore 另用独立 VM,不与本章混算。 失效边界:可重构平台频率归一化不等于同频硬件;历史阈值/mask 不设为新协议;比较层未对齐时不能归因 CPU;L3 eligible 仍 unknown。 验证条件:每个 cell 记录 raw/checksum/frequency/affinity/PMU;3p 后按预先登记的研发方法示例(比较目标/误差/波动阈值)决定是否追加 4–6 进程;保留 sample-1..6 与 metadata。 成本与权衡:3p 成本低但不确定性大;6p 更稳但机时翻倍;PMU 避免 multiplex 需要事件分组,增加采集复杂度 [S29][S71]。
3.25 优化候选:事实与假设分栏
候选按“能否用现有 public benchmark 直接验证”分三档:第一档是已有 cell 能直接扫描的(memcpy 的 size×alignment、malloc 的 decay/purge、clock 的 vDSO/syscall、mutex 的无竞争/原子 lowering);第二档是需要新增 harness 或改参数才能隔离的(有竞争 mutex、纯元数据 malloc、子进程 PMU 归因);第三档是只有设备/镜像/硬件配置 才能回答的(VLEN 实际收益、可重构平台 频率/内存子系统、具体微架构的 cache/TLB 行为)。第一档先做,第二档在设备上补 harness,第三档只作为获取项,不在离线稿里下结论。
| 候选 | 事实(源码/pin) | 假设(需实测) | 验证方法 |
|---|---|---|---|
riscv64 memcpy 对齐/尺寸分派 | 单一 RVV 字节循环,无对齐/尺寸分支 [S07] | 小尺寸标量/定长、未对齐首尾预处理、超大尺寸 NT store 可降成本 | 同 cell 扫 size×alignment;A/B 反汇编确认调用点;看 ns/op、bytes/s、CPI |
| RVV VLEN/多向量展开 | vsetvli e8,m8 每次按剩余 n 设置 VL | 更宽 VLEN/多倍展开降低循环开销,但尾块/跨页/频率可能抵消 | 固定 VLEN 对比、PMU cycles/instructions、代码尺寸 |
| AOR math 代码生成 | expf/powf/sincosf 为 AOR 查表+多项式,libm 整库引入 [S34][S74] | FMA/round 宏、编译器调度影响 ULP 与吞吐 | 同输入向量、latency/throughput 两版;对拍 ULP/errno |
| Scudo 分配路径 | libc 薄层 + primary/secondary;MallocFree 含页驻留 [S26][S27] | size class/thread cache/quarantine/decay 影响多大取决于 size/线程 | 分离页驻留与元数据;多线程吞吐 + purge 分栏 |
| pthread 无竞争快路径 | 普通 NonPI fast/slow 均不写 owner_tid;recursive/errorcheck 才维护 owner_tid,PI 独立结构;unlock 仅 contended 唤醒 [S22] | AMO/LR-SC、fence 强度、cache line 对齐影响成本 | 无竞争/有竞争两套 cell;perf futex/调度事件 |
| vDSO 时间路径 | 函数指针选择 vDSO,缺失回退 syscall [S16] | 目标镜像 vDSO 符号/clock 支持/时间页重试影响 ns/op | getauxval+符号表+strace/perf 三证;对照 _syscall |
| linker relocation | 169 个生成共享库 + 1 个 main,父进程 real time [S73] | RELR/类型分布/符号查找/页缺页/构造影响启动 | 子进程 perf/链接器统计;分装载/查找/重定位/构造 |
这张表的原则是:事实列只写源码能直接证明的内容,假设列一律标注“需实测”。任何百分比、倍数、达标承诺都不在本章出现;后续设备实验若产生数字,应单独入设备台账,并区分原生轨与 Fixed-N 轨、区分 可重构平台归一化与真实同频。优化候选还必须与正确性绑定:例如给 memcpy 加对齐/展开不能越界读;memcpy 重叠不属定义行为,重叠正确性归 memmove,不要给 memcpy 增加或要求重叠语义;给 mutex 换原子序列不能破坏 acquire/release;给 Scudo 调 size class 不能改变 malloc_usable_size/对齐要求;给 vDSO/时间路径做缓存不能返回过期时间。每一个候选的验收都应包含“同 cell 的 A/B + 正确性测试 + 失败路径”,不能只看 ns/op 下降。对跨 ISA 对照,还要把“源代码相同、编译配置可不同、二进制不必相同”写进实验记录,避免把工具链/镜像差异误判为微架构差异。具体执行时,每个候选先做一个最小 A/B:同一 cell、同一输入、同一绑核/频率,改动只在一处(例如换 memcpy 实现或换 size class),同时保留改动前后的二进制 SHA 与反汇编;如果 A/B 的差异小于同 cell 的 3p 波动,就不能把它当成优化收益。对需要新增 harness 的候选(竞争 mutex、纯元数据 malloc),先把 harness 纳入版本控制并记录 setup 边界,再谈数字。对只有设备才能回答的候选(VLEN/可重构平台/微架构),先写清获取项与判定阈值,避免在离线稿里用“应该更快”代替实测。
调用链:候选 → 同 cell A/B → 反汇编/正确性 → 3p/6p → 进入/放弃优化队列。 实现与接口依据:候选必须落到 libc/arch-riscv64/string/memcpy.S、libc/bionic/pthread_mutex.cpp、external/scudo/standalone/combined.h、libc/bionic/vdso.cpp 等具体实现;接口/ABI 约束见对应头文件与 bionic-unit-tests。 失效边界:A/B 差异小于 3p 波动;工具链/镜像/二进制不同;正确性/内存序/对齐要求被破坏;可重构平台归一化被误当同频。 验证条件:同一 cell、同一输入/绑核/频率;保留改动前后二进制 SHA 与反汇编;跑正确性测试与 3p/6p;记录失败路径。 成本与权衡:代码特化/多版本增加维护与代码尺寸;测量/正确性验证增加机时;收益必须大于波动才进入优化队列 [S07][S22][S27][S71]。
3.26 小结
Bionic 的 benchmark 套件是一套“可执行的机制地图”:333 个注册点覆盖 string/memory、allocator、math、stdio、pthread/atomic、syscall/time、property、linker/spawn、stdlib/字符分类等家族;每个家族都有自己的 harness、参数简写、计时边界和正确性盲区。真正可复现的结论需要同时回答五个问题:调到了哪个符号(-fno-builtin/反汇编)、跑了哪个实例(XML/简写/参数)、计时包含什么(Setup/页驻留/子进程/暂停区)、机器码是什么(RVV/AOR/AMO/fence/vDSO)、失败/回退是什么(vDSO 缺失、分配失败、exec 127、竞争未覆盖)。把这五个问题固定下来,才能把 RISC-V64 与 Arm64 的差异拆成“软件路径差异 / 硬件差异 / 工具链差异”,并为下一阶段的设备对照提供可审计的 cell 清单,而不是一组无法解释的分数。
还要把三层范围写在结论旁边:L1 历史账本只说明“过去测过什么”,L2 公开 pin 枚举只说明“源码里有什么”,L3 未来 eligible 才说明“下一轮实际能测什么”;三者不能互相替代。本文的 333 个注册点属于 L2,历史行数属于 L1,L3 仍是 unknown。依据也分四级:源码/pin 可证的(S)、本报告离线编译观察(O)、只读方法学参考(R)、未运行/未取得(N)。本章的结论只在 S/O/R 范围内成立;任何“快多少/提升多少/是否达标”的判断都需要 N 类依据变成实测数据,且必须区分原生轨与 Fixed-N 轨、区分 可重构平台归一化与真实同频硬件。
本章的家族/注册点统计、固定 pin 与可复制离线检查是公开材料;设备原始数据与实验记录不进入公开稿,二者用 cell 标识关联而不混写。
最后要明确本章没有做什么:没有上板、没有运行 bionic-benchmarks/spawn/linker-reloc、没有采 PMU、没有反汇编设备 libc.so、没有取得目标平台的 硬件配置/image/kernel/eligible manifest(Java/libcore 的 odex 另列),也没有产生任何新成绩。因此,本章的所有性能相关表述都是方法、假设或待检项,不是 PASS/收益承诺。等设备实验产生数据后,应在同一 cell 标识下补充 raw/checksum/frequency/PMU,并按 R1 的三层范围与原生/Fixed-N 分账重新核对;如果新数据推翻本章的某个假设,应更新章节并保留旧版本的差异记录,而不是悄悄改数字。
调用链:符号确认 → 实例/参数 → 计时边界 → 机器码/观测 → 失败/回退。 实现与接口依据:benchmarks/bionic_benchmarks.cpp、libc/arch-riscv64/string/memcpy.S、libc/bionic/malloc_common.cpp、libc/bionic/vdso.cpp、libc/bionic/pthread_mutex.cpp 是本章的五条主锚点;公开 pin 与 SHA 见 固定 pin 清单。 失效边界:未上板/未采 PMU/未反汇编设备 libc;L3 eligible 未知;历史账本不是新范围;任何分数都不能从本章推出。 验证条件:源码/pin 用 SHA 校验;离线探针可重跑;设备实验按 配套设备实验计划 的 cell/3p/PMU 字段执行。 成本与权衡:本章覆盖机制与方法,但不覆盖设备实测;把公开方法与设备实验记录分开,避免设备原始数据进入公开稿 [S07][S16][S22][S26][S29][S70]。
这段小结也提醒读者:本章的 333 是公开 pin 的注册点,不是默认运行实例或历史成绩;任何跨平台数字都必须同时带 cell、harness、计时边界与失败路径,否则只能作为方法学记录,不能作为性能结论。
故障定位速查
本表把"症状 → 先查什么 → 看哪个文件/符号"串成可复制入口,全部为静态检索方法,不含实测结论。使用时的通用前提是:先在产出物上用 nm -D/objdump -d 确认符号落入哪个实现(避免把内联或 libc++ 的开销误判为 bionic),再用 strace/perf 观察是否走系统调用或采样,最后回到对应源文件核对调用链与分支。
| 症状 | 先查什么 | 定位入口(pin=06356e41) |
|---|---|---|
memcpy 小尺寸偏慢 | 是否落到 RVV 单循环、有无尺寸分派 | libc/arch-riscv64/string/memcpy.S;对照 libc/arch-arm64/ifuncs.cpp [S07][S09] |
| 怀疑 RISC-V 无向量/无回退 | 编译期 ISA 是否含 RVV;无 hwprobe 分派 | 源码直接含 vsetvli/vle8.v,无 ifuncs.cpp [S07][S15] |
dlopen 慢 | 符号查找走 GNU 还是 SysV、待重定位库数 | linker/linker_soinfo.cpp:300(gnu_lookup)[S43] |
dlopen 报未定义符号 | 命名空间可见性与查找顺序 | linker/linker.cpp:2125(do_dlopen)、linker_namespaces.cpp [S12] |
COMPL/未知重定位报错 | RISC-V 专用重定位是否落入 default | linker/linker_relocate.cpp(unknown reloc type)[S13] |
clock_gettime 走了 syscall | vDSO 是否解析到符号 | libc/bionic/vdso.cpp:109(__libc_init_vdso)[S16] |
| 线程大量创建时慢 | 线程主映射尺寸/guard/缺页 | libc/bionic/pthread_create.cpp:221 [S21] |
| 锁争用下退化 | fast path CAS 是否失败、是否进 futex wait | libc/bionic/pthread_mutex.cpp:560/610 [S22] |
pthread_join 卡住 | 线程是否 detached、tid 是否被 CLONE_CHILD_CLEARTID 清零 | libc/bionic/pthread_join.cpp:64 [S23] |
| 堆 RSS 偏高 | M_DECAY_TIME/release 间隔、quarantine 是否启用 | libc/include/malloc.h、scudo/.../primary64.h:372 [S28][S49] |
| 分配变慢且疑似调试层 | 是否装了 libc_malloc_debug/heapprofd/GWP-ASan | libc/bionic/malloc_common_dynamic.cpp:96 [S26] |
| 浮点域错误未设 errno | libm 是否 -fno-math-errno/WANT_ERRNO=0 | libm/Android.bp、external/aor [S34][S36] |
sigreturn/信号栈异常 | riscv64 无 SA_RESTORER、altstack 是否建立 | libc/bionic/sigaction.cpp:50、pthread_create.cpp:100 [S33][S21] |
setjmp 校验失败 | cookie/checksum 是否被破坏 | libc/arch-riscv64/bionic/setjmp.S(_JB_CHECKSUM)[S24] |
| 解析域名慢/异常 | 是否经 netd 代理而非直连 | libc/dns/net/getaddrinfo.c:425 [S38] |
总结与展望
本报告以官方公开源码为唯一依据,把 bionic 从"库边界"到"热点定位"的链条钉在一组可复核的文件与函数上。最值得带走的几条判断是:第一,Bionic 的行为横跨多个仓库——malloc 在 external/scudo、部分 memcpy/exp 在 external/arm-optimized-routines、C++ 标准库在 libc++,定位热点时必须跨仓库检索,不能把 C++/libcore 侧开销记到 bionic 名下 [S01][S26][S09]。第二,启动与链接路径的顺序与约束决定"何时能安全调用什么"——链接器在自重定位前只能做最小初始化,libc.so 以最高优先级构造函数完成自身初始化,AT_SECURE 会清除不安全环境变量 [S11][S19][S20]。第三,RISC-V64 的字符串/内存实现是"编译期 RVV、无运行分派",而 ARM64 是"AOR 主体 + IFUNC 按 hwcap 运行分派";这是软件路径差异,具体影响必须实测 [S07][S09]。第四,"存在"不等于"支持完成"——IFUNC 机制在 riscv 可用但 bionic 未注册任何 riscv IFUNC,__riscv_hwprobe 存在但未用于 string/libm 分派 [S15][S16]。
面向后续 RISC-V64 优化的可执行问题清单(区分事实与待验证假设):
- 事实:riscv64
memcpy/memset为单一 VLEN-agnostic 循环、无短尺寸特化、无对齐处理 [S07]。待验证假设:在小尺寸(8–64B)区间引入尺寸分派(标量/短向量/长向量)或对齐处理可能改善表现;需按AT_COMMON_SIZES一致条件 A/B 实测,并做越界读与未对齐的正确性回归 [S29][S50]。 - 事实:riscv64 字符串例程广泛使用
vle8ff.v(fault-only-first)以保证跨页安全;strcmp已有 LMUL 阶梯 [S08]。待验证假设:strlen/strchr等是否能借鉴strcmp的自适应 LMUL 获得收益,取决于目标 VLEN 与实现代价。 - 事实:bionic 支持 riscv IFUNC(传
(AT_HWCAP, __riscv_hwprobe, nullptr))但自身未使用;riscv64 不支持 heap tagging(MaySupportMemoryTagging仅 aarch64)[S15][S27]。待验证假设:若为 riscv 字符串例程注册 IFUNC 以实现按 VLEN/扩展的运行分派,需评估装载期开销与正确性门(IFUNC 解析在链接器自重定位前运行,约束多)[S11][S15]。 - 事实:
malloc/free默认 Scudo,primary release 间隔实际 1000ms、quarantine 默认编译掉 [S27]。待验证假设:分配密集型用例的 RSS/延迟对M_DECAY_TIME/M_PURGE/cache 参数的敏感性,需在应用默认(MALLOC_USE_APP_DEFAULTS)与命令行默认两种模式下分别测量 [S49][S05]。 - 事实:riscv64 无手写原子汇编,具体 AMO vs LR/SC 由编译器 lowering 决定(源码不可证)[S22]。待验证假设:热锁路径的机器码序列(需反汇编
libc.so)是否在某些操作上可用更优的 AMO/Zacas序列——只有拿到反汇编才能提出有依据的优化。 - 方法门:任何候选优化都必须先复现基线(锁核、锁频、
DoNotOptimize、resident 化)、再采周期并按频率归一化、再做正确性回归,最后做同条件 A/B;不得把墙钟时间与周期混算,不得在多路复用未标明的采样上直接下结论 [S29][S30][S50]。
展望方面,RISC-V64 支持在公开主线上的走向可由提交序列观察:近期(2025–2026)bionic 持续为 riscv64 补充优化例程(如 strchrnul.S、strrchr.S 的新增)并更新 fenv-riscv64.c,说明 Google 在维持该 ABI 上的实投入方向是"补齐例程"而非"引入运行分派" [S07][S35]。这为后续优化给出两条现实路径:一是继续补齐/优化 bionic 内的 riscv 例程(自主可控、不依赖硬件分派);二是谨慎评估引入 IFUNC/hwprobe 运行分派(收益依赖硬件多样性,成本与正确性门较高)。两条路径都需要本报告 3.12 所述的测量纪律来验收。
参考来源
- [S01] bionic 仓库 maintainer overview 与结构说明,
README.md,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/README.md - [S02] bionic
docs/status.md(函数可用性与行为变更),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/docs/status.md - [S03] bionic
docs/elf-tls.md(ELF TLS 设计与 bionic 布局),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/docs/elf-tls.md - [S04] bionic
docs/clang_fortify_anatomy.md(Clang FORTIFY 机制),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/docs/clang_fortify_anatomy.md - [S05] bionic
docs/native_allocator.md(分配器验证与 Android 扩展),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/docs/native_allocator.md - [S06] bionic
docs/defines.md(平台宏选择),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/docs/defines.md - [S07] RISC-V64 字符串/内存汇编,
libc/arch-riscv64/string/{memcpy,memset,memmove,memchr,memcmp,strlen}.S,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/arch-riscv64/string/ - [S08] RISC-V64
strcmp.S(LMUL 阶梯)等,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/arch-riscv64/string/strcmp.S - [S09] ARM64 IFUNC 与外部 AOR 引用,
libc/arch-arm64/ifuncs.cpp、libc/Android.bp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/arch-arm64/ifuncs.cpp - [S10] bionic
libc/Android.bp(arch 分派与分配器 defaults),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/Android.bp - [S11] bionic
linker/linker_main.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/linker_main.cpp - [S12] bionic
linker/linker.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/linker.cpp - [S13] bionic
linker/linker_relocate.cpp(含 RELR/APS2/重定位处理),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/linker_relocate.cpp - [S14] bionic
linker/linker_relocs.h(RISC-V 重定位映射),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/linker_relocs.h - [S15] bionic
libc/bionic/bionic_call_ifunc_resolver.cpp(__riscv分支),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/bionic_call_ifunc_resolver.cpp - [S16] bionic
libc/bionic/vdso.cpp(vDSO 与__riscv_hwprobe),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/vdso.cpp - [S17] bionic
libc/SYSCALLS.TXT(syscall 生成格式),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/SYSCALLS.TXT - [S18] bionic
libc/bionic/__libc_init_main_thread.cpp、libc/bionic/__errno.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/__libc_init_main_thread.cpp - [S19] bionic
libc/bionic/libc_init_dynamic.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/libc_init_dynamic.cpp - [S20] bionic
libc/bionic/libc_init_common.cpp(__libc_init_common/AT_SECURE),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/libc_init_common.cpp - [S21] bionic
libc/bionic/pthread_create.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/pthread_create.cpp - [S22] bionic
libc/bionic/pthread_mutex.cpp、pthread_once.cpp、pthread_rwlock.cpp、pthread_cond.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/pthread_mutex.cpp - [S23] bionic
libc/bionic/pthread_join.cpp、libc/private/bionic_futex.h,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/pthread_join.cpp - [S24] bionic
libc/arch-riscv64/bionic/setjmp.S,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/arch-riscv64/bionic/setjmp.S - [S25] bionic
libc/platform/bionic/tls_defines.h、libc/platform/bionic/tls.h,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/platform/bionic/tls_defines.h - [S26] bionic
libc/bionic/malloc_common.cpp、malloc_common.h、bionic_allocator.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/malloc_common.cpp - [S27] Scudo 配置与初始化,
external/scudo/config/custom_scudo_config.h、standalone/combined.h,commit15715589:https://android.googlesource.com/platform/external/scudo/+/15715589eebe155c799ce6fe27005bc8775b86ad/config/custom_scudo_config.h - [S28] Scudo primary/chunk/mmap,
external/scudo/standalone/primary64.h、chunk.h、mem_map_linux.cpp,commit15715589:https://android.googlesource.com/platform/external/scudo/+/15715589eebe155c799ce6fe27005bc8775b86ad/standalone/primary64.h - [S29] bionic 基准与尺寸简写,
benchmarks/README.md、benchmarks/bionic_benchmarks.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/benchmarks/bionic_benchmarks.cpp - [S30] bionic 基准工具头,
benchmarks/util.h,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/benchmarks/util.h - [S31] RISC-V64 syscall 汇编,
libc/arch-riscv64/bionic/syscall.S,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/arch-riscv64/bionic/syscall.S - [S32] RISC-V64 线程栈拆卸,
libc/arch-riscv64/bionic/_exit_with_stack_teardown.S,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/arch-riscv64/bionic/_exit_with_stack_teardown.S - [S33] bionic
libc/bionic/sigaction.cpp、sigprocmask.cpp、signal.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/sigaction.cpp - [S34] bionic
libm/Android.bp、libm/builtins.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libm/Android.bp - [S35] bionic
libm/fenv-riscv64.c,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libm/fenv-riscv64.c - [S36] arm-optimized-routines 构建与数学实现,
Android.bp、math/exp.c,commit498b3de1:https://android.googlesource.com/platform/external/arm-optimized-routines/+/498b3de1a06e8cd87b2f9d49d1d8a23ebba5cc5a/Android.bp - [S37] bionic
libc/bionic/locale.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/locale.cpp - [S38] bionic
libc/dns/net/getaddrinfo.c(netd 代理边界),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/dns/net/getaddrinfo.c - [S39] RISC-V64 链接器入口,
linker/arch/riscv64/begin.S,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/arch/riscv64/begin.S - [S40] RISC-V64 TLSDESC 解析器,
linker/arch/riscv64/tlsdesc_resolver.S,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/arch/riscv64/tlsdesc_resolver.S - [S41] bionic
libc/include/sys/hwprobe.h,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/include/sys/hwprobe.h - [S42] AOSP manifest(分支语义),
platform/manifestdefault.xml,commitad156f32:https://android.googlesource.com/platform/manifest/+/ad156f32caaa06dae91c02d443f6a8fe210eaa54/default.xml - [S43] bionic
linker/linker_soinfo.cpp(符号查找/构造函数),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/linker_soinfo.cpp - [S44] bionic
libc/bionic/bionic_arc4random.cpp、getentropy.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/bionic_arc4random.cpp - [S45] FreeBSD msun
sin/cos实现,libm/upstream-freebsd/lib/msun/src/{s_sin.c,k_sin.c},commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libm/upstream-freebsd/lib/msun/src/s_sin.c - [S46] AOR
exp实现,math/exp.c,commit498b3de1:https://android.googlesource.com/platform/external/arm-optimized-routines/+/498b3de1a06e8cd87b2f9d49d1d8a23ebba5cc5a/math/exp.c - [S47] bionic
libc/private/KernelArgumentBlock.h,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/private/KernelArgumentBlock.h - [S48] bionic
libc/libstdc++.map.txt(libstdc++ ABI 导出),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/libstdc++.map.txt - [S49] bionic
libc/bionic/android_mallopt.cpp、libc/include/malloc.h,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libc/bionic/android_mallopt.cpp - [S50] bionic
docs/libc_assembler.md(汇编例程验证方法),commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/docs/libc_assembler.md - [S51]
external/scudo的 Android 16 对照快照(v1.1 取),android-16.0.0_r4commit03a49bce:https://android.googlesource.com/platform/external/scudo/+/03a49bce2224f003b603379a78c88c3dd5bed2af/config/custom_scudo_config.h - [S52]
external/arm-optimized-routines的 Android 16 对照快照(v1.1 取),android-16.0.0_r4commit1a2b3aea:https://android.googlesource.com/platform/external/arm-optimized-routines/+/1a2b3aeae89d8d89155305e22cec96fc1028081c/Android.bp - [S53]
external/libunwind_llvm公开镜像(默认分支 HEAD 冻结于b0da2a6a,2021-02-20;无android16/17分支;Android.bp中enabled: false、仅arm打开):https://android.googlesource.com/platform/external/libunwind_llvm/+/b0da2a6ab983c43638eea6e70f46d8265cb29d3a/Android.bp - [S54] Linux 内核 RISC-V vDSO 信号跳板,
arch/riscv/kernel/vdso/rt_sigreturn.S:以SYM_FUNC_START(__vdso_rt_sigreturn)定义函数/跳板符号(非数据符号),li a7,__NR_rt_sigreturn+ecall,并在vdso.lds.S的LINUX_4.15版本节点 global 导出;固定 pin:torvalds/linuxtagv6.12(URL 即该 tag 下的文件):https://github.com/torvalds/linux/blob/v6.12/arch/riscv/kernel/vdso/rt_sigreturn.S - [S55]
external/libunwind(GNU libunwind)公开镜像(HEAD 停在ec57b05e,2021-04-08,无对应 Soong 模块):https://android.googlesource.com/platform/external/libunwind/+/ec57b05e0acc7838d2a0f75551a18d28f670692e - [S56] bionic
libm/Android.bp的libm模块定义(cc_library { name: "libm" },含libm/libm.map.txtstubs 与 llndk):https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/libm/Android.bp - [S57] Android LLVM 工具链构建的 libunwind 目标,
toolchain/llvm_androidsrc/llvm_android/builders.py(class LibUnwindBuilder;把output_dir/lib/libunwind.a安装为libunwind-exported.a;riscv64 平台隐藏符号副本;LIBUNWIND_HIDE_SYMBOLS/LIBUNWIND_USE_FRAME_HEADER_CACHE),main commitffe9eb86:https://android.googlesource.com/toolchain/llvm_android/+/ffe9eb8642b9f1561c92f091ad078a97428f9913/src/llvm_android/builders.py - [S58] Android LLVM 工具链版本常量,
toolchain/llvm_androidsrc/llvm_android/constants.py(CLANG_PREBUILT_VERSION = 'clang-r547379')与android_version.py(当前 LLVMr563880/ git386af4a5),main commitffe9eb86:https://android.googlesource.com/toolchain/llvm_android/+/ffe9eb8642b9f1561c92f091ad078a97428f9913/src/llvm_android/constants.py (注意:这是llvm_androidmain分支的常量,不是本 bionic pin 的默认编译器——本 pin 的 Soong 默认是clang-r584948,源为 LLVM6d4a0935+picks,见 [S66][S67]) - [S59] clang 预编译仓库 pin 与目录(已取),
platform/prebuilts/clang/host/linux-x86android17-releasecommit29182889399818f97baa65ea238c80e8117981eb;根目录含clang-r547379/clang-r563880c/clang-r584948/clang-r584948b/clang-r596125/clang-stable:https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/29182889399818f97baa65ea238c80e8117981eb/ - [S60] Android LLVM 源码 fork,
toolchain/llvm-project(libunwind/子项目即libunwind-exported.a的实现来源;本报告不再以该 fork 的某个分支做绑定——v1.3 冻结表曾误记llvm-r487747(LLVM 17/Android 14 线),已在 v1.6 声明作废;实际绑定见 [S66][S67][S69]):https://android.googlesource.com/toolchain/llvm-project/ - [S61] debuggerd 栈回溯的 riscv64 寄存器模型,
system/unwinding/libunwindstackRegsRiscv64.cpp(枚举 pc/ra/sp/gp/tp/t0..t6/s0..s11/a0..a7;PTRACE_GETREGSET读NT_RISCV_VECTOR),commit31327c70:https://android.googlesource.com/platform/system/unwinding/+/31327c70da147fdc069598c4a5fd075aea8d3312/libunwindstack/RegsRiscv64.cpp - [S62] 上游 LLVM libunwind 的 riscv64 实现(上游 LLVM 源码),
llvm/llvm-projectcommita60110bb6bad9c475c3a01c2a1427d6b9600532d:Registers.hpp(sha2567dfd33fc…)、UnwindRegistersSave.S(343d875c…)、UnwindRegistersRestore.S(30587db7…);仅作机制参考,非 Android17 精确 fork:https://github.com/llvm/llvm-project/commit/a60110bb6bad9c475c3a01c2a1427d6b9600532d - [S63] RISC-V hwprobe 官方语义(unknown key 清零 key=-1/value=0 的前向兼容约定):https://docs.kernel.org/arch/riscv/hwprobe.html
- [S64] RISC-V 向量规范 v1.0(Unit-stride Fault-Only-First Loads 的 element-0 同步异常与
vl缩短语义):https://docs.riscv.org/reference/isa/unpriv/v-st-ext - [S65] 同 pin clang 预编译模块定义,
platform/prebuilts/clang/host/linux-x86Android.bp(commit29182889…;libunwind-android-default、libunwind、libunwind-exported,均llvm_prebuilt_library_static):https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/29182889399818f97baa65ea238c80e8117981eb/Android.bp - [S66] 同 pin Soong 默认 clang 与覆盖机制,
platform/build/soong(android17-release,commit6722dd8833db7482df1a2543ca3fcf67ddf0f7b1)cc/config/global.go(:436ClangDefaultVersion="clang-r584948";:617-624LLVM_PREBUILTS_{BASE,VERSION}覆盖;:631-632ClangVersion()→ReleaseBuildClangVersion(...)):https://android.googlesource.com/platform/build/soong/+/6722dd8833db7482df1a2543ca3fcf67ddf0f7b1/cc/config/global.go - [S69] 本 pin 默认 clang 的 base LLVM 源快照(v1.6 取),
llvm/llvm-projectcommit6d4a0935c850ec3ddfc70c4ba97b98adc35c676e的libunwind/src/{UnwindRegistersSave.S,UnwindRegistersRestore.S,Registers.hpp,UnwindCursor.hpp,DwarfInstructions.hpp}(本目录上游源码,含 provenance 与 SHA;riscv64 机制与 [S62] 一致):https://github.com/llvm/llvm-project/commit/6d4a0935c850ec3ddfc70c4ba97b98adc35c676e - [S67]
clang-r584948的源信息(取自 pin29182889…的 prebuilt 目录):AndroidVersion.txt(22.0.0/based on r584948):https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/29182889399818f97baa65ea238c80e8117981eb/clang-r584948/AndroidVersion.txt ;clang_source_info.md(Base revision6d4a0935c850ec3ddfc70c4ba97b98adc35c676e;cherry-picks 来自 llvm_androidd71d2816):https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86/+/29182889399818f97baa65ea238c80e8117981eb/clang-r584948/clang_source_info.md - [S68] 上游 LLVM libunwind 的 CFI 步进实现,commit
a60110bb6bad9c475c3a01c2a1427d6b9600532d(经上游公开源码取回):UnwindCursor.hpp(stepWithDwarfFDE:1071):https://raw.githubusercontent.com/llvm/llvm-project/a60110bb6bad9c475c3a01c2a1427d6b9600532d/libunwind/src/UnwindCursor.hpp ;DwarfInstructions.hpp(stepWithDwarf:36、getCFA:66、getSavedRegister定义 :99):https://raw.githubusercontent.com/llvm/llvm-project/a60110bb6bad9c475c3a01c2a1427d6b9600532d/libunwind/src/DwarfInstructions.hpp - [S70] bionic benchmark 构建与套件,
benchmarks/Android.bp、benchmarks/run-on-host.sh、benchmarks/suites/*.xml,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/benchmarks/Android.bp - [S71] Google Benchmark 计时/迭代/报告实现,
src/benchmark_runner.cc、src/benchmark.cc、src/commandlineflags.cc,commitc42745df:https://android.googlesource.com/platform/external/google-benchmark/+/c42745dfc2b140fde975369e41a097b0ccddc346/src/benchmark_runner.cc - [S72] Google Benchmark 时钟与统计,
src/timers.cc、src/thread_timer.h、src/statistics.cc,commitc42745df:https://android.googlesource.com/platform/external/google-benchmark/+/c42745dfc2b140fde975369e41a097b0ccddc346/src/timers.cc - [S73] linker relocation 与进程启动 harness,
benchmarks/linker_relocation/README.md、linker_reloc_bench.cpp、benchmarks/spawn/spawn_benchmark.h、spawn_benchmark.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/benchmarks/linker_relocation/README.md - [S74] AOR 单精度数学实现,
math/expf.c、math/powf.c、math/sincosf.c,commit498b3de1:https://android.googlesource.com/platform/external/arm-optimized-routines/+/498b3de1a06e8cd87b2f9d49d1d8a23ebba5cc5a/math/expf.c - [S75] bionic Android 16 benchmark 对照树,tag
android-16.0.0_r4,commitf22516cb:https://android.googlesource.com/platform/bionic/+/f22516cbc67e81c13cf943ce02f01b8929a98726/benchmarks/ - [S76] bionic property/context lookup benchmark 源,
benchmarks/property_benchmark.cpp、libc/system_properties/context_lookup_benchmark.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/benchmarks/property_benchmark.cpp - [S77] bionic 链接器
LD_DEBUG解析与合法类别,linker/linker_debug.cpp,commit06356e41:https://android.googlesource.com/platform/bionic/+/06356e41c5ed7b12220b24c05ba4fdb873b126a7/linker/linker_debug.cpp
附录 A:主要结论与依据对照
| 结论 | 依据(文件/函数/commit) | 关键来源 |
|---|---|---|
| bionic 的 libstdc++ 仅 C++ ABI,无 STL | libc/libstdc++.map.txt(_Znwm/__cxa_*) | [S48][S01] |
默认 malloc 是 Scudo,构建期 USE_SCUDO;bionic_allocator.cpp 非 malloc | libc/Android.bp、malloc_common.h、bionic_allocator.cpp | [S26][S10] |
启动链:_start→__linker_init→__libc_init_main_thread_{early,late,final}→构造→__libc_init→main | linker_main.cpp、begin.S、__libc_init_main_thread.cpp、libc_init_dynamic.cpp | [S11][S39][S18][S19] |
| 无惰性绑定,装载时一次性重定位 | linker.cpp(DT_BIND_NOW 忽略)、linker_relocate.cpp | [S12][S13] |
RISC-V 重定位经通用映射处理,COPY 不支持 | linker_relocs.h、linker_relocate.cpp | [S14][S13] |
vDSO 加速时钟;__riscv_hwprobe vDSO 优先否则内联 ecall | vdso.cpp、sys/hwprobe.h | [S16][S41] |
| riscv64 无手写原子汇编,AMO/LR-SC 由编译器决定(未证) | 全树检索 arch-riscv64/arch-arm64 无原子指令 | [S22] |
riscv64 TLS 用 Variant I、负偏移槽位;errno 在 pthread_internal_t | tls_defines.h、__errno.cpp | [S25][S18] |
| riscv64 memcpy/memset 为单一 VLEN-agnostic RVV 循环,无尺寸分派 | arch-riscv64/string/memcpy.S、memset.S | [S07] |
| ARM64 memcpy 主体来自 AOR 并经 IFUNC 按 hwcap 分派 | arch-arm64/ifuncs.cpp、libc/Android.bp | [S09][S10] |
| libm 不保证设 errno;riscv64 无 FP 异常陷入 | libm/Android.bp、fenv-riscv64.c、AOR WANT_ERRNO=0 | [S34][S35][S36] |
riscv64 无 SA_RESTORER,信号返回依赖内核/vDSO | sigaction.cpp、asm-riscv/asm/signal.h | [S33] |
getaddrinfo 经 netd 代理,不在 libc 内直发查询 | dns/net/getaddrinfo.c | [S38] |
分支语义:manifest default=android17-release,bionic main 冻结 | platform/manifest default.xml、ls-remote | [S42][S02] |
a16→a17 新增 riscv64 strchrnul.S/strrchr.S,linker 未涉 riscv | 目录 diff、git log(63e9530f4) | [S07][S12] |
观察身份说明(按 references/system-deep-research.md §5 分档):①已执行(离线编译):两处——§3.5 的 riscv64 原子 lowering 与 §3.11 的 stack-protector guard 取法;本报告实际运行了 clang 交叉编译并阅读 .s(样例/命令/产物 SHA 见离线编译记录),但均为"编译器 lowering 形态"、不是设备或系统 libc 的机器码。②源码确证(未运行):其余机制结论均为读 pin commit 的实现与调用所得。③可复现但本报告未运行:各节标注"(可复现,本报告未运行)"的命令。④外部来源:规范/内核源码/上游 LLVM 已注明出处(含 上游源码)。没有本人实测(未上板、未跑基准、未采集 PMU);性能量与正确性分开陈述,未测量一律不给数值。
A.2 问题×覆盖矩阵(核心 / 可选)
| 问题 | 级别 | 覆盖位置 | 状态 | 依据 |
|---|---|---|---|---|
| 各部件职责与边界(libc/libm/libdl/libstdc++/linker、外部依赖、UAPI、APEX) | 核心 | 3.1 | 已覆盖 | 源码 [S01][S48][S34] |
| 启动链 exec→__libc_init→main、auxv/TLS/errno/构造 | 核心 | 3.2 | 已覆盖 | 源码 [S11][S39][S19] |
| linker 装载/符号查找/重定位/命名空间/构造顺序 | 核心 | 3.3 | 已覆盖 | 源码 [S12][S13][S43] |
| syscall 生成/ABI/vDSO/hwprobe | 核心 | 3.4 | 已覆盖 | 源码 [S16][S15][S41] |
| pthread/TLS/futex/原子(含 riscv 原子指令形态) | 核心 | 3.5 | 部分交付(原子 AMO/LR-SC 形态需反汇编,本报告未上板) | 源码 [S22];路径见附录 C |
| 分配器选择与 Scudo 参数 | 核心 | 3.6 | 已覆盖 | 源码 [S26][S27][S28] |
| string/memory 通用与 riscv 实现、IFUNC/RVV | 核心 | 3.7 | 已覆盖 | 源码 [S07][S08][S09] |
| libm 来源/errno/精度/fenv | 核心 | 3.8 | 已覆盖 | 源码 [S34][S36] |
| signal/setjmp/unwind | 核心 | 3.9 | 部分交付(来源链、riscv64 save/restore/CFI、本 pin 默认 clang-r584948 绑定已补;"产品实物实际选用"不可断言) | 源码 [S24][S33][S57][S62][S66];路径见附录 C |
| stdio/时间/随机/locale/网络解析 | 核心 | 3.10 | 已覆盖 | 源码 [S37][S38][S44] |
| FORTIFY/stack protector/构建实现选择/兼容 | 核心 | 3.11 | 已覆盖 | 源码 [S04][S06] |
| 基准与测量口径、污染控制 | 核心 | 3.12 | 已覆盖(方法与可复现命令;本报告未实测) | 源码 [S29][S30] |
| 主线 vs Android16 差异、RISC-V64 支持分层 | 核心 | 3.13 | 已覆盖 | 源码/diff [S07][S12] |
| riscv64 与 arm64 的实测倍数、VLEN 具体表现 | 可选扩展 | — | 未交付(需上板 PMU,超出本报告范围) | 见附录 C |
"部分交付"与"未交付"均不等同"已完成";缺口原因与补齐路径见附录 C。
A.3 架构/平台特定代码与通用代码边界表
下表列出同一机制在"通用代码"与"架构/平台特定代码"两侧的入口与分派点,便于判断"改哪边、影响谁"(版本均为 bionic 06356e41)。
| 机制 | 通用代码(位置) | 架构/平台特定代码(位置) | 分派/接口点 | 依据 |
|---|---|---|---|---|
| syscall 包装 | SYSCALLS.TXT + gensyscalls.py | libc/arch-riscv64/bionic/syscall.S | 构建期按 arch_list 生成 | [S17][S31] |
| string/memory(riscv) | (无通用回退) | libc/arch-riscv64/string/*.S(RVV) | 链接期静态选择 | [S07][S08] |
| string/memory(arm64) | external/arm-optimized-routines | libc/arch-arm64/ifuncs.cpp | IFUNC 装载期按 hwcap | [S09][S10] |
| TLS 布局 | libc/private/bionic_elf_tls.h | libc/platform/bionic/tls_defines.h(riscv Variant I,负偏移) | 编译期 | [S25][S03] |
| 浮点环境 | libm/fenv-*.c 通用骨架 | libm/fenv-riscv64.c(fcsr/fflags/frrm) | 编译期 | [S35] |
| 原子操作 | C11 <stdatomic.h>(通用) | 无手写 asm(编译器 lowering) | 编译期 | [S22] |
| ifunc 解析 | libc/bionic/bionic_call_ifunc_resolver.cpp | __riscv(传 (__riscv_hwprobe))/__aarch64__(传 hwcap)分支 | 链接器装载期 | [S15] |
| setjmp/longjmp | —(setjmp_cookie.cpp 通用 cookie/校验) | libc/arch-riscv64/bionic/setjmp.S | 编译期 | [S24] |
| 线程栈拆卸 | — | libc/arch-riscv64/bionic/_exit_with_stack_teardown.S | 编译期 | [S32] |
| 信号返回跳板 | libc/bionic/sigaction.cpp | riscv 不设 sa_restorer(内核/vDSO __vdso_rt_sigreturn);x86_64/arm32 设 __restore_rt | 编译期/ABI | [S33][S54] |
附录 B:检查与验收清单
| 检查项 | 方法 | 期望 | 覆盖来源 |
|---|---|---|---|
| 版本身份 | git ls-remote 核 commit;确认 tag 与 main 分开 | 与 pin 表一致 | [S42] |
| riscv64 存在 RVV 字符串实现 | 读 arch-riscv64/string/*.S | 含 `vsetvli/vle8(v | .ff)` |
| riscv64 无运行分派 | 查是否存在 arch-riscv64/ifuncs.cpp | 不存在 | [S09][S15] |
| 分配器选择 | 读 libc/Android.bp 的 USE_SCUDO | 默认 Scudo | [S26][S10] |
| 启动顺序 | 读 linker_main.cpp/begin.S | early→relocate→late→final→main | [S11][S39] |
| 重定位类型 | 读 linker_relocs.h RISC-V 映射 | 无遗漏映射;COPY 拒绝 | [S14][S13] |
| TLS 布局 | 读 tls_defines.h、elf-tls.md | riscv64 Variant I | [S25][S03] |
| 编辑/回归 | 运行 bionic-unit-tests 及字符串边界测试 | 通过 | [S01][S50] |
| 基准方法 | 锁核/锁频、DoNotOptimize、resident 化 | 同条件可比 | [S29][S30] |
| 公开稿脱敏 | 检索内网地址/绝对路径/设备原始数据 | 无命中 | 本报告约定 |
附录 C:研究方法与局限
本报告采用"先固定版本、再建覆盖矩阵、逐机制做源码级阅读、用矩阵反向找漏项"的方法。版本固定上,全部结论锚定官方 android.googlesource.com 的公开 commit,当前主线取 android17-release@06356e41,Android 16 基线取 android-16.0.0_r4@f22516cb,外部依赖(external/scudo、external/arm-optimized-routines、external/llvm-libc、external/libcxx)同为 android17-release 固定 commit;分支与版本身份在 2026-10-16 用官方 git 协议核验,并在任务目录的 离线编译记录 留下 pin 表与访问方式。取码方式为 git clone(浅取或 --filter=blob:none),仅做只读阅读,未修改任何上游源码。
覆盖方法上,本报告先建立了按机制(而非按章节)的问题/能力矩阵,记录每个机制的研究问题、已查版本/文件/函数、调用链、失败/回退、验证方式、性能关联、反例与剩余缺口;研究完成后用该矩阵反向核对覆盖,并对关键断言做独立复核。
局限与未决点必须显式列出,不能以篇幅或锚点数代替正确性。第一,未在真实设备上运行任何基准或采集 PMU,全文无任何性能数字;实际执行过的计算是两处离线交叉编译(§3.5 原子 lowering、§3.11 stack-protector guard 取法;clang + 阅读 .s,非设备或系统 libc 机器码,见离线编译记录);凡涉及收益的表述均为"待验证假设"并给出验证门。第二,若干结论为推断而非源码直证,已就地标注:riscv64 RISC-V 专用重定位若出现在动态表会落入 default 报未知类型;riscv64 原子的具体 AMO/LR-SC 形态需反汇编 libc.so 才能确认(本报告未上板)。信号返回跳板一项已在 v1.2 由公开内核源码确认:riscv64 的 __vdso_rt_sigreturn 定义于 arch/riscv/kernel/vdso/rt_sigreturn.S 并用 ecall 发 rt_sigreturn [S54]。第三,外部实现源覆盖:external/arm-optimized-routines 与 external/scudo 已补取 Android 16 快照做同仓库对比(关键常量与 riscv 构建取向一致);libc++/libc++abi 只固定 commit 未逐文件研究;unwinder 的 _Unwind_* 实现来源链已确证为 Android LLVM 工具链的 LibUnwindBuilder 从 toolchain/llvm-project 的 libunwind 子项目构建、安装为 libunwind-exported.a 供 libc.so 静态链入(详见 §3.9;此前"模块不在公开镜像集合"的表述是中途结论,已修正)[S57][S58][S60];riscv64 的 save/restore、寄存器映射与 CFI 步进已由上游 LLVM a60110bb 与同 pin prebuilt/Soong 证据补入 §3.9 [S62][S65][S66][S67][S68];该 pin 的默认编译器已绑定为 clang-r584948(源=LLVM 6d4a0935+llvm_android d71d2816 的 picks),并记录 LLVM_PREBUILTS_* 覆盖机制。仍不可断言的是"某具体产品构建实物实际选用哪一版 clang"(属发布构建记录,不在本节可核范围)[S66][S67]。第四,本报告聚焦 bionic,ART/libcore 内部实现分属独立研究,二者交界(如 JNI、native bridge、libsigchain)只点到边界未深入。第五,分支名与版本演进部分仅覆盖到公开可核的 commit 序列;对尚未发布或仅存于内部的实现与优化,本报告不纳入、不臆测。
冻结台账更正(过程性记录):V1.3-FREEZE.json 曾把 toolchain/llvm-project 记为 llvm-r487747(d9f89f4d…)——该分支属 LLVM 17/Android 14 时代的另一条线,与本报告 pin 无关,该行作废;为不改不可变历史,旧 freeze 文件保持原样,更正记录见 V1.6-FREEZE.json 与 V1.7-FREEZE.json。与本 pin 相关的两条线是:默认线 clang-r584948 ← base LLVM 6d4a0935 + 40 picks;另一条 clang-r547379 ← llvm-r547379-release@b718bcaf(存在于同 pin prebuilt 目录,但非默认)[S66][S67]。
附录 D:术语与缩略语
- bionic:Android 的 C 库、数学库与动态链接器集合。
- linker / linker64:动态链接器可执行体,由 ELF
PT_INTERP触发,负责映射、符号解析与重定位。 - DT_NEEDED / PT_INTERP / PT_GNU_RELRO:ELF 动态标签依赖项、程序头解释器类型、只读重定位段(
DT_*是动态段标签、PT_*是程序头类型,不可混用;解释器是PT_INTERP)。 - RELRO:
PT_GNU_RELRO描述的、重定位后改只读的段。 - RELR / APS2:紧凑相对重定位(
DT_RELR)与 Android packed relocations(APS2)。 - IFUNC:间接函数,装载期由解析器按 CPU 能力返回实际实现地址。
- auxv / AT_*:内核传给用户态的程序参数向量及其标签(如
AT_BASE、AT_ENTRY、AT_SYSINFO_EHDR、AT_RANDOM、AT_SECURE)。 - vDSO:内核映射到用户态的共享对象,提供如
clock_gettime的快速路径。 - hwprobe(
__riscv_hwprobe):RISC-V 探测 CPU/扩展能力的接口(vDSO 或 syscall)。 - TLS / TP / TCB / DTV / TLSDESC:线程局部存储、线程指针、线程控制块、动态线程向量、TLS 描述符。
- futex / PI:快速用户态互斥原语;PI 指优先级继承(
FUTEX_LOCK_PI等)。 - FORTIFY:基于
pass_object_size/__builtin_object_size的编译期与运行期缓冲区检查。 - Scudo:Android 默认原生分配器(
external/scudo)。 - AOR:
external/arm-optimized-routines,提供优化的字符串/内存/数学例程。 - LMUL / VLEN / vsetvli / vle8ff.v:RISC-V 向量扩展;LMUL 为寄存器组倍数;VLEN 为向量位宽;
vsetvli配置向量长度;vle8ff.v为 Unit-stride Fault-Only-First 加载(element 0 异常仍 trap,后续元素异常可缩短vl)。 - SCS:影子调用栈(Shadow Call Stack)。
- APEX:Android 的模块化系统组件封装(此处指
com.android.runtime)。 - netd:Android 网络守护进程,
getaddrinfo的 DNS 事务由其DnsProxyListener承担。