先看结论与判断条件

  • Map 文件必须由实际 Release 链接步骤生成,事后重链得到的 Map 即使源码相同,也可能因工具链、对象顺序、LTO 或依赖解析漂移而失去对应关系。
  • 关键资产要映射到稳定 assetId、目标对象或归档成员、输入节和最终 SO,不能只在 Map 中搜索一个易变或被混淆的函数名。
  • 被丢弃输入节是重要证据:它可能表示未引用代码、COMDAT 选择、节垃圾回收或构建漂移,必须结合项目链接选项和最终 ELF 解释。
  • readelf 用于核对最终 ELF 的 header、program header、dynamic section、符号、重定位和 unwind;Map 与 ELF 交叉验证,彼此不能替代。
  • RELRO 与 NOW 等链接状态可由最终 ELF 检查,但只覆盖部分重定位和装载边界,不能证明业务算法、运行时明文或加固效果。
  • Map、SO、构建参数和归属报告应通过 provenance 与有类型 attestation 绑定到候选摘要,并限制原始路径和内部符号的访问。

Map 回答链接器做了什么,不回答运行时一定怎样

Native 链接 Map 是一次链接决策的文本证据,通常记录输入对象、静态归档成员、输出节、地址范围、符号来源和被丢弃输入节。它可以解释关键逻辑是否进入目标 SO、来自哪个库,以及某些对象为何未贡献最终代码。对加固交付而言,这比只看 SO 文件名更接近真实构成。

Map 不是运行轨迹。对象出现在输出节中,不表示对应函数会被执行;符号未出现在可见列表,也可能因局部化、内联、LTO 或剥离发生变化。Map 更不能证明动态加载成功、异常传播正确、设备兼容或保护强度。任何运行结论仍需最终候选和真实设备回执。

合理交付包包含 mapSha256、finalSoSha256、ABI、Build ID、linkerIdentity、linkCommandDigest、criticalOwnershipReport 和生成时间。原始 Map 可能暴露路径、符号和内部库名,应保存在受控归档;对外报告只输出必要的 assetId、摘要和状态。

链接 Map 可以和不能回答的问题
问题Map 能回答还需证据禁止推导
对象是否参与链接输入对象或归档成员出现最终 ELF 交叉检查函数必然执行
节是否被丢弃discarded section 记录链接选项和资产契约代码一定无用
符号来自哪里对象与归档归属LTO 和符号表源码语义正确
SO 是否匹配Map 本身不能单独证明摘要、Build ID 和证明同名文件即同候选
运行是否兼容不能设备加载与业务回归构建成功等于兼容

必须在实际 Release 链接时生成,不能事后补做

GNU ld options 提供 Map 输出、交叉引用以及多种链接控制能力。Map 应作为正式链接命令的一部分,在生成实际加固输入 SO 的同一动作中输出。若故障后重新链接生成 Map,链接器版本、对象时间、归档成员选择、并行顺序和环境变量都可能已经变化。

命令行、响应文件和工具链都要固定。CMake、Gradle 或自定义脚本可能隐藏最终 linker invocation,发布证据应保存规范化命令摘要而不是只写“已启用 Map”。若一个 ABI 生成多个 SO,每个链接目标都应有独立 Map 和产物摘要,不能让一个项目级文件代表全部目标。

Map 生成失败应阻断需要该证据的候选,而不是在发布后补一份近似文件。工程判断是使用临时路径写入,链接成功后与 SO 一起原子归档;重试会产生新的证据对象。是否允许无 Map 发布属于组织决策,不能把缺失状态伪装成已核对。

Release 链接 Map 的身份字段
字段回答的问题漂移信号门禁
linkerIdentity哪个链接器执行本地与流水线版本不同锁定 toolchain
linkCommandDigest使用何种输入和选项响应文件变化保存规范化摘要
mapSha256哪份 Map同名内容不同禁止覆盖
finalSoSha256对应哪个输出重链后 SO 变化重新生成证据
ABI 与 target属于哪个架构和库一份 Map 复用多目标逐目标归档

用 assetId 连接对象、归档成员、节和最终 SO

关键代码在构建系统中可能经过编译单元拆分、静态归档、模板实例化和 LTO,函数名也可能因重构或 C++ 修饰变化。直接搜索一个符号容易误报。更稳定的方法是为业务资产定义 assetId,再记录预期 objectSuffix、archiveMember、sectionPattern 和 owningSo。

链接归属表应分别列出 linkedSections 和 discardedSections。一个对象既可能有输入节进入最终 SO,也可能有未使用节被垃圾回收。只要关键所需节仍链接且契约允许其他节丢弃,就不应因出现 discarded 记录直接失败;如果对象只出现在丢弃区,才需要阻断解释。

