先看结论与判断条件

  • 从最终 APK 的每个 ABI 提取 SO 和未定义符号,绑定文件摘要、Machine、minSdk、NDK 与依赖来源,不能只检查中间构建目录。
  • 直接链接的系统符号必须在 minSdk 边界可用;高版本能力应使用经过工具链支持的弱引用或 dlopen/dlsym,并验证缺失时的回退。
  • 链接成功只说明构建时输入能够解析,不能证明低 API 系统镜像、linker namespace、厂商实现和设备安装集合提供相同符号。
  • ABI 具有独立调用约定、寄存器与对齐,符号策略要与最终 ABI 产物逐一绑定,但本文不重复完整 ABI 交付矩阵。
  • CTS 验证设备实现与 Android 兼容性要求的一致性,不代表第三方 App 的加固 SO、业务 JNI 和自定义动态加载路径已经通过。
  • 静态目录、未定义符号扫描和设备 instrumented test 分层记录;任何未登记系统符号、越界直接调用或无守卫回退都阻断发布。

链接成功与目标设备可用是两件事

Native 链接发生在构建环境,使用指定 sysroot、API 级别、对象文件和库;设备装载发生在目标 Android 系统,使用实际系统镜像、ABI、linker namespace 与安装集合。构建时能生成 SO,只说明当前链接步骤满足,并不证明 minSdk 设备存在同名系统符号,也不证明符号版本、调用约定和运行分支适合目标环境。

Android NDK stable APIs 明确给出稳定 Native API 与可用级别边界。若某 API 高于应用 minSdk,不能直接静态调用并期待低版本安全忽略;需要在调用前完成版本和符号可用性判断,并提供缺失时的业务回退。发布检查从最终 SO 的未定义符号出发,把每个系统引用连接到首次可用 API 与调用方式。

检查范围只覆盖系统符号与 minSdk 可用性,不重复完整重定位、DT_NEEDED、linker namespace 或 ABI 交付矩阵。相邻问题通过证据链接:本页发现动态解析时,可以继续参考同站 [dlopen 与 linker namespace 排查](/zh-cn/articles/dlopen-linker-namespace-hardening/);当前结论仍限定在符号目录、守卫和实际调用结果。

构建、装载和调用三个阶段的差异
阶段主要输入成功表示不能证明
编译头文件、宏和目标 API源码可生成对象设备符号存在
链接sysroot、对象、依赖和参数SO 可形成低 API 能装载
APK 打包各 ABI SO 与 split文件进入候选目标设备收到正确集合
动态装载系统镜像、namespace 和依赖linker 接受库可选函数路径正确
符号调用版本守卫、参数和线程本次调用完成所有输入与设备兼容
业务验收真实入口和失败回退列出场景符合契约未执行矩阵通过

先固定最终 SO、minSdk 与 ABI 身份

扫描对象必须从最终签名 APK 或设备实际 split 中提取,不能使用未 strip 中间库替代。记录 APK SHA-256、SO SHA-256、压缩路径、ABI、ELF Machine、Build ID、build variant、minSdk、targetSdk、NDK 和链接器版本。加固处理、strip、打包或渠道步骤都可能改变最终文件,旧输出不能自动继承。

Android ABIs 文档说明每个 ABI 具有独立调用约定、寄存器使用、对齐和设备支持范围。即使两个 ABI 导出相同 C 名称,它们的最终对象、依赖和优化结果仍不同。符号清单按 ABI 建立,Machine 与 APK 路径必须一致;声明支持某 ABI 不证明对应文件已经构建、打包或在设备运行。

minSdk 也必须来自发布变体,而不是仓库默认值或开发者记忆。不同 product flavor、动态模块和预编译库可能有不同最低要求,最终应用取值还会受合并配置影响。门禁记录实际候选使用的 minSdk,并让每个系统符号目录以该值判定。无法从发布证明确认时,状态为待审,不能选择一个较高值让扫描通过。

