#!/usr/bin/env python3 """APK native 库体检(T-AND-015):纯标准库解析 ELF,不需要 readelf / NDK。 为什么需要它(T-AND-012 的「先按 ABI / native / R8 排查」): `ExceptionInInitializerError` / `UnsatisfiedLinkError` 这类「一装上就崩」的问题, 根因绝大多数落在 native 库上,而它们**都能在没有设备的情况下查出来**: 1. **ABI 与 ELF 机器类型不匹配** —— 把 x86_64 的 .so 放进 `lib/arm64-v8a/`, 安装能过、一加载就崩; 2. **DT_NEEDED 依赖缺失** —— 例如只打包了 `libmediapipe_jni.so` 却没带 `libc++_shared.so`(MediaPipe 早期版本的经典坑)。系统库由 Android 提供, 非系统库必须**同 ABI 目录内**自带; 3. **16 KB 页面对齐** —— Android 15 起部分设备页大小为 16 KB,`PT_LOAD` 的 `p_align` 若仍是 4 KB(0x1000),这些设备上会直接加载失败(Google Play 2025-11 起也要求 16 KB 对齐); 4. **路径/命名**:.so 必须直接位于 `lib//` 下(子目录不会被安装)。 本脚本把这些一次查完,并给出可读结论。**它不能替代真机**(运行时行为、 GPU/驱动差异仍需设备),但能把「打包配置错误」这一类彻底挡在发布前。 用法: python3 build/check_native_libs.py [--expected-abis arm64-v8a,armeabi-v7a] [--min-page-align 16384] [--json] 退出码:0 通过 / 1 有问题 / 2 用法或文件问题。 """ from __future__ import annotations import argparse import json import struct import sys import zipfile # Android 系统提供的库(不需要 App 自带) SYSTEM_LIBS = { "libc.so", "libm.so", "libdl.so", "liblog.so", "libz.so", "libstdc++.so", "libandroid.so", "libjnigraphics.so", "libEGL.so", "libGLESv1_CM.so", "libGLESv2.so", "libOpenSLES.so", "libOpenMAXAL.so", "libnativewindow.so", "libvulkan.so", "libsync.so", "libcutils.so", "libutils.so", "libui.so", "libgui.so", "libbinder.so", "libhardware.so", "libcamera2ndk.so", "libmediandk.so", "libaaudio.so", "libneuralnetworks.so", "libOpenCL.so", } # ABI → 期望的 (ELF class, e_machine) ABI_MACHINE = { "arm64-v8a": (2, 183), # ELFCLASS64, EM_AARCH64 "armeabi-v7a": (1, 40), # ELFCLASS32, EM_ARM "x86_64": (2, 62), # ELFCLASS64, EM_X86_64 "x86": (1, 3), # ELFCLASS32, EM_386 } MACHINE_NAME = {183: "AArch64", 40: "ARM", 62: "x86-64", 3: "Intel 80386"} class ElfError(Exception): pass # 人类可读输出;--json 模式下静音,保证 stdout 是**纯 JSON**(便于脚本消费) EMIT = True def emit(*parts) -> None: if EMIT: print(*parts) def parse_elf(data: bytes) -> dict: """解析出 {elf_class, machine, is_64, needed[], soname, page_aligns[]}""" if len(data) < 64 or data[:4] != b"\x7fELF": raise ElfError("不是 ELF 文件(魔数 %r)" % data[:4]) elf_class = data[4] # 1=32bit, 2=64bit data_encoding = data[5] # 1=LE, 2=BE if data_encoding != 1: raise ElfError("只支持小端 ELF(EI_DATA=%d)" % data_encoding) is_64 = elf_class == 2 if not is_64 and elf_class != 1: raise ElfError("未知的 EI_CLASS=%d" % elf_class) machine = struct.unpack_from(" len(data): raise ElfError("程序头越界") p_type, = struct.unpack_from(" int | None: for v, o, sz in loads: if v <= vaddr < v + sz: return o + (vaddr - v) return None strtab_off = vaddr_to_offset(strtab_vaddr) if strtab_vaddr is not None else None if strtab_off is not None: limit = strtab_off + (strsz or 0) for d_tag, d_val in entries: if d_tag not in (1, 14): # DT_NEEDED / DT_SONAME continue start = strtab_off + d_val end = data.find(b"\0", start, limit if limit > start else len(data)) if end < 0: continue name = data[start:end].decode("utf-8", "replace") if d_tag == 1: needed.append(name) else: soname = name return { "elf_class": elf_class, "machine": machine, "needed": needed, "soname": soname, "page_aligns": page_aligns, } def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(add_help=True) parser.add_argument("apk") parser.add_argument("--expected-abis", default="arm64-v8a,armeabi-v7a") parser.add_argument("--min-page-align", type=int, default=16384) parser.add_argument("--json", action="store_true") args = parser.parse_args(argv[1:]) expected_abis = [a.strip() for a in args.expected_abis.split(",") if a.strip()] global EMIT EMIT = not args.json try: zf = zipfile.ZipFile(args.apk) except (OSError, zipfile.BadZipFile) as exc: print("✗ 无法读取 APK:%s" % exc, file=sys.stderr) return 2 libs: dict[str, list[str]] = {} for name in zf.namelist(): parts = name.split("/") if len(parts) == 3 and parts[0] == "lib" and name.endswith(".so"): libs.setdefault(parts[1], []).append(parts[2]) for abi in libs: libs[abi].sort() problems: list[str] = [] notes: list[str] = [] if not libs: print("✗ APK 里没有任何 lib//*.so", file=sys.stderr) return 1 found_abis = sorted(libs.keys()) if found_abis != sorted(expected_abis): problems.append("ABI 集合为 %s,期望 %s" % (found_abis, sorted(expected_abis))) report: dict = {"abis": {}} emit(" APK native 库体检:%s" % args.apk) for abi in found_abis: emit(" [%s]" % abi) entries = [] abi_files = set(libs[abi]) for lib in libs[abi]: raw = zf.read("lib/%s/%s" % (abi, lib)) try: info = parse_elf(raw) except ElfError as exc: problems.append("%s/%s:%s" % (abi, lib, exc)) emit(" ✗ %-32s %s" % (lib, exc)) continue want_class, want_machine = ABI_MACHINE.get(abi, (None, None)) machine_name = MACHINE_NAME.get(info["machine"], str(info["machine"])) if want_machine is not None and (info["elf_class"], info["machine"]) != (want_class, want_machine): problems.append( "%s/%s:ELF class=%d machine=%s,与该 ABI 不符(该 ABI 下会加载失败)" % (abi, lib, info["elf_class"], machine_name) ) emit(" ✗ %-32s ELF 类型不符:class=%d machine=%s" % (lib, info["elf_class"], machine_name)) else: emit(" ✓ %-32s ELF%s %s" % (lib, "64" if info["elf_class"] == 2 else "32", machine_name)) # 依赖闭包:非系统库必须在同 ABI 目录内自带 missing = [ dep for dep in info["needed"] if dep not in SYSTEM_LIBS and dep not in abi_files ] if missing: problems.append("%s/%s:缺少依赖 %s(同 ABI 目录内没有)" % (abi, lib, ", ".join(missing))) emit(" ✗ 缺少依赖:%s" % ", ".join(missing)) else: extra = [d for d in info["needed"] if d not in SYSTEM_LIBS] emit(" ✓ 依赖闭包完整(需自带 %s;系统库 %d 项)" % ( ", ".join(extra) if extra else "无", len(info["needed"]) - len(extra))) # 16 KB 页面对齐 bad_align = [a for a in info["page_aligns"] if a < args.min_page_align] if bad_align: problems.append( "%s/%s:PT_LOAD p_align=%s < %d,Android 15+ 的 16 KB 页设备会加载失败" % (abi, lib, ", ".join(hex(a) for a in bad_align), args.min_page_align) ) emit(" ✗ 页对齐不足:%s" % ", ".join(hex(a) for a in bad_align)) else: aligns = sorted(set(info["page_aligns"])) emit(" ✓ 页对齐 %s(≥ %d,16 KB 页兼容)" % ( " ".join(hex(a) for a in aligns), args.min_page_align)) entries.append({"lib": lib, **{k: info[k] for k in ("machine", "elf_class", "needed", "soname")}, "page_aligns": info["page_aligns"]}) report["abis"][abi] = entries # libc++ 共享库的经典坑:若某个 .so 需要它,必须自带 for abi, libs_here in libs.items(): if "libc++_shared.so" in libs_here: notes.append("%s 自带 libc++_shared.so" % abi) report["problems"] = problems report["notes"] = notes if args.json: print(json.dumps(report, ensure_ascii=False, indent=2)) if problems: print("") for p in problems: print(" ✗ %s" % p, file=sys.stderr) print(" ✗ native 库体检发现 %d 个问题" % len(problems), file=sys.stderr) return 1 emit(" ✓ native 库体检通过:%s | ELF 类型匹配、依赖闭包完整、16 KB 页对齐" % ", ".join(found_abis)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))