归档成员要精确到 `library.a(member.o)`,不能只记录静态库名,因为同一 archive 中不同成员承担不同资产。objectSuffix 也要保证唯一,两个路径都以相同文件名结尾时应报告歧义。自动化报告只做归属核对,最终业务判断由资产负责人结合源码和链接规则完成。

关键 Native 资产的归属契约
字段用途通过条件异常
assetId稳定连接业务资产全局唯一用函数名充当业务身份
objectSuffix匹配对象或归档成员只命中一个来源同名对象歧义
linkedSections证明贡献输出存在批准节只在丢弃区
discardedSections解释未进入内容符合允许规则关键节被丢弃
owningSo连接最终保护目标SO 摘要匹配报告绑定错误库

被丢弃节需要解释,而不是一律判错或一律忽略

链接器可能因未引用、节垃圾回收、COMDAT 选择、弱符号解析、重复实现或 LTO 优化而不把某些输入节放入输出。Map 的 discarded input sections 可以揭示这一决定。关键对象完全被丢弃,可能表示入口未被引用、注册丢失或配置选错,也可能是设计上允许的备用实现。

门禁应按资产契约判断。对必须存在的核心入口,只有丢弃记录而没有 linked section 时阻断;对允许裁剪的模板实例或平台替代实现,可记录 allowedDiscardReason。规则不能只按 `.text` 名称,因为 `.rodata`、异常表、初始化数组和自定义节也可能属于完整语义。

Map 仍可能因 linker 实现和格式不同而变化。解析器必须声明支持的格式和版本,无法识别时失败关闭,而不是悄悄输出空表。工程判断是先生成一份内部规范化归属报告,再把原始 Map、解析器版本和结果摘要共同归档。

丢弃输入节的处理策略
状态可能原因门禁判断所需材料
关键对象仅丢弃入口未引用或配置漂移阻断链接命令与资产负责人解释
部分节链接部分丢弃函数级裁剪或模板选择按契约审查linked 与 discarded 明细
备用实现丢弃平台选择或弱符号可批准允许理由和触发条件
未知格式未解析linker 版本变化阻断解析格式样本和解析器升级
LTO 后对象折叠链接时优化结合 LTO 证据工具链与最终 ELF

readelf 与 Map 交叉验证最终 ELF,而不是重复同一结论

GNU readelf 文档说明可检查 ELF header、program header、dynamic section、符号、重定位和 unwind 信息。Map 描述链接器如何从输入形成输出,readelf 描述最终 ELF 实际包含的结构。两者从不同方向核对,Map 中预期存在的输出节应能在最终文件结构中找到合理对应。

符号被局部化或剥离后,最终动态符号表可能不再显示内部名称,这不等于对象没有链接。此时 Map、Build ID、节布局和必要的受控符号材料共同解释。相反,Map 记录某对象参与链接,也不能替代 readelf 对真实交付 SO 的检查,因为发布目录可能拿错文件。

交叉验证要绑定同一个 finalSoSha256。读取中间未剥离 SO 的 readelf 结果,再把结论贴到剥离或加固输出上会造成证据错配。若加固改变 ELF 布局,应对输出重新执行允许的结构检查,并通过变换回执连接前后对象。

Map 与 readelf 的互补关系
检查对象Map 证据readelf 证据限制
输入来源对象和归档成员通常不能完整还原LTO 可能折叠
输出节输入到输出映射最终 section table剥离和加固可改变
装载段链接布局线索program headers静态存在不证明加载成功
动态依赖链接选择背景dynamic section不证明依赖可用
unwind 与重定位输入贡献线索最终字段检查不证明异常传播成功

RELRO 与 NOW 是链接状态,不是业务保护结论

GNU ld options 说明 `-z relro` 可生成 PT_GNU_RELRO,`-z now` 要求装载时完成符号解析。团队可以在链接命令和最终 program header、dynamic flags 中核对这些状态。Map 能帮助确认相应链接动作属于当前目标,但最终存在性仍应由 ELF 检查。

这些标志降低部分重定位和装载边界风险,不会隐藏业务算法、阻止所有内存修改或证明运行时明文安全。它们也不能替代 VMP、控制流保护、设备测试或威胁模型。页面只应写可验证链接状态与限制,不能把开启选项扩张成攻击阻断结论。

加固处理可能保留、重写或重新生成 ELF 结构,因此输入 SO 的 RELRO/NOW 结果不能直接贴到输出。发布门禁应对最终加固 SO 再检查,并把 inputSoSha256、outputSoSha256、Map 和变换回执连接起来。若输出结构无法按常规工具解释,应由工具方提供可复核证据。