系统符号审计的候选身份
字段来源作用错误替代
apkSha256最终签名候选连接安装对象同名 APK
soSha256APK 中最终文件连接静态与运行证据中间 build 输出
ABI 与 MachineAPK 路径和 ELF header选择调用约定与目录按文件名猜测
minSdk发布变体证明判定直接 API 边界使用 targetSdk
NDK/linker构建证明解释符号与弱引用行为只写最新版本
Build ID最终 ELF note连接符号和崩溃另一个候选 ID

未定义符号需要先区分系统、运行库和项目依赖

GNU readelf 可以读取 ELF header、dynamic section、符号和重定位。使用宽输出查看动态符号表,将索引为 UND 的符号视为需要外部解析的候选,再按来源分类为 Android 稳定系统 API、C/C++ 运行库、NDK 库、项目其他 SO、第三方预编译库或未知来源。UND 不等于错误,但每个条目必须知道由谁在何时提供。

符号名称可能带版本后缀、弱绑定、C++ 名字修饰或工具显示差异。归一化时保留原始行,并在目录中同时记录 rawName、baseName、binding、visibility、version 和 sourceLibrary。粗暴删除 @ 后内容用于查表可能方便,但最终判定仍要确认版本需求;只 grep 一个字符串会漏掉绑定与版本差异。

项目内部符号和系统符号应分开。系统目录回答首次可用 API 与调用策略;项目依赖目录回答哪个最终 SO 提供、是否同 ABI 打包和装载顺序。未知符号不应自动归入 libc,也不因名称以下划线开头就忽略。门禁输出未知来源、目录缺项和 ABI 不匹配,交给依赖或 API 专项处理。

未定义符号的来源分类
来源判定证据主要风险后续检查
稳定系统 APINDK API 目录与首次级别minSdk 越界直接或守卫调用
C/C++ 运行库STL 配置和依赖运行库不一致全 APK 依赖组合
项目 SO最终提供者符号表缺库或装载顺序DT_NEEDED 与入口
第三方预编译库供应方版本与文件摘要最低 API 或 ABI 不明供应链和设备回归
弱未定义符号binding 与守卫证据缺失后仍被调用无符号分支
未知来源目录无匹配无法解释装载行为默认阻断

直接调用必须满足 minSdk,守卫要控制真实调用

直接引用系统函数时,最终 SO 的未定义符号会由动态链接过程解析。若函数首次可用 API 高于 minSdk,低版本可能在业务分支执行前就遇到装载或解析问题。静态门禁把 usage=direct 与 introducedApi 比较,introducedApi 大于 minSdk 时失败,不接受“这段代码在低版本不会点到”作为证据。

高版本能力可以采用运行时解析,但必须把库加载、符号查询、返回值检查、版本条件和失败回退放在同一可审阅路径。Android NDK stable APIs 提到使用 dlopen 与 dlsym 处理版本化 API 的方式。系统版本判断本身不足够,仍要检查符号指针;符号存在也不保证当前 namespace、设备实现和业务前置条件全部成立。

工具链支持的弱 API 引用同样要求调用前检查。弱绑定只改变符号缺失时的链接或解析语义,不会自动阻止空地址调用,也不定义产品回退。目录为 weak_checked 或 runtime_lookup 条目记录 guardEvidenceId、fallbackId、最低测试 API 和最高测试 API;缺少守卫证据时即使静态符号标为 WEAK 也不放行。

系统 API 调用策略判定
策略静态要求运行要求阻断条件
directintroducedApi 不高于 minSdk边界设备真实调用API 级别越界
runtime_lookup无越界直接引用dlopen/dlsym 判空和回退缺守卫或 namespace 不明
weak_checked绑定与工具链策略明确调用前检查并执行缺失分支只标 weak 不判空
project_dependency提供者同 ABI 打包装载顺序和真实入口提供者缺失
optional_feature能力目录和所有者明确有与无两种设备状态失败后继续误用
forbidden不应出现在最终 SO无需运行放行任何实际引用

ABI 与 namespace 是独立于 API level 的边界

同一系统函数的首次可用 API 满足 minSdk,不等于所有 ABI 产物一致。编译条件、预处理宏、汇编、第三方库和链接输入可能让某个 ABI 引用额外符号。扫描对每个最终 SO 独立执行,并比较同源模块在不同 ABI 的目录差异。差异需要解释,但不能用一个 ABI 的成功覆盖另一个 ABI。

