先看结论与判断条件
- 重定位类型与目标机器、ABI、编译模型和链接方式相关,允许列表必须按 Machine 和发布变体维护,不能跨架构复制名称集合。
- 使用同一版本 readelf 归档 ELF header、program header、dynamic section、symbol 与 relocation 输出,并绑定最终 SO 与 APK 摘要。
- 未知重定位、TEXTREL、未解析符号、越过 minSdk 的直接 API 引用和装载段对齐异常分别阻断,不能用一个总分掩盖具体原因。
- -z relro 与 -z now 属于链接配置和装载策略证据,不代表业务算法受保护,也不能替代目标设备的 dlopen、JNI 和异常路径回归。
- 16 KB 兼容同时涉及 ELF LOAD 对齐、APK 打包、预编译依赖和真实设备,单看 zipalign 或单一主库不足以放行。
- 静态门禁用于发现候选差异,最终结论必须包含目标 API、ABI、linker namespace、装载入口和关键 Native 调用的运行证据。
重定位检查从候选身份和机器类型开始
ELF 重定位记录描述装载器或链接过程需要根据最终地址修正的位置和符号关系。不同机器使用不同的 relocation 编号与名称,同一源码在 arm64-v8a、armeabi-v7a、x86_64 等目标上会产生不同集合。发布门禁不能维护一个脱离 Machine 的全局“危险名称表”,而应先读取 ELF header 的 Class、Data、Machine、Type 和 ABI,再选择对应策略。
候选身份至少包含最终 SO 的 SHA-256、所在 APK 或 split 摘要、ABI、build variant、minSdk、NDK 与 linker 版本、编译和链接参数、Build ID 及保护配置摘要。加固前基线与加固后候选必须来自同一业务版本和同等构建条件。若测试人员重新 strip、重签、复制或替换 SO,静态输出已经不再代表原发布候选。
GNU readelf 可以检查 ELF header、program header、section、dynamic section、符号、重定位和 unwind 信息。它适合作为只读取证工具,但静态字段存在不能证明 Android 动态链接器在目标设备能完成装载,也不能证明 JNI 注册、构造函数、异常传播和业务调用正确。门禁把 readelf 输出视为第一层证据,运行测试是独立层。
| 字段 | 作用 | 来源 | 不一致时动作 |
|---|---|---|---|
| soSha256 | 固定被检查文件 | 最终 APK 中提取 | 阻断并重新取证 |
| apkSha256 | 连接安装候选 | 最终签名 APK | 禁止沿用旧回执 |
| Machine 与 Class | 选择架构策略 | readelf header | 未知机器阻断 |
| minSdk | 判断系统 API 下限 | 发布变体配置 | 缺失不得推断 |
| NDK 与 linker | 解释输出变化 | 构建证明和命令 | 版本漂移重审 |
| Build ID | 连接符号和崩溃证据 | ELF note | 候选不匹配拒绝 |
允许列表按 ABI、工具链和来源收敛
允许列表不是把基线中出现的所有 R_* 名称一次复制。每个条目记录 Machine、重定位名称、产生它的目标文件或依赖、符号类型、是否动态、链接模型、首次引入变更和负责人。基线只能提供候选;最终允许需要解释为什么发布路径需要该类型,以及目标 linker 与 ABI 是否支持。无法解释的新增项保持阻断。
统计数量同样重要,但不能设一个跨项目通用阈值。某次依赖升级、LTO、编译选项或保护处理可能改变 relocation 类型和数量。门禁比较同一业务版本的基线与候选,输出新增、删除和数量变化,再由工程师回到链接 map、对象文件和依赖来源定位。数量变化是调查信号,不等于风险等级或攻击结论。
绝对、相对、跳转槽、复制或 TLS 相关重定位的名称与语义受 ABI 定义约束。本文不发布一张通用白名单,也不把某一类型永久标红。工程策略是每个 Machine 只允许已经通过来源解释和设备回归的集合,其他类型失败关闭;工具链升级后重新生成基线,保留审批差异,而不是静默扩充允许列表。
| 字段 | 回答的问题 | 通过证据 | 禁止做法 |
|---|---|---|---|
| machine | 规则属于哪个架构 | ELF header 精确值 | 跨 ABI 复用 |
| relocationType | 允许哪个 R_* 类型 | readelf 与 ABI 解释 | 只凭名称判断 |
| producer | 哪个对象或库产生 | link map 与依赖来源 | 来源未知放行 |
| symbolClass | 作用于何种符号 | 符号表和 relocation 行 | 忽略未定义符号 |
| changeId | 为何本次新增 | 代码或工具链变更 | 自动接受基线差异 |
| runtimeReceipt | 在哪些设备验证 | 同候选装载和调用 | 用静态 PASS 替代 |
动态段和程序头用于发现发布阻断项
dynamic section 提供运行时链接所需信息,包括依赖、字符串表、符号表、重定位表位置和动态标志。program header 描述装载段和其他运行时区间。检查顺序是先确认结构可解析,再查动态标签与重定位,最后连接符号和依赖。结构损坏、表偏移异常或 readelf 无法完整解析时直接阻断,不尝试从部分输出推导安全。
TEXTREL 表示运行时可能需要处理文本段重定位,是 Android Native 发布需要重点审查的信号。门禁可以使用链接器策略在构建阶段拒绝产生相应动态标记,并在最终文件再次确认。具体 Android 版本行为和错误以 NDK 文档与设备日志为准;不要通过修改系统策略或忽略警告让候选继续发布。
GNU ld options 描述 -z relro 生成 PT_GNU_RELRO,-z now 要求在程序启动或共享库装载时完成符号解析。发布检查可以记录 GNU_RELRO 程序头和 NOW 相关标志是否符合项目政策,但这不等于业务保护,也不是本页的单独防护结论。更完整的边界参照同站 [RELRO 与 BIND_NOW 防护边界](/zh-cn/articles/relro-bind-now-protection-boundary/);当前门禁只处理候选一致性和装载风险。
| 检查项 | 读取位置 | 失败含义 | 下一步 |
|---|---|---|---|
| ELF 可解析 | header 与各表 | 候选结构或工具不匹配 | 停止发布并回构建链 |
| TEXTREL | dynamic tag | 存在文本重定位信号 | 定位对象和链接选项 |
| NOW policy | dynamic flags | 解析策略与项目期望不同 | 核对 linker 命令 |
| GNU_RELRO | program header | RELRO 策略未体现 | 核对链接产物 |
| relocation tables | dynamic 与 section | 表缺失或新增 | 连接 Machine 允许列表 |
| unresolved symbols | symbol 与 relocation | 装载或调用可能失败 | 核对 API 和依赖 |
系统 API 与缺失符号必须按 minSdk 判断
Android NDK stable APIs 说明,高于应用 minSdk 的 Native API 不能直接静态调用;需要版本化使用时,可在确认系统版本和符号可用后通过 dlopen 与 dlsym 处理。若最终 SO 直接留下对高 API 符号的未定义引用,低版本设备可能在装载或调用前失败。门禁应连接符号名、首次可用 API、minSdk 和实际调用路径,而不是看到 libandroid 或 libc 依赖就统一放行。
动态解析也不是自动安全。dlopen 的库名、linker namespace、系统镜像和厂商环境会影响可达性,dlsym 返回值必须被检查,缺失时走明确回退。静态检查只能发现字符串和未定义符号的部分线索,无法证明分支顺序和 namespace。需要在低于、等于和高于目标 API 边界的代表设备执行相同业务入口,记录实际错误。
Android NDK common problems 汇总 API level、缺失符号、STL、异常类型和依赖装载等常见故障。重定位门禁应把 readelf 发现映射到这些故障类别:未知未定义符号查 API 与依赖,C++ 运行时符号查 STL 一致性,异常跨库查类型与构建参数,装载次序查 DT_NEEDED 与显式加载。问题清单帮助路由,不能替设备回归。
| 问题 | 静态证据 | 运行证据 | 放行边界 |
|---|---|---|---|
| 系统 API 高于 minSdk | 符号与 API 目录 | 低 API 装载和调用 | 有版本守卫与回退 |
| 依赖库缺失 | DT_NEEDED 与 APK 文件 | 目标 namespace 装载 | 同候选依赖完整 |
| C++ 运行时不一致 | STL 依赖和符号 | 构造、异常和析构 | 实际库组合通过 |
| JNI 入口缺失 | 导出或注册清单 | 真实 Java 调用 | 签名和线程正确 |
| 弱符号或可选能力 | 绑定属性与引用 | 能力存在和不存在两路 | 失败回退可执行 |
| 厂商或私有符号 | 非稳定来源 | 代表设备现场 | 无稳定依据默认阻断 |
页大小检查是重定位发布门禁的相邻条件
Support 16 KB page sizes 说明兼容性需要同时检查 ELF LOAD 段对齐、APK 对齐、预编译库和目标设备运行。重定位表本身没有异常,不代表装载段满足目标页大小;反过来,zipalign 通过也不能证明 ELF 内部 LOAD 对齐、所有 ABI 依赖和运行路径正确。发布检查把这些证据并列,不互相代替。
每个最终 APK 中的 Native 库都要检查,包括主库、第三方预编译库和动态 feature 携带的 SO。记录每个文件的 ABI、LOAD p_align、来源、是否重建、APK 内存储方式和打包对齐。若只有部分依赖满足策略,整个目标设备集合仍不能放行。更完整的矩阵与工具链配置应进入 16 KB 专项,本页只要求重定位候选同时携带其对齐回执。
16 KB 设备运行至少覆盖应用启动、实际 dlopen 或 System.loadLibrary 入口、JNI 注册、关键业务调用和异常路径。若项目使用按需模块或不同 ABI split,测试对象必须是该设备实际获得的集合。静态 readelf 输出和 APK 对齐报告绑定同一摘要,不能拿未加固基线的对齐证明加固候选。
| 证据 | 对象 | 回答的问题 | 不能证明 |
|---|---|---|---|
| ELF LOAD 对齐 | 每个最终 SO | 装载段对齐字段 | APK 打包正确 |
| APK 对齐 | 最终签名候选 | 库在包内的对齐 | ELF 内部正确 |
| 预编译库清单 | 第三方和闭源 SO | 是否全部纳入检查 | 运行一定兼容 |
| ABI split 清单 | 设备派生集合 | 实际交付哪些库 | 其他 ABI 已通过 |
| 16 KB 设备加载 | 安装候选 | linker 接受目标库 | 关键业务调用正确 |
| 业务回归 | 真实 JNI 入口 | 输入输出和异常保持 | 未测试设备兼容 |
用 readelf 输出和策略文件生成可审阅允许列表
自动门禁接收 readelf 可执行文件、最终 SO 和 JSON 策略。策略按 readelf 的 Machine 字符串保存 allowedRelocations、forbiddenDynamicTags、requiredDynamicTags 和 requiredProgramHeaders。脚本运行同一条 readelf 命令读取 header、program header、dynamic section、symbol 与 relocation,解析实际集合后输出允许、观察、未知和缺失项。策略缺少当前 Machine 时失败关闭。
下面 Python 示例只执行 readelf 的只读检查,不修改 ELF、不生成加载器、不包含绕过代码或私有符号。它验证三个输入文件,检查 readelf 返回值,提取 Machine、R_* 类型、动态标签和关键程序头标记,再按策略拒绝未知 relocation、禁止标签、缺少动态标签或缺少程序头。通过时输出 JSON 证据,包含允许列表和本次观察集合。
正则解析适合稳定门禁前提,但仍要固定 binutils 版本并为工具输出变化建立测试。真实项目可以改为结构化 ELF 库或同时保留原始输出,避免只留下摘要。策略文件必须经过代码审阅,不能让脚本在发现新类型时自动写回 allowedRelocations;新增项需要来源、变更、ABI 解释和设备回执后才能单独更新。
- readelf、SO 和策略文件都使用显式路径
- 策略以精确 Machine 字符串选择
- 新增 relocation 不自动写回允许列表
- TEXTREL 等禁止标签由项目策略显式声明
- 原始 readelf 输出与 JSON 摘要同时归档
- 策略更新绑定来源解释和设备回执
from pathlib import Path
import json
import re
import subprocess
import sys
if len(sys.argv) != 4:
raise SystemExit("usage: check_relocations.py readelf lib.so policy.json")
readelf_bin = Path(sys.argv[1])
so_file = Path(sys.argv[2])
policy_file = Path(sys.argv[3])
for required in (readelf_bin, so_file, policy_file):
if not required.is_file():
raise SystemExit(f"missing input file: {required}")
policy = json.loads(policy_file.read_text(encoding="utf-8"))
result = subprocess.run(
[str(readelf_bin), "-W", "-h", "-l", "-d", "-r", "-s", str(so_file)],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise SystemExit(f"readelf failed: {result.stderr.strip()}")
output = result.stdout
machine_match = re.search(r"^\s*Machine:\s*(.+?)\s*$", output, re.MULTILINE)
if not machine_match:
raise SystemExit("ELF Machine field was not found")
machine = machine_match.group(1)
machine_policy = policy.get("machinePolicies", {}).get(machine)
if not isinstance(machine_policy, dict):
raise SystemExit(f"no relocation policy for Machine={machine}")
observed_relocations = set(re.findall(r"\bR_[A-Z0-9_]+\b", output))
dynamic_tags = set(re.findall(r"\(([A-Z0-9_]+)\)", output))
program_headers = {name for name in ("GNU_RELRO", "GNU_STACK") if name in output}
allowed = set(machine_policy.get("allowedRelocations", []))
forbidden_tags = set(machine_policy.get("forbiddenDynamicTags", []))
required_tags = set(machine_policy.get("requiredDynamicTags", []))
required_headers = set(machine_policy.get("requiredProgramHeaders", []))
unexpected = sorted(observed_relocations - allowed)
forbidden_seen = sorted(dynamic_tags & forbidden_tags)
missing_tags = sorted(required_tags - dynamic_tags)
missing_headers = sorted(required_headers - program_headers)
failures = []
if unexpected:
failures.append(f"unexpected relocations: {unexpected}")
if forbidden_seen:
failures.append(f"forbidden dynamic tags: {forbidden_seen}")
if missing_tags:
failures.append(f"required dynamic tags missing: {missing_tags}")
if missing_headers:
failures.append(f"required program headers missing: {missing_headers}")
if failures:
for failure in failures:
print(failure, file=sys.stderr)
raise SystemExit(2)
print(json.dumps({
"machine": machine,
"allowedRelocations": sorted(allowed),
"observedRelocations": sorted(observed_relocations),
"dynamicTags": sorted(dynamic_tags),
"programHeaders": sorted(program_headers),
}, indent=2))静态失败按最早差异回到构建链
出现未知 relocation 时,先确认 Machine 与工具版本,再从 relocation 行连接符号、section、目标对象和 link map。若变化来自新增源码、编译模型、LTO、PIC 配置、保护处理或第三方库升级,分别复现单一变量。不要先把新名称加入允许列表,也不要通过 strip 掉证据后重新扫描;strip 可能改变可见信息,但不会成为运行正确性的证明。
出现 TEXTREL、缺失符号或动态策略差异时回到 linker 命令与对象来源。依赖图属于相邻专项,可用同站的 DT_NEEDED 文章建立库级关系;本页只把依赖身份作为重定位解释输入。若通过排除整个核心 SO 的保护使问题消失,应继续缩小到具体方法、section 或链接参数,并评估保护损失,不把大范围排除当成最终根因。
一次修复产生一个新候选和新证据链。新 SO 摘要、APK 摘要、readelf 原始输出、差异说明和设备矩阵全部重新绑定。静态门禁通过后若设备仍无法加载,以设备 linker 错误和最早失败入口为准;不要反过来把静态 PASS 写成 linker 故障不存在。
- 先确认 Machine、工具版本和候选摘要
- 连接 relocation、符号、section、对象与 link map
- 每次只调整一个编译或链接变量
- 禁止用 strip 或自动扩表掩盖未知项
- 大范围保护排除必须继续缩小根因
- 每个新候选重新生成完整静态和运行证据
目标设备装载与业务调用决定最终放行
Android instrumented tests 运行在设备或模拟器上,适合验证依赖 Android linker、JNI、组件和系统 API 的行为。Native 回归应从应用真实入口触发 System.loadLibrary 或项目加载路径,确认构造阶段、JNI 注册、关键函数输入输出、错误返回、异常跨库和卸载或进程恢复。单独执行 dlopen 成功只能证明部分装载路径,不能代表业务接口。
矩阵至少覆盖实际发布 ABI、minSdk 边界、主流 API、最新目标、需要的 16 KB 设备和关键厂商环境。每次记录 APK 与 SO 摘要、设备、API、ABI、页大小、已加载库、linker 日志和业务结果。单一设备或模拟器通过不能代表完整矩阵,静态允许列表也不能代替未执行组合。
放行结论应限定为指定摘要候选在列出的 Machine 策略中没有未知 relocation 或禁止动态标签,并在列出的设备与入口完成装载和业务回归。不能写成所有重定位安全、所有设备兼容或算法得到保护。准备 SO 发布评估时,可整理最终 APK 与 SO、readelf 输出、链接参数、允许列表、minSdk、依赖和设备矩阵,再通过御盾中央平台提交申请。
| 场景 | 关键输入 | 运行断言 | 结论边界 |
|---|---|---|---|
| 最低 API | 目标 ABI 与最终 APK | 装载及关键调用成功 | 只覆盖该设备 |
| API 边界 | 可选系统符号存在或缺失 | 版本守卫和回退正确 | namespace 另行记录 |
| 16 KB 设备 | 全部最终 SO 与打包 | 装载、JNI 和业务路径 | 不替代其他 ABI |
| 异常路径 | 真实错误输入 | 异常类型和返回保持 | 不代表所有输入 |
| 进程恢复 | 加载后杀进程重进 | 构造和注册可重复 | 只覆盖列出入口 |
| 加固对比 | 同业务版本基线与候选 | 最早差异可解释 | 不产生通用性能结论 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| readelf 可以检查 ELF header、program header、dynamic section、符号、重定位和 unwind 信息。 | GNU readelf 描述对应选项和输出范围。 | 静态字段可解析不能证明 Android 目标设备的动态装载和异常传播成功。 |
| -z relro 可生成 PT_GNU_RELRO,-z now 要求装载时完成符号解析。 | GNU ld options 描述相关链接器选项。 | 链接标志不保护业务算法,也不能替代目标设备的装载与调用回归。 |
| 高于 minSdk 的 Native API 不能直接静态调用,版本化使用需要运行时检查。 | Android NDK stable APIs 描述 API 可用性与 dlopen、dlsym 处理边界。 | API 版本满足不证明目标 linker namespace 中一定可达。 |
| Native 兼容故障常涉及 API level、缺失符号、STL、异常类型和依赖装载。 | Android NDK common problems 汇总相关问题和诊断方向。 | 问题清单只能用于路由,不能替代目标 ABI 设备的真实回归。 |
| 16 KB 兼容需要同时核对 ELF LOAD 对齐、APK 对齐、预编译库和运行结果。 | Support 16 KB page sizes 描述构建、打包和测试要求。 | 只检查 zipalign、单一 SO 或单一 ABI 都不能得出完整兼容结论。 |
| 依赖 Android linker、JNI 和系统 API 的语义适合设备端测试。 | Android instrumented tests 描述在设备或模拟器上访问 Android 框架的测试。 | 单一设备通过不能代表全部 API、ABI、页大小和厂商环境。 |
| 重定位允许列表必须按 Machine、工具链、来源和发布变体维护。 | 工程判断:不同 ABI 和链接模型产生不同 relocation 集合,名称脱离上下文无法判定。 | 每个项目仍需用真实基线、变更来源和设备回执审阅具体条目。 |
| SO、APK、readelf 输出、策略和运行回执必须绑定同一候选身份。 | 工程判断:只有同候选证据才能排除重构建、strip、重签和文件替换导致的漂移。 | 证据绑定完整仍不能外推未执行设备或证明业务算法受到保护。 |
工程常见问题
某个 R_* relocation 是否可以直接判为危险?
不能脱离 Machine、ABI、链接模型、符号和来源判断。应按架构维护允许列表,并对新增项连接对象文件与设备回执。
readelf 检查通过能否证明 SO 一定能加载?
不能。它提供静态结构证据,还要在目标 API、ABI、页大小和 linker namespace 中执行真实加载、JNI 与业务调用。
看到 TEXTREL 应怎样处理?
将其作为发布阻断信号,回到对象来源、编译模型和链接参数定位,不通过忽略警告或自动加入允许列表放行。
RELRO 与 BIND_NOW 是否等于 SO 业务代码已保护?
不等于。它们是链接和装载策略,不能隐藏业务算法、保护运行时明文或替代 VMP 与服务端控制。
16 KB 对齐为什么要放进重定位发布检查?
两者都影响最终 SO 装载,但证据不同。应并列检查 ELF LOAD、APK 对齐、全部依赖和设备运行,不能互相替代。
申请 SO 重定位发布评估要准备什么?
准备最终 APK 与 SO、各 ABI readelf 原始输出、链接参数、Machine 允许列表、minSdk、依赖清单和设备回执,再通过御盾中央平台提交申请。