先看结论与判断条件
- 文件名、DT_SONAME、DT_NEEDED 和显式 dlopen 名称是四类不同事实,不能用其中一个字段代替完整装载身份。
- 同一 ABI 中多个不同文件声明相同 SONAME 会制造装载歧义,文件改名但 SONAME 与依赖引用不变也可能让替换结果偏离预期。
- Android 动态链接器按调用者 namespace、可见库和搜索路径解析依赖,静态清单必须保留调用者与进程上下文。
- CMake 导入名、输出名和预编译库文件需要与最终 ELF 动态段交叉核对,构建配置只说明意图,不代表最终产物。
- readelf 可以取得 SONAME、NEEDED、header、program header 和符号信息,但静态字段通过不能证明目标设备已成功装载。
- 发布门禁应按 ABI 绑定最终 APK 摘要、每个 SO 摘要、名称决策和设备加载回执,禁止用源码目录或另一渠道库替代。
先分清文件名、SONAME 和依赖名
APK 中 `lib/<abi>/libsample.so` 的 basename 是打包文件名,ELF dynamic section 的 `DT_SONAME` 是该共享对象声明的逻辑身份,另一个 ELF 的 `DT_NEEDED` 则保存它在链接时记录的依赖名称。三者经常相同,但这只是良好构建结果,不是概念上同一字段。简单重命名文件不会自动改写 SONAME,也不会修改所有调用方的 NEEDED。
运行时还有第四个名称来源:应用或第三方库传给加载 API 的字符串。显式路径、basename、逻辑库名和已经加载对象的身份可能走不同解析路径。诊断必须把加载调用者、进程、namespace、请求字符串、候选文件和最终 ELF 动态段放在一张表中。只看到目录里存在同名 so,不能证明调用方会选中它,也不能证明它的依赖会继续解析到同一组对象。
本文只回答 SO 文件名、SONAME 与 DT_NEEDED 不一致为何造成装载歧义,以及发布前怎样检查,不重复完整依赖闭包审计。静态一致性不能证明初始化函数、符号解析和异常传播成功;设备加载成功也不能证明业务算法或保护强度。没有最终 APK、目标 ABI 和真实回执时,只能报告名称事实与风险判断。
| 名称来源 | 示例位置 | 由谁产生 | 主要用途 |
|---|---|---|---|
| APK 文件名 | lib 目录 basename | 打包和构建输出 | 定位随包文件 |
| DT_SONAME | ELF dynamic section | 链接器选项或默认输出 | 声明运行身份 |
| DT_NEEDED | 调用方 dynamic section | 链接时依赖解析 | 请求依赖对象 |
| 显式加载名 | dlopen 或上层加载调用 | 应用与运行库代码 | 启动一次装载查找 |
| 已加载对象 | 进程 linker 状态 | 运行时解析结果 | 后续符号绑定 |
| 构建目标名 | CMake 或其他构建配置 | 工程配置 | 描述构建意图 |
不一致会怎样制造错误命中
最常见错误是把 `libold.so` 复制并改名为 `libnew.so`,却保留内部 SONAME 为 `libold.so`。若调用方的 DT_NEEDED 仍请求 `libold.so`,改名文件不一定成为它期待的替代;若同时打包旧文件和新文件,两个不同字节又可能声明相同身份。加载顺序、namespace 可见性和已加载对象状态会共同影响结果,不能把“文件名不同”当作自然隔离。
第二类错误是升级供应商库时只替换一个 so,没有重新链接所有直接调用方。新文件的 SONAME 可能改变,而旧调用方仍把原名称写在 DT_NEEDED 中;或者 SONAME 不变但 ABI 和符号集合已经改变,静态名称看似一致,运行时却在符号解析或初始化阶段失败。名称门禁要与摘要和导出、导入符号差异配合,不能只用字符串相等放行。
第三类错误来自多个 AAR、jniLibs 和构建任务携带重复库。Gradle 最终可能按打包规则选择其中一份,源码树里的每个副本都不能代表交付结果。必须从最终 APK 或真实派生 APK 集提取每个 ABI 的 so,计算摘要并读取 dynamic section。若同一 SONAME 对应不同摘要,先查明所有者和选择规则;不能按目录顺序猜测哪个会被加载。
- 文件改名同时审查 SONAME 和全部依赖引用
- 供应商升级后重新链接并检查直接调用方
- 相同 SONAME 的不同摘要被视为冲突
- AAR、jniLibs 和构建产物重复来源可追溯
- 最终 APK 而非源码目录作为静态事实
- 名称一致后继续检查 ABI 和符号兼容
namespace 决定调用者能看到哪些库
Android 动态链接器按调用者所在 namespace 解析 DT_NEEDED、dlopen、搜索路径和允许库。相同请求字符串从不同调用者或进程发起,候选集合可能不同。AOSP VNDK namespace 资料展示了隔离、链接和允许列表等机制,但普通 App 的实际行为还受系统版本、应用打包、ClassLoader 和加载入口影响,不能把 VNDK 配置直接照搬成 App 结论。
静态清单因此要记录依赖边,而不仅是全局文件集合。每条 DT_NEEDED 应包含调用方路径、调用方 ABI、请求名称和在当前交付集中的候选提供者。显式 dlopen 还要记录调用模块、是否使用路径、请求发生的进程和加载时序。若扫描器只建立“项目里有这个名字”的全局索引,会把另一 ABI、另一 split 或不可见 namespace 中的文件误当成可解析候选。
已加载对象也会影响后续判断。某个库先由第三方 SDK 或系统组件加载,后续请求可能复用已存在对象,而不是按开发者预期再次选择文件。静态工具无法完整模拟每台设备的 linker 状态,只能标出同名、同 SONAME、重复摘要和路径歧义。最终放行还要在目标设备采集实际加载结果和失败日志,并与同一候选绑定。
| 场景 | 静态现象 | 运行风险 | 门禁决定 |
|---|---|---|---|
| 改名未改 SONAME | 文件名与声明不同 | 身份复用或替换失效 | 要求来源说明和设备验证 |
| 两个文件同 SONAME | 不同路径或摘要同身份 | 加载顺序产生歧义 | 默认阻断 |
| NEEDED 无本地提供者 | 依赖名未命中 APK | 依赖系统库或直接失败 | 对照允许系统库清单 |
| 提供者改 SONAME | 旧调用方仍请求旧名 | 依赖无法解析 | 重新链接调用方 |
| 不同 ABI 名称漂移 | 架构间 SONAME 不一致 | 设备表现分裂 | 按 ABI 独立阻断 |
| namespace 不可见 | 全局有文件但调用者不可见 | dlopen 或 NEEDED 失败 | 设备验证调用上下文 |
从最终 APK 读取动态段而不是猜配置
GNU readelf 可以检查 ELF header、program header、dynamic section、符号、重定位和 unwind 信息。名称门禁至少对每个最终 so 执行 dynamic section 检查,提取 SONAME 与全部 NEEDED,同时读取 ELF class、machine 和 program header,用文件摘要绑定结果。工具输出、工具版本和原始文件路径都要保存,避免后续只剩手工抄写的名称。
SONAME 缺失不一定表示文件绝对无法加载,但会让身份和依赖匹配更多依赖文件名与调用方式。项目应定义哪些应用自有和第三方库允许没有 SONAME,并保存供应商或构建理由。扫描器不能自动给缺失项补一个文件名,也不能修改二进制后继续沿用原签名和摘要。任何修复都要回到构建链重新链接、重新打包并生成新候选。
DT_NEEDED 中的系统库不能要求在 APK 内出现,否则会产生大量误报。门禁需要按目标 minSdk、NDK 稳定 API 和平台允许范围维护受控系统库清单;应用自有依赖则必须在对应 ABI 和交付模块中有唯一提供者。第三方预编译库可能携带非公共或高版本依赖,静态名称发现只是第一步,仍需目标系统装载验证。
- 每个最终 so 的 dynamic section 和文件摘要绑定
- ELF machine、class、SONAME 与 NEEDED 同时保存
- SONAME 缺失具有明确允许策略和来源
- 系统库清单按 minSdk 与平台边界维护
- 应用自有 NEEDED 在同 ABI 有唯一提供者
- 修复通过重新链接生成新候选而非修改已签名包
构建系统名称必须与最终 ELF 交叉核对
Android NDK 的 CMake toolchain 用于固定 ABI、平台版本和第三方库导入方式。CMake target 名、OUTPUT_NAME、IMPORTED_LOCATION 和链接依赖会影响最终文件与链接命令,但它们只是构建意图。预编译库内部的 SONAME 可能与导入 target 或文件名不同,IMPORTED_NO_SONAME 等配置也会改变调用方记录什么名称。发布门禁必须查看最终链接输出和 ELF 动态段。
非标准构建系统同样需要显式固定 NDK toolchain、API level、ABI 和编译目标。手写 clang 命令、ndk-build、Rust 或供应商脚本可能使用不同 SONAME 选项和复制规则。团队应生成统一的最终 ELF 清单,而不是为每种构建系统编写一套不可比较的报告。配置正确也不证明第三方预编译库内部选项与目标运行时兼容。
构建审查还要保存链接命令和版本替换责任。若业务需要保持稳定 SONAME,升级时应确保所有直接调用方重新链接并验证符号契约;若计划改变 SONAME,则要同步更新文件名、加载调用和交付清单。不能通过复制两份同一字节、分别改文件名来模拟两个独立版本,因为内部身份和依赖边仍可能相同。
| 构建字段 | 最终证据 | 可能漂移 | 审查动作 |
|---|---|---|---|
| CMake target | 最终文件和链接命令 | 逻辑名与输出名不同 | 保存目标映射 |
| OUTPUT_NAME | APK basename | 复制任务再次改名 | 以最终 APK 为准 |
| SONAME 选项 | DT_SONAME | 预编译库保留旧值 | 逐文件读取动态段 |
| target_link_libraries | 调用方 DT_NEEDED | 链接选择另一副本 | 比较实际依赖名 |
| IMPORTED_LOCATION | 提供者摘要与路径 | ABI 目录串用 | 逐 ABI 校验 machine |
| 平台与 toolchain | ELF ABI 和最低 API 假设 | 自定义脚本未继承 | 记录工具链版本 |
RELRO 与 NOW 是独立的链接硬化维度
GNU ld 的 `-z relro` 会生成 PT_GNU_RELRO,`-z now` 要求装载时完成符号解析。这些标志有助于降低部分重定位面风险,也可能让缺失符号更早暴露,但它们不解决 SONAME、文件名和 DT_NEEDED 不一致。一个同时启用 RELRO 与 NOW 的库仍可能声明错误 SONAME,调用方也可能请求另一个名称。
发布报告应将名称一致性、依赖可解析性、符号绑定策略和段权限分成独立门禁。把 `-z now` 当作名称修复,会掩盖真正的构建关系;把名称一致当作安全加固通过,也会遗漏段权限和延迟绑定风险。每项结论都要引用对应 ELF 字段和目标文件摘要,不能用一条链接器参数覆盖全部二进制属性。
VMP 或 SO 保护流程若重新链接、包装或复制库,需要重新读取 SONAME、NEEDED、program header 和摘要。保护前的 readelf 回执不能代表保护后候选,保护后名称相同也不能证明字节或符号契约未变。本文不宣称任何保护流程自动保持这些字段,实际候选需要独立静态和设备门禁。
- SONAME 一致性与 RELRO、NOW 分开登记
- 链接参数和最终 program header 交叉核对
- NOW 使解析更早不等于依赖名称正确
- 名称一致不替代段权限和符号审查
- 保护后重新读取全部动态段与 program header
- 每条结论绑定实际候选文件摘要
用清单脚本发现名称冲突和未解析依赖
下面的 Python 示例读取由 readelf 提取器生成的脱敏 JSON。每个条目包含 ABI、路径、文件名、SHA-256、SONAME 和 NEEDED,文档还提供允许的系统库集合。脚本按 ABI 建立提供者索引,报告文件名与 SONAME 不一致、SONAME 缺失、同一 SONAME 对应不同摘要,以及应用自有 NEEDED 没有唯一提供者。它不执行 dlopen、不修改 ELF,也不模拟 Android linker。
输入必须来自同一个最终 APK 或明确的设备交付集,不能混入源码输出目录。示例对缺失文件、空清单、字段不完整、路径 basename 与 file_name 不符、重复 SONAME 冲突和无法解析依赖设置可达失败路径。文件名与 SONAME 不同会进入 review 列表而不是一律判死,因为某些受控构建可能有明确理由;同身份不同字节则直接阻断。
生产实现还应加入模块、split、调用进程、显式加载调用、ELF machine、Native 符号和 namespace 设备证据。系统库 allowlist 要按目标平台维护,不能为了让门禁通过不断追加未知名称。脚本输出是静态决策表,真正加载对象、搜索顺序和初始化结果仍需目标设备确认。
from pathlib import Path
import json
import sys
if len(sys.argv) != 2:
raise SystemExit("usage: soname_gate.py elf-inventory.json")
path = Path(sys.argv[1]).resolve()
if not path.is_file():
raise SystemExit(f"input missing: {path.name}")
document = json.loads(path.read_text(encoding="utf-8"))
objects = document.get("objects")
if not isinstance(objects, list) or not objects:
raise SystemExit("objects must be a non-empty list")
system_libraries = set(document.get("system_libraries", []))
providers = {}
reviews = []
for item in objects:
required = ("abi", "path", "file_name", "sha256", "needed")
if any(item.get(key) in (None, "") for key in required):
raise SystemExit("ELF inventory entry is incomplete")
if Path(item["path"]).name != item["file_name"]:
raise SystemExit(f"path and file_name differ: {item['path']}")
soname = str(item.get("soname") or "").strip()
if not soname:
reviews.append({"abi": item["abi"], "file": item["file_name"], "issue": "missing-soname"})
continue
key = (item["abi"], soname)
providers.setdefault(key, []).append(item)
if soname != item["file_name"]:
reviews.append({"abi": item["abi"], "file": item["file_name"], "issue": "file-soname-diff", "soname": soname})
collisions = []
for (abi, soname), matches in providers.items():
digests = {item["sha256"] for item in matches}
if len(digests) > 1:
collisions.append({"abi": abi, "soname": soname, "files": [item["path"] for item in matches]})
if collisions:
raise SystemExit(json.dumps({"soname_collisions": collisions}, ensure_ascii=False))
unresolved = []
for caller in objects:
for needed in caller["needed"]:
if needed in system_libraries:
continue
matches = providers.get((caller["abi"], needed), [])
if len(matches) != 1:
unresolved.append({"caller": caller["path"], "needed": needed, "providers": len(matches)})
if unresolved:
raise SystemExit(json.dumps({"unresolved_needed": unresolved}, ensure_ascii=False))
print(json.dumps({"status": "static-consistent", "reviews": reviews}, ensure_ascii=False, indent=2))在目标 ABI 和真实调用路径完成装载回归
NDK 常见问题包括缺失符号、STL、API level、异常类型和错误依赖装载,名称门禁通过后仍要在目标设备测试。矩阵至少覆盖每个交付 ABI、最低支持系统、主要系统版本、冷启动、延迟功能入口、显式 dlopen 路径和所有使用目标库的进程。只验证 System.loadLibrary 返回不够,还要调用代表性符号并触发初始化、错误和卸载相关路径。
设备回执应记录最终 APK 摘要、so 摘要、ABI、系统、进程、调用者、请求名称、实际加载路径、linker 错误和业务入口。若系统不提供完整加载路径,应保存可合法取得的日志和模块状态,并明确证据限制。单一设备成功不能代表全部 namespace 与厂商实现,另一个 ABI 的成功也不能覆盖当前 ABI。
完整发布材料包括最终 ELF 清单、readelf 原始输出、CMake 或其他 toolchain 配置、链接命令、重复库来源、名称决策、系统库清单和设备回归。需要评估实际 SO 时,可通过御盾中央平台提交脱敏的 ELF 动态段、文件摘要、ABI、加载调用和最终候选摘要,先固定名称所有权,再处理 namespace 与设备装载。
- 每个交付 ABI 都在真实设备或等价环境验证
- 冷启动、延迟加载和目标进程入口均被触发
- 加载成功后继续调用代表性符号和初始化路径
- 设备回执绑定最终 APK 和每个 so 摘要
- linker 错误与文件名、SONAME、NEEDED 结果关联
- 单一设备结果不会外推到所有 namespace 和厂商
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| readelf 可以检查 ELF header、program header、dynamic section、符号、重定位和 unwind 信息。 | GNU readelf 说明 ELF 文件检查选项和输出范围。 | 静态字段存在不能证明动态加载、符号调用或异常传播成功。 |
| -z relro 生成 PT_GNU_RELRO,-z now 要求装载时完成符号解析。 | GNU ld options 说明 RELRO 与 NOW 等链接器选项。 | 链接标志不能修复 SONAME 不一致,也不保护业务算法或运行时明文。 |
| Android 动态链接器按调用者 namespace 解析 DT_NEEDED、dlopen、搜索路径和允许库。 | AOSP linker namespace 说明动态链接器 namespace 的隔离与链接机制。 | VNDK namespace 文档不能替代普通 App 进程在目标设备上的实测。 |
| NDK 兼容故障常见于 API level、缺失符号、STL、异常类型和错误依赖装载。 | Android NDK common problems 汇总常见 Native 构建与运行问题。 | 故障清单不能替代目标 ABI 的实际启动、符号调用和异常回归。 |
| CMake toolchain 用于固定 ABI、平台版本和第三方库导入方式。 | Android NDK CMake 说明 Android CMake toolchain 和库导入配置。 | CMake 配置不覆盖已预编译二进制的内部编译与 SONAME 选项。 |
| 非标准构建必须显式固定 NDK toolchain、API level、ABI 与编译目标。 | NDK with other build systems 说明非 CMake 构建的工具链和目标配置。 | 构建参数正确不能证明第三方预编译库与目标运行时兼容。 |
| 文件名、SONAME、DT_NEEDED 和显式加载名应作为四类独立事实记录。 | 工程判断:分层记录可以发现改名未重链接、重复身份和加载调用漂移。 | 静态名称关系不能完整模拟 namespace 搜索和已加载对象状态。 |
| 同一 ABI 中一个 SONAME 对应不同摘要应在发布前阻断。 | 工程判断:多个不同字节共享运行身份会让实际命中依赖加载顺序与可见范围。 | 具体装载结果仍需目标进程和设备证据确认,阻断规则是保守工程门禁。 |
工程常见问题
把 libold.so 改名为 libnew.so 会自动改变 SONAME 吗?
不会。文件复制和改名不会自动改写 ELF dynamic section,也不会更新其他库中已有的 DT_NEEDED。
文件名与 SONAME 不同是否一定无法加载?
不一定,但会增加依赖解析和替换的解释成本。需要检查调用方式、DT_NEEDED、namespace、重复库和目标设备结果。
两个不同文件可以声明相同 SONAME 吗?
技术上可能出现,但同一 ABI 与交付范围内会制造身份歧义,应默认阻断并查清构建与所有者。
readelf 检查通过是否代表 SO 一定能运行?
不代表。还要在目标 ABI 和系统验证 namespace 可见性、符号解析、初始化、错误路径和代表性业务调用。
启用 RELRO 和 NOW 能修复名称不一致吗?
不能。它们属于链接硬化和符号解析策略,SONAME、文件名与 DT_NEEDED 关系仍需独立修复。
准备 SO 名称一致性审查需要哪些材料?
准备最终 APK、逐 ABI so 摘要、readelf 动态段、链接命令、CMake 或工具链配置、重复库来源、加载调用和设备回执。