linker namespace 进一步限制库与符号是否可达。NDK stable APIs 说明的稳定 API 是使用基础,但应用自定义 dlopen 路径、非公开库或厂商库仍可能受 namespace 与设备环境影响。系统符号目录不应把“设备上看见某个文件”当成稳定依赖。动态解析路径的 namespace 行为进入相邻专项,本页只要求记录实际库名、符号和回退。

ABI 产物完整性也要先确认。APK 或 split 中缺少目标 ABI、第三方 SO 只有部分架构、Machine 与目录不一致时,符号扫描结果没有资格进入兼容判定。完整 ABI 交付矩阵另行维护;当前文章只在已确认的最终产物上检查系统符号,并将缺包状态明确返回给产物门禁。

API、ABI 与 namespace 的责任分工
边界主要问题静态证据运行证据
API level系统何时提供符号introducedApi 与 minSdk边界 API 设备
ABI调用约定和实际产物Machine、路径和符号表对应 ABI 设备
namespace库和符号是否可达加载目标与依赖真实进程 dlopen/dlsym
工具链弱引用和代码生成行为NDK 与链接参数有符号与无符号分支
打包SO 是否进入设备集合APK/split 文件清单设备安装现场
业务缺失时是否安全回退guard 与 fallback 标识真实用户入口

用 readelf 和 API 目录阻断越界系统符号

自动门禁接收 readelf、最终 SO 和审核过的 API 目录。目录包含 minSdk、Machine、ABI 以及每个允许外部符号的 source、introducedApi、usage、abis、guardEvidenceId 和 fallbackId。脚本读取宽格式动态符号表,提取 UND 条目并保留绑定属性,去除显示用途的版本后缀后查目录;未知符号、Machine 不符、ABI 不符和越界直接调用都失败。

下面 Python 示例只读 ELF 和 JSON,不修改文件、不加载 SO、不包含私有符号。它要求三个显式输入,运行 readelf header 与 dynamic symbols,解析 Machine 和 UND 行,按目录筛选 source=system 的条目,并检查 direct、runtime_lookup、weak_checked 三种策略。动态或弱路径若没有守卫与回退标识,同样阻断。通过时输出 Machine、ABI、minSdk 和已核对系统符号。

目录必须由项目从 NDK API 信息、供应链和源码审阅中产生,不能让模型或扫描器猜 introducedApi。readelf 输出格式变化要用固定版本和测试捕获,原始 stdout 与 JSON 摘要同时归档。项目内部依赖可以在另一目录核对,但不可简单忽略;示例只输出系统条目,是为了保持当前搜索意图集中。

  • readelf、最终 SO 和 API 目录使用显式路径
  • Machine、ABI 与发布变体目录一致
  • introducedApi 来自审核过的权威目录
  • direct 调用不得越过 minSdk
  • 动态与弱引用同时记录守卫和回退
  • 原始 readelf 输出与门禁 JSON 一起归档
扫描未定义系统符号并核对 minSdk 与调用策略
from pathlib import Path
import json
import re
import subprocess
import sys

if len(sys.argv) != 4:
    raise SystemExit("usage: check_native_apis.py readelf lib.so api-catalog.json")

readelf_bin = Path(sys.argv[1])
so_file = Path(sys.argv[2])
catalog_file = Path(sys.argv[3])
for required in (readelf_bin, so_file, catalog_file):
    if not required.is_file():
        raise SystemExit(f"missing input file: {required}")

catalog = json.loads(catalog_file.read_text(encoding="utf-8"))
result = subprocess.run(
    [str(readelf_bin), "-W", "-h", "--dyn-syms", str(so_file)],
    text=True,
    capture_output=True,
    check=False,
)
if result.returncode != 0:
    raise SystemExit(f"readelf failed: {result.stderr.strip()}")

machine_match = re.search(r"^\s*Machine:\s*(.+?)\s*$", result.stdout, re.MULTILINE)
if not machine_match:
    raise SystemExit("ELF Machine field was not found")
machine = machine_match.group(1)