链接加固状态的证据边界
状态输入证据输出核对不能声称
RELRO链接选项与 MapPT_GNU_RELRO业务算法已保护
NOW链接选项与命令摘要动态标志所有加载风险消失
节垃圾回收discarded sections最终节与资产表所有丢弃都合理
符号可见性对象归属和规则符号表与导出运行调用一定正确
加固输出输入 Map 和变换回执输出 ELF 与设备结果输入状态自动继承

CMake、其他构建系统与供应链证明共同固定证据

Android NDK CMake 说明 CMake toolchain 固定 ABI、平台版本和第三方库导入方式。项目可以在目标链接选项中生成 Map,但要确认选项确实进入最终 target,而不是只配置在未发布的测试目标。Imported library 的内部链接过程不由当前 Map 解释,只能看到它作为输入的参与方式。

NDK with other build systems 说明非标准构建需要显式固定 toolchain、API level、ABI 和编译目标。自定义构建更要保存真实 linker invocation、响应文件和环境摘要。构建参数正确不表示第三方预编译库运行兼容,Map 也无法恢复供应方内部链接决策。

SLSA Provenance v1.1 可把最终 SO 作为 subject,把 Map、对象、toolchain 和参数作为材料或外部参数;in-toto Attestation Statement v1 可用 subject digest 与有类型 predicate 绑定归属审查。证明格式不保证内容真实,仍需可信构建者、签名者和门禁核验。

  • Map 选项作用于真实 Release target
  • 逐 ABI、逐 SO 保存链接命令和 Map
  • Imported library 只声明当前可见边界
  • 非标准构建固定 toolchain 与 API level
  • provenance 绑定 SO、Map、参数和材料摘要
  • attestation 绑定有类型归属审查
  • 证明格式不冒充内容真实性或运行安全

用解析器生成关键对象到最终 SO 的归属表

下面的 Python 示例接收 GNU 风格 Map、最终 SO 和关键资产契约。契约中的每项包含 assetId 与唯一 objectSuffix。解析器区分正常链接区和 `Discarded input sections`,收集对象贡献的输入节,计算 Map 与 SO 的 SHA-256,并输出 linkedSections 与 discardedSections。

若关键对象未出现、后缀匹配到多个来源或只出现在丢弃区,脚本返回非零状态。解析器明确要求出现 `Linker script and memory map` 标记,无法识别格式时失败关闭。它不读取源代码、私钥或设备数据,也不把对象参与链接写成函数会执行。

准备 Native Map 交付核对时,可整理 Release SO、逐 ABI Map、链接命令、关键资产契约、readelf 结果、加固变换回执、provenance 与 attestation,再通过御盾中央平台提交申请。归属门禁通过只证明构建映射满足契约,不证明运行兼容或保护强度。

  • 输入 Map 来自实际 Release 链接
  • 最终 SO 与 Map 同批归档
  • 关键资产使用唯一 objectSuffix
  • linked 与 discarded 输入节分开记录
  • 缺失、歧义和仅丢弃状态阻断
  • Map 与 SO 分别计算 SHA-256
  • 归属结果不冒充运行或保护结论
解析 GNU 风格链接 Map 并核对关键对象归属
from pathlib import Path
import hashlib
import json
import re
import sys

if len(sys.argv) != 4:
    raise SystemExit(2)
map_path = Path(sys.argv[1])
final_so = Path(sys.argv[2])
contract_path = Path(sys.argv[3])
if any(not path.is_file() for path in [map_path, final_so, contract_path]):
    raise SystemExit(2)
contract = json.loads(contract_path.read_text(encoding="utf-8"))
assets = contract.get("assets") if isinstance(contract, dict) else None
if not isinstance(assets, list) or not assets:
    raise SystemExit(2)
if any(not item.get("assetId") or not item.get("objectSuffix") for item in assets):
    raise SystemExit(2)

def digest(path):
    value = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            value.update(chunk)
    return value.hexdigest()

text = map_path.read_text(encoding="utf-8", errors="strict")
if "Linker script and memory map" not in text:
    raise SystemExit(2)
entry_pattern = re.compile(r"^\s*(\.[A-Za-z0-9_.$-]+)\s+0x[0-9a-fA-F]+\s+0x[0-9a-fA-F]+\s+(\S+(?:\.o|\.a\([^)]*\.o\)))\s*$")
mode = "linked"
entries = []
for line in text.splitlines():
    stripped = line.strip()
    if stripped == "Discarded input sections":
        mode = "discarded"
        continue
    if stripped == "Linker script and memory map":
        mode = "linked"
        continue
    match = entry_pattern.match(line)
    if match:
        entries.append({"section": match.group(1), "object": match.group(2), "mode": mode})