if catalog.get("machine") != machine:
    raise SystemExit("catalog Machine does not match the ELF")
min_sdk = catalog.get("minSdk")
abi = catalog.get("abi")
if not isinstance(min_sdk, int) or not isinstance(abi, str):
    raise SystemExit("catalog minSdk and abi are required")

undefined = set()
for line in result.stdout.splitlines():
    if not re.search(r"\bUND\b", line):
        continue
    fields = line.split()
    if fields:
        undefined.add(fields[-1].split("@", 1)[0])

symbols = catalog.get("symbols", {})
failures = []
checked = []
for name in sorted(undefined):
    item = symbols.get(name)
    if not isinstance(item, dict):
        failures.append(f"{name}: symbol is absent from the approved catalog")
        continue
    if item.get("source") != "system":
        continue
    if abi not in item.get("abis", []):
        failures.append(f"{name}: symbol is not approved for abi={abi}")
        continue
    introduced = item.get("introducedApi")
    usage = item.get("usage")
    if not isinstance(introduced, int):
        failures.append(f"{name}: introducedApi is missing")
    elif usage == "direct" and introduced > min_sdk:
        failures.append(f"{name}: direct call exceeds minSdk={min_sdk}")
    elif usage in {"runtime_lookup", "weak_checked"}:
        if not item.get("guardEvidenceId") or not item.get("fallbackId"):
            failures.append(f"{name}: guarded use lacks guard or fallback evidence")
    elif usage != "direct":
        failures.append(f"{name}: unsupported usage policy")
    checked.append(name)

if failures:
    for failure in failures:
        print(failure, file=sys.stderr)
    raise SystemExit(2)

print(json.dumps({
    "machine": machine,
    "abi": abi,
    "minSdk": min_sdk,
    "checkedSystemSymbols": checked,
}, indent=2))

CTS 合规与应用兼容证据不能互换

Android CTS overview 说明 CTS 用于验证设备实现与 Android 兼容性定义的一致性。设备通过 CTS 有助于建立平台基础,但不能证明某个第三方应用、特定 NDK 版本、预编译库、加固变换、JNI 签名和自定义动态加载路径兼容。发布报告可以记录设备认证状态,但不能用 CTS 标签替代应用级证据。

Android instrumented tests 运行在设备或模拟器上,适合从真实 Java 或 Kotlin 入口触发 Native 加载和调用。API 边界测试至少覆盖 minSdk、每个高版本可选符号引入前后的 API、对应 ABI 和缺失回退。对于 runtime_lookup 或 weak_checked,分别验证符号存在与不存在时的路径;只在高版本成功调用不能证明低版本回退。

测试结果绑定 APK 与 SO 摘要、设备、API、ABI、系统构建、WebView 等无关组件无需混入。记录加载返回、linker 日志、JNI 注册、业务输入输出、错误和进程存活。单一设备通过不能代表完整矩阵;模拟器和云真机有各自证据等级,未执行项保持未测试。

系统符号可用性的最小运行矩阵
场景目标条件关键断言不能外推
minSdk 设备最低 API 与目标 ABISO 装载和核心调用高版本可选能力
引入前 API可选符号尚不可用守卫和回退执行其他厂商实现
引入 API首次可用系统版本查询与调用成功所有后续版本
高版本 API主流与最新目标正常高能力路径低版本回退
每个 ABI对应最终 split调用约定和结果一致未打包 ABI
异常输入真实错误参数返回与异常契约保持全部业务输入

按最早符号差异修复并限定放行结论

静态门禁出现未知符号时,先确定它来自系统、C++ 运行库、项目依赖还是第三方库,再回到对象文件、链接 map 和变更记录。introducedApi 越界时,不通过提高 minSdk、删除扫描记录或把 usage 改成 runtime_lookup 文字标签掩盖;需要真实代码守卫、回退和边界测试。每次只改变一个可解释变量。

设备装载失败时比较静态与现场:候选摘要是否一致、目标 ABI 是否安装、Machine 是否正确、系统 API 是否达到边界、namespace 是否可达、提供者库是否存在、弱指针是否判空。若通过排除整个核心 SO 的加固使问题消失,继续缩小到符号、方法或链接参数,评估保护损失,不能把全库排除直接当作最终修复。

放行结论应限定为指定摘要候选的列出系统符号满足当前 minSdk 与调用策略,并在列出的 API、ABI 和业务入口完成装载与回退测试。不能写成所有系统符号可用、全部设备兼容或加固不会影响 Native。准备评估时,可整理最终 APK 与 SO、readelf 输出、API 目录、minSdk、守卫与回退清单和设备回执,再通过御盾中央平台提交申请。

  • 未知符号先确定真实提供者和对象来源
  • API 越界通过代码守卫和回退修复
  • 每次只改变一个编译、链接或保护变量
  • 运行失败核对摘要、ABI、API 与 namespace
  • 大范围保护排除继续缩小到具体边界
  • 新候选重新生成静态目录和设备回执

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
高于 minSdk 的 Native API 不能被直接静态调用,版本化使用需要运行时处理。Android NDK stable APIs 描述稳定 API、可用级别与 dlopen、dlsym 使用边界。API 级别满足不证明目标 linker namespace 和厂商设备一定可达。
Native 常见兼容问题包括 API level、缺失符号、STL、异常类型和依赖装载。Android NDK common problems 汇总相关问题和诊断方向。问题清单不能替代目标 ABI 的真实启动、调用和异常回归。
每个 Android ABI 具有独立调用约定、寄存器、对齐和设备支持范围。Android ABIs 描述各 ABI 的体系结构约定和支持边界。声明支持 ABI 不证明对应 SO 已构建、打包或在设备运行。
readelf 可以检查 ELF header、dynamic section、符号与重定位信息。GNU readelf 描述对应选项和输出。静态 UND 和符号属性存在不能证明动态装载、调用和异常传播成功。
CTS 用于验证设备实现与 Android 兼容性定义的一致性。Android CTS overview 描述 CTS 的定位与执行范围。设备 CTS 状态不代表第三方 App 的加固 SO 和业务路径兼容通过。
依赖真实 Android linker、JNI 和系统 API 的行为适合设备端测试。Android instrumented tests 描述在设备或模拟器上访问 Android 框架的测试。单一设备通过不能代表全部 API、ABI、厂商和系统构建。
每个未定义系统符号应记录 introducedApi、调用策略、ABI、守卫和回退。工程判断:只有这些字段才能区分直接越界引用与可验证的版本化能力。目录内容必须来自项目审阅和权威 API 信息,不能由名称或模型回答猜测。
最终 SO、API 目录、静态输出和设备回执必须绑定同一候选身份。工程判断:只有同候选证据才能排除重构建、strip、重签和 ABI 替换造成的漂移。证据绑定完整仍不能外推未执行设备或证明全部 Native 行为正确。

工程常见问题

SO 在构建机链接成功,为什么低版本仍可能加载失败?

构建使用指定 sysroot 和库,设备使用实际系统镜像与 namespace。最终 SO 可能直接引用 minSdk 之后才提供的符号。

怎样判断一个 UND 符号是否有问题?

先确定提供者,再核对首次可用 API、ABI、绑定、调用策略和运行回执。UND 只是外部解析候选,不等于自动失败。

只检查系统版本再调用高 API 函数是否足够?

不够。动态路径还要确认 dlopen、dlsym 结果与 namespace,并对缺失符号执行明确回退。弱引用也必须在调用前判空。

一个 ABI 的符号扫描能否代表其他 ABI?

不能。不同 ABI 的调用约定、编译分支、预编译依赖和最终符号集合可能不同,应逐个最终产物扫描和测试。

设备通过 CTS 是否表示加固 SO 一定兼容?

不表示。CTS 验证设备兼容性要求,第三方 App 的 SO、JNI、依赖、守卫和业务路径仍需独立设备回归。

申请 Native API 符号边界评估要准备什么?

准备最终 APK 与各 ABI SO、readelf 输出、minSdk、审核过的 API 目录、守卫和回退实现、目标设备回执,再通过御盾中央平台提交申请。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: SO 加固与 Native 兼容性检查清单