if not entries:
    raise SystemExit(2)
violations = []
reports = []
for asset in assets:
    matches = [entry for entry in entries if entry["object"].endswith(asset["objectSuffix"])]
    objects = sorted({entry["object"] for entry in matches})
    if len(objects) != 1:
        violations.append({"assetId": asset["assetId"], "reason": "missing-or-ambiguous-object", "objects": objects})
    linked = sorted({entry["section"] for entry in matches if entry["mode"] == "linked"})
    discarded = sorted({entry["section"] for entry in matches if entry["mode"] == "discarded"})
    if not linked:
        violations.append({"assetId": asset["assetId"], "reason": "object-only-discarded"})
    reports.append({"assetId": asset["assetId"], "object": objects[0] if len(objects) == 1 else None, "linkedSections": linked, "discardedSections": discarded})
result = {"status": "pass" if not violations else "blocked", "mapSha256": digest(map_path), "finalSoSha256": digest(final_so), "ownership": reports, "violations": violations, "boundary": "link ownership does not prove runtime execution or hardening strength"}
print(json.dumps(result, ensure_ascii=False, indent=2))
if violations:
    raise SystemExit(3)

事实依据与适用边界

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

本文判断事实或工程依据适用限制
GNU ld 能生成链接 Map,并支持 RELRO、NOW 等链接控制选项。GNU ld options 描述 Map 输出、交叉引用、`-z relro` 与 `-z now` 等选项。链接选项与 Map 不能证明业务算法、运行时明文或加固保护强度。
readelf 可以检查 ELF header、program header、dynamic section、符号、重定位和 unwind 信息。GNU readelf 描述 ELF 文件各类静态检查能力。静态字段存在不证明动态加载、异常传播或设备运行成功。
CMake toolchain 能固定 ABI、平台版本和第三方预编译库导入方式。Android NDK CMake 描述 Android NDK CMake toolchain 和 imported library。CMake 配置不能解释预编译库内部链接选项或证明其运行兼容。
非标准构建需要显式固定 NDK toolchain、API level、ABI 和编译目标。NDK with other build systems 描述非 CMake 构建接入 NDK 的必要参数。构建参数正确不证明第三方预编译库或最终加固输出兼容。
构建证明可以绑定产物、构建者、构建类型、外部参数和依赖材料。SLSA Provenance v1.1 定义 subject、builder、buildType、parameters 与 materials。provenance 记录构建关系,不能单独证明运行时安全性或声明内容真实。
供应链声明可以用产物摘要和有类型 predicate 绑定归属审查。in-toto Attestation Statement v1 定义 subject、predicateType 和 predicate 结构。声明格式不保证内容真实,仍需可信签名者和独立门禁核验。
关键 Native 资产应通过 assetId 映射到对象、归档成员、输入节和最终 SO。工程判断:函数名会受重构、修饰、LTO 和局部化影响,单一符号搜索不稳定。归属映射提高构建可追溯性,不证明函数执行、业务正确或保护强度。
只出现在 discarded sections 的关键对象应阻断并要求链接解释。工程判断:对象未贡献任何输出节可能表示入口、注册或链接配置漂移。具体丢弃是否合理仍需链接选项、LTO、源码和资产契约共同判断。

工程常见问题

链接 Map 中看到关键对象是否说明代码一定会运行?

不能。Map 只说明链接归属,是否加载和执行需要最终候选上的设备路径、业务断言和运行日志证明。

为什么不能在发布后重新链接生成 Map?

重新链接可能改变工具链、对象顺序、归档选择、LTO 和环境,必须从生成实际 Release SO 的同一次链接动作保存 Map。

discarded input section 出现就一定是错误吗?

不一定。可能是未引用、COMDAT、节回收或平台替代;关键对象只有丢弃没有链接时才应阻断并要求解释。

Map 可以替代 readelf 检查最终 SO 吗?

不能。Map 描述输入到输出的链接决策,readelf 检查最终 ELF 结构,两者应绑定同一 SO 摘要并交叉验证。

开启 RELRO 和 NOW 是否说明 SO 已完成加固?

不能。这些是特定链接和装载状态,不保护全部业务算法或运行时明文,也不能替代加固与设备验收。

申请 Native Map 交付核对前要准备什么?

准备逐 ABI Release SO、链接 Map、链接命令、关键资产契约、readelf 结果、变换回执、provenance 和 attestation,再从御盾中央平台提交申请。

想用自己的 App 验证?

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

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