先看结论与判断条件

  • 兼容基线必须绑定旧版与候选 SO 的 SHA-256、ABI、ELF Class、Machine、SONAME 和构建身份,不能只比较文件名。
  • 动态导出的名称相同并不足够,还要比较版本节点、默认版本、绑定、符号类型、可见性和定义状态,任何变化都需要所有者解释。
  • 每个发布 ABI 有独立调用约定、寄存器和对齐规则,arm64-v8a 的静态通过不能替代其他已承诺 ABI 的产物与设备回归。
  • C++ 符号携带名称修饰、模板、异常、RTTI 与运行库耦合;跨 SO 公共接口应优先采用稳定、显式且可测试的边界。
  • 版本脚本可以收敛导出并维护版本节点,但不能隐藏运行时必需入口,也不能把 ABI 破坏自动变成兼容升级。
  • 静态差异报告是发布阻断输入,最终结论仍需同一候选在目标设备上完成加载、符号解析、调用、异常与核心路径回归。

先把 SO 的公共 ABI 从实现细节中分离出来

SO 升级兼容的对象不是“库里所有符号”,而是调用方已经依赖的公共二进制契约。这个契约至少包含导出名称、符号版本、绑定、类型、可见性、参数与返回值的机器级传递方式、结构体布局、SONAME、依赖库和失败语义。若团队只保存一份头文件,实际动态符号与调用方依赖发生漂移时,头文件无法解释已经发布的二进制行为。

Android NDK symbol visibility 建议使用版本脚本和默认隐藏来收敛 Native 导出面,同时显式保留 JNI 或公共 API 入口。可核查事实是链接器能够控制导出;工程判断是所有保留入口都应进入 ABI 清单并拥有 owner、introducedVersion、allowedConsumers 和 removalPolicy。未进入清单的内部实现可以收敛,但不能因为名称看似内部就擅自删除已被外部模块调用的符号。

验收顺序应是旧版基线、候选静态对比、真实调用方回归,而不是先加固再从崩溃反推。本文没有具体旧版 SO、候选产物或设备回执,因此只给出契约与门禁方法,不声称任何库、SDK 或御盾项目已经兼容。保护变换也不是 ABI 修复器,基线中的错误导出和不稳定 C++ 边界必须先由代码所有者处理。

公共 Native ABI 的最小契约字段
字段回答的问题破坏性变化示例证据来源
symbolName调用方解析什么名称删除或重命名动态符号表
version/default名称绑定哪个版本节点默认版本切换版本定义与需求
bind/type/visibility怎样导出和解析GLOBAL 变 LOCALdynsym
callingContract参数、返回值和布局怎样传递结构体布局变化头文件、编译器与测试
SONAME/NEEDED装载身份与依赖是什么依赖新增或 SONAME 漂移dynamic section

基线必须绑定候选身份并逐 ABI 采集

比较前先固定 old 与 candidate 的文件摘要、构建版本、源码提交、NDK 与编译器信息、目标 ABI 和构建变体。两个都叫 libengine.so 的文件可能来自不同缓存或不同 product flavor,若没有 SHA-256,差异报告无法回答扫描的是哪一对产物。SONAME 也应单独保存,因为归档文件名相同不代表动态装载身份没有变化。

Android ABIs 说明不同 ABI 具有各自的调用约定、寄存器、对齐与设备支持范围。契约清单必须按 armeabi-v7a、arm64-v8a、x86 或 x86_64 分开,且只覆盖产品实际承诺的发布范围。不能把一个 ABI 的符号集合机械复制到另一个 ABI,因为条件编译、汇编实现、数据模型和构建脚本都可能让导出或类型发生差异。

GNU readelf 可以检查 ELF header、program header、dynamic section、符号、重定位和 unwind 信息。采集器应从 ELF 本身读取 Class、Machine、SONAME、NEEDED、动态符号和版本信息,同时保存工具版本与原始输出摘要。静态字段能够建立可复核基线,但它不执行加载器、JNI 注册或函数调用,因此报告状态应写成 static-contract-passed,而不是 runtime-compatible。

旧版与候选身份核对
对象必须相同或明确变化阻断条件允许继续的证据
构建变体release、flavor、配置比较了错误变体构建记录与产物摘要
ABI同一目标架构逐对比较跨 ABI 直接对比ELF Machine 与目录一致
文件身份SHA-256 唯一摘要缺失或重复使用旧报告不可变文件与哈希
SONAME保持或经过迁移批准无说明的装载身份变化调用方和装载策略更新
工具链版本与标志可追溯来源无法还原编译、链接和依赖清单

动态符号要比较名称、类型、绑定和可见性

只用 nm 提取一列名字会漏掉关键差异。一个符号可以保持同名,却从 GLOBAL 变成 LOCAL、从 DEFAULT 可见性变成 HIDDEN、从 FUNC 变成 OBJECT,或从已定义项变成 UND。调用方看到的解析结果会随这些字段改变,所以清单应使用统一键表示 name 与 version,并把 bind、type、visibility、section state 作为契约属性。

删除旧版公共导出通常应直接阻断;新增导出不一定破坏兼容,但可能扩大公共面或造成名称冲突,也需要审阅。属性变化必须由所有者分类:内部符号误入基线可以通过批准的 contract manifest 移除,外部 ABI 则需要保持、提供兼容包装或安排明确迁移。禁止用一张无限增长的 allowlist 把所有差异静默忽略。

弱符号、IFUNC、TLS 对象和编译器生成符号需要单独分类,不能与普通函数一概比较。文章中的示例脚本只接受 FUNC 与 OBJECT 等 readelf 可见类型并原样报告属性,它不推断参数签名,也不反编译函数体。若公共契约依赖结构体布局、枚举宽度或调用约定,还要用头文件 ABI 检查与真实调用测试补足。

动态符号差异分类
差异默认处置理由解除阻断条件
公共符号删除阻断既有调用方可能无法解析兼容入口恢复或迁移完成
版本节点变化阻断名称可能绑定到不同实现契约版本策略和调用方验证
bind/type/visibility 变化阻断解析或使用方式发生变化所有者逐项批准并回归
新增公共符号审阅可能扩大 ABI 面或冲突进入契约或改为隐藏
内部符号删除条件允许不属于外部契约manifest 证明没有允许消费者

版本节点和默认版本决定同名符号怎样绑定

ELF 符号版本允许同名接口关联不同版本节点,调用方还可能要求某个具体节点。验收时应同时采集 version definition、version need 与动态符号上的版本标记,区分默认版本和非默认版本。只看到 libfoo.so 仍导出 api_open,并不能证明旧调用方请求的 api_open@LIB_1.0 仍可解析。

版本演进的稳健做法是保留旧节点的行为契约,为不兼容语义引入新节点,并明确哪个版本作为新链接的默认项。删除旧节点、把默认项直接切换到行为不兼容实现,或让候选新增未声明的 version need,都应先阻断。具体兼容策略取决于调用方分布和发布模型,不能用“版本号更大”替代二进制验证。

GNU ld options 包含版本脚本等链接控制能力,也说明 -z relro 与 -z now 等选项会改变生成段或符号解析时机。版本脚本负责选择和组织导出,安全链接标志负责其他链接属性,两者不能互相替代。启用 RELRO 或立即绑定并不会恢复被删除的版本节点,也不能保护业务算法或证明运行时调用成功。

符号版本演进决策
候选变化旧调用方风险门禁验证重点
保留旧节点并新增节点通常可控审阅旧版和新版消费者分别调用
删除旧节点明确解析失败风险阻断完成调用方迁移后再移除
切换默认版本新链接行为改变阻断默认与显式版本均回归
新增 version need部署环境可能缺少节点阻断minSdk 与目标设备解析
只改版本脚本未改源码导出仍可能改变完整对比dynsym 与 version-info

调用约定与数据布局要在每个架构上单独验收

符号成功解析只说明加载器找到了一个地址,不说明调用方和被调用方对参数、返回值、寄存器、栈、对齐和结构体布局达成一致。公共函数若改变整数宽度、浮点传递、可变参数、结构体字段顺序或打包方式,名称完全不变也可能产生错误数据或崩溃。C 接口同样需要稳定头文件、显式类型和架构测试。

Android ABIs 的差异要求团队逐 ABI 构建调用方夹具。夹具应覆盖边界值、空值策略、结构体尺寸与对齐、回调、错误码和资源释放,并把旧版与候选结果绑定到同一输入。若某个 ABI 没有实际候选产物或设备覆盖,报告必须标记未验证,不能由其他 ABI 的通过结果推断。

静态工具可以记录 ELF Class、Machine 与符号大小,却无法从动态符号表完整恢复 C 或 C++ 函数签名。工程上可把公共头文件生成的 ABI 描述、编译器诊断和调用夹具加入证据包。本文示例只检查可见的 ELF 契约变化,发现 size 变化会提示审阅,但不把 size 相同写成参数布局兼容。

调用约定回归维度
维度静态输入运行验证失败信号
标量参数固定宽度类型声明边界值往返截断或符号扩展变化
结构体sizeof、alignof、字段偏移旧调用方传入候选字段错位或越界
返回值返回类型与所有权成功和失败路径寄存器或释放约定不一致
回调函数指针签名同步、异步和取消栈破坏或生命周期错误
可变参数调用规则与格式真实格式组合架构相关读取错误

C++ ABI 还受运行库、异常和 RTTI 约束

C++ 公共符号通常包含名称修饰,并可能暴露 std::string、容器、模板、虚表、type_info 或异常类型。这些对象的二进制契约受编译器、标准库、编译标志和类布局影响。两个版本的 mangled name 相同不能覆盖所有语义,名字变化也可能只是源码签名已经改变的直接证据,因此跨 SO 公共边界要比普通 C 函数更谨慎。

Android C++ library support 说明 libc++ 的链接方式以及异常、RTTI 等能力取决于构建系统和运行库选择。若多个 SO 交换 C++ 对象或传播异常,应保存每个模块的运行库链接方式、异常和 RTTI 标志,并验证 type_info 身份、捕获行为、析构与 unwind。只看到 libc++_shared.so 已被打包,不能证明跨模块异常链完整。

工程上更容易维护的边界是 C ABI 加显式句柄、固定宽度标量和由同一模块分配释放的缓冲区,并把 C++ 实现留在库内部。这不是所有产品的强制规则,但能减少工具链细节泄漏到消费者。已有 C++ ABI 不能粗暴替换,需先枚举真实消费者、保持旧包装层并安排迁移回归。

C++ 跨 SO 契约风险
对象耦合来源需要记录不能只看
mangled function编译器与签名完整符号和工具链可读函数名
STL 对象libc++ 与布局运行库链接方式头文件相同
异常type_info 与 unwind异常、RTTI 标志和设备回归能够编译链接
虚表类布局与继承类 ABI 和构造析构路径某个方法仍存在
跨模块内存分配器与所有权创建、释放和错误规则指针类型相同

平台 Native API 需求必须服从 minSdk 与装载环境

候选新增 NEEDED 或未定义动态符号时,还要判断目标平台是否在 minSdk 范围内提供它。Android NDK stable APIs 说明高于 minSdk 的 Native API 不能直接静态调用,必要时可以通过 dlopen 与 dlsym 做版本化处理。静态对比因此要把新增未定义符号和依赖库列为阻断项,交由 API 可用性审查。

动态解析也不是无条件通行证。库所在 namespace、装载路径、设备系统版本和符号可见性都会影响实际结果,且 dlsym 失败需要明确降级分支。来源只支持版本化处理方法,不证明任意库在所有 namespace 都可达。验收应在最低支持 API、代表性中间版本和当前目标版本上触发真实解析与功能路径。

候选若通过版本脚本隐藏了 JNI 或供其他 SO 使用的入口,平台 API 检查也无法补救。依赖方向应分开记录:本库提供给消费者的 export contract,以及本库向系统或其他库提出的 import contract。前者看 removed exports 与版本节点,后者看新增 UND、NEEDED 和版本需求,两边任一无解释变化都应阻断。

导入契约的 API 可用性门禁
变化首要问题静态处置设备验证
新增 UND 符号minSdk 是否提供阻断并核对 API最低支持版本解析
新增 NEEDED库是否可装载阻断并核对 namespace真实安装与加载
新增 version need目标节点是否存在阻断并采集需求代表版本逐一解析
改用 dlopen/dlsym失败分支是否完整审阅路径和错误处理可用与不可用版本均测试
删除 UND 或 NEEDED行为是否改变审阅功能与错误路径回归

把静态差异、真实调用和发布证据合成一道门禁

下方脚本以 old SO、candidate SO 和契约 JSON 为输入,使用 readelf 读取 ELF 身份、dynamic section、动态符号与版本信息。它按 name 与 version 组成键,报告删除、新增、属性变化、SONAME、NEEDED、Class、Machine 和版本信息摘要变化。契约 JSON 只允许列出经过所有者批准的内部删除,输入不合法、工具失败或存在未批准破坏时明确返回非零。

脚本不会解析 C++ 参数签名、结构体布局、运行时 namespace 或函数行为,也不会执行候选。其通过状态只表示采集范围内没有未批准的静态变化。随后还要让真实旧调用方在目标 ABI 设备上加载候选,覆盖版本化与默认符号、JNI 入口、错误码、结构体、回调、C++ 异常和释放路径,并对比未加固基线。

发布证据应保存旧版与候选摘要、脚本与 readelf 版本、契约 manifest、原始输出摘要、设备矩阵、调用回执和批准记录。任何未覆盖 ABI、API 或消费者必须列为限制,不得折算成通过。需要提交实际 SO 符号版本兼容评估时,先准备这套不可变材料,再通过御盾中央平台提交申请。

SO ABI 发布门禁状态
状态含义允许动作仍需证据
identity-invalid候选或 ABI 无法确认停止比较正确产物与构建记录
static-contract-blocked存在未批准破坏修复或迁移新静态差异报告
static-contract-passed静态契约未见阻断进入设备回归真实加载与调用
runtime-limited部分矩阵通过保留限制剩余 ABI、API、消费者
release-review-ready证据链完整提交批准业务与发布责任人签署
  • old 与 candidate 均绑定 SHA-256、构建变体和同一目标 ABI
  • readelf 工具版本和原始输出摘要随报告保存
  • 删除、版本节点和属性变化默认阻断,内部例外必须逐项批准
  • 新增导出与 NEEDED 进入人工审阅,不因兼容性较低风险而忽略
  • 静态通过后用真实旧调用方在目标设备执行加载与调用回归
  • 未覆盖 ABI、API、厂商或消费者明确列为限制
对比两个 SO 的动态符号、版本节点与装载身份
#!/usr/bin/env python3
import hashlib
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path

SYMBOL_RE = re.compile(
    r"^\s*\d+:\s+([0-9a-fA-F]+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$"
)
SONAME_RE = re.compile(r"\(SONAME\).*?\[(.+?)\]")
NEEDED_RE = re.compile(r"\(NEEDED\).*?\[(.+?)\]")
CLASS_RE = re.compile(r"^\s*Class:\s*(.+)$", re.MULTILINE)
MACHINE_RE = re.compile(r"^\s*Machine:\s*(.+)$", re.MULTILINE)

def fail(message, code=2):
    print(message, file=sys.stderr)
    raise SystemExit(code)

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

def run_readelf(readelf, args, path):
    command = [readelf, *args, str(path)]
    completed = subprocess.run(
        command,
        check=False,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        timeout=30,
    )
    if completed.returncode != 0:
        fail("readelf failed for " + path.name + ": " + completed.stderr.strip(), 10)
    return completed.stdout

def load_contract(path):
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
        fail("cannot read contract: " + str(exc))
    if not isinstance(value, dict):
        fail("contract must be a JSON object")
    allowed = value.get("allowedRemovedInternalSymbols", [])
    if not isinstance(allowed, list) or any(not isinstance(item, str) or not item for item in allowed):
        fail("allowedRemovedInternalSymbols must be an array of non-empty strings")
    return set(allowed)

def split_name_and_version(raw_name):
    clean = raw_name.split()[0]
    if "@@" in clean:
        name, version = clean.split("@@", 1)
        return name, version, True
    if "@" in clean:
        name, version = clean.split("@", 1)
        return name, version, False
    return clean, "", False

def parse_symbols(text):
    symbols = {}
    for line in text.splitlines():
        match = SYMBOL_RE.match(line)
        if not match:
            continue
        value, size, kind, bind, visibility, ndx, raw_name = match.groups()
        name, version, is_default = split_name_and_version(raw_name)
        if not name or name == "0":
            continue
        key = name + "@" + version
        record = {
            "name": name,
            "version": version,
            "defaultVersion": is_default,
            "value": value,
            "size": int(size),
            "type": kind,
            "bind": bind,
            "visibility": visibility,
            "ndx": ndx,
        }
        if key in symbols and symbols[key] != record:
            fail("duplicate dynamic symbol key: " + key, 11)
        symbols[key] = record
    if not symbols:
        fail("no dynamic symbols were parsed", 12)
    return symbols

def parse_identity(header_and_dynamic):
    class_match = CLASS_RE.search(header_and_dynamic)
    machine_match = MACHINE_RE.search(header_and_dynamic)
    if not class_match or not machine_match:
        fail("ELF Class or Machine is missing", 13)
    soname_match = SONAME_RE.search(header_and_dynamic)
    needed = sorted(set(NEEDED_RE.findall(header_and_dynamic)))
    return {
        "class": class_match.group(1).strip(),
        "machine": machine_match.group(1).strip(),
        "soname": soname_match.group(1).strip() if soname_match else "",
        "needed": needed,
    }

def inspect(readelf, path):
    header_dynamic = run_readelf(readelf, ["--file-header", "--dynamic", "--wide"], path)
    dynsym = run_readelf(readelf, ["--dyn-syms", "--wide"], path)
    versions = run_readelf(readelf, ["--version-info", "--wide"], path)
    return {
        "path": path.name,
        "sha256": sha256_file(path),
        "identity": parse_identity(header_dynamic),
        "symbols": parse_symbols(dynsym),
        "versionInfoSha256": hashlib.sha256(versions.encode("utf-8")).hexdigest(),
    }

def public_symbols(records):
    return {
        key: value
        for key, value in records.items()
        if value["ndx"] != "UND" and value["bind"] in {"GLOBAL", "WEAK"}
    }

def compare(old, candidate, allowed_removed):
    findings = []
    for field in ("class", "machine", "soname"):
        if old["identity"][field] != candidate["identity"][field]:
            findings.append({"kind": "identity-changed", "field": field, "old": old["identity"][field], "new": candidate["identity"][field]})
    old_needed = set(old["identity"]["needed"])
    new_needed = set(candidate["identity"]["needed"])
    for name in sorted(new_needed - old_needed):
        findings.append({"kind": "needed-added", "name": name})
    for name in sorted(old_needed - new_needed):
        findings.append({"kind": "needed-removed", "name": name})

    old_public = public_symbols(old["symbols"])
    new_public = public_symbols(candidate["symbols"])
    for key in sorted(old_public.keys() - new_public.keys()):
        if key not in allowed_removed:
            findings.append({"kind": "public-symbol-removed", "symbol": key})
    for key in sorted(new_public.keys() - old_public.keys()):
        findings.append({"kind": "public-symbol-added-review", "symbol": key})
    compared_fields = ("type", "bind", "visibility", "ndx", "defaultVersion", "size")
    for key in sorted(old_public.keys() & new_public.keys()):
        changes = {
            field: {"old": old_public[key][field], "new": new_public[key][field]}
            for field in compared_fields
            if old_public[key][field] != new_public[key][field]
        }
        if changes:
            findings.append({"kind": "public-symbol-attributes-changed", "symbol": key, "changes": changes})
    if old["versionInfoSha256"] != candidate["versionInfoSha256"]:
        findings.append({"kind": "version-info-changed-review"})
    return findings

def main():
    if len(sys.argv) != 4:
        print("Usage: compare_so_abi.py OLD_SO CANDIDATE_SO CONTRACT_JSON", file=sys.stderr)
        raise SystemExit(2)
    old_path = Path(sys.argv[1])
    candidate_path = Path(sys.argv[2])
    contract_path = Path(sys.argv[3])
    if not old_path.is_file() or not candidate_path.is_file() or not contract_path.is_file():
        print("all three input files must exist", file=sys.stderr)
        raise SystemExit(2)
    readelf = shutil.which("readelf")
    if not readelf:
        fail("readelf is not available", 3)
    allowed_removed = load_contract(contract_path)
    old = inspect(readelf, old_path)
    candidate = inspect(readelf, candidate_path)
    findings = compare(old, candidate, allowed_removed)
    blocking = [item for item in findings if not item["kind"].endswith("-review")]
    result = {
        "status": "static-contract-blocked" if blocking else "static-contract-passed",
        "old": {"sha256": old["sha256"], "identity": old["identity"]},
        "candidate": {"sha256": candidate["sha256"], "identity": candidate["identity"]},
        "findings": findings,
        "boundary": "Static ELF comparison does not prove runtime ABI compatibility.",
    }
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    if blocking:
        raise SystemExit(20)

if __name__ == "__main__":
    main()

事实依据与适用边界

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

本文判断事实或工程依据适用限制
版本脚本和默认隐藏可以收敛 Native 导出面并保留明确入口。Android NDK symbol visibility 说明 Android NDK 的符号可见性控制。减少导出不能隐藏运行时必需接口,也不等于 VMP 或 ABI 兼容通过。
链接器选项可以控制版本脚本、RELRO 与符号解析等链接行为。GNU ld options 描述 GNU 链接器的版本和 -z 相关选项。链接标志不能恢复被删除接口,也不保护业务算法或运行时明文。
ELF header、dynamic section、符号、重定位和 unwind 信息可以静态检查。GNU readelf 描述 readelf 支持检查的 ELF 信息。静态字段存在不证明动态加载、参数布局、调用行为或异常传播成功。
高于 minSdk 的 Native API 不能被直接静态调用,必要时可做版本化动态解析。Android NDK stable APIs 说明 Native API 的版本可用性。API 可用性不证明动态链接路径在全部 namespace 与设备中可达。
每个 Android ABI 有独立调用约定、寄存器、对齐与设备支持范围。Android ABIs 说明 NDK 支持 ABI 的技术差异。声明支持某 ABI 不证明对应产物已经构建、打包和运行验证。
C++ 异常、RTTI 与 libc++ 链接方式取决于构建系统和运行库选择。Android C++ library support 说明 NDK 的 C++ 运行库与功能配置。启用编译选项不证明跨 SO 的 type_info、析构与 unwind 完整。
公共符号删除、版本节点漂移及绑定、类型或可见性变化应默认阻断。工程判断:这些变化使既有调用方的解析或调用契约无法得到唯一解释。内部符号可在真实消费者清单和所有者批准后例外,不得用无限 allowlist 掩盖差异。
静态 ABI 差异通过后仍需真实旧调用方在目标 ABI 设备执行加载和调用回归。工程判断:动态符号表无法完整证明调用约定、数据布局、namespace 和运行行为。单一设备与 ABI 通过不能外推到完整 API、ABI、厂商和消费者矩阵。

工程常见问题

两个 SO 导出符号名称完全相同,是否说明 ABI 兼容?

不能。还要比较版本节点、默认版本、绑定、类型、可见性、SONAME、依赖、调用约定和数据布局,并让真实旧调用方在目标 ABI 设备执行。

新增动态导出为什么也需要审阅?

新增通常不会让旧调用方立即失效,但可能扩大公共 ABI、造成名称冲突或暴露内部实现。应确认它是有意契约,否则通过版本脚本或隐藏可见性收敛。

保留旧符号但切换默认版本是否安全?

不应默认安全。新链接调用方可能绑定到新的默认实现,显式要求旧节点的消费者行为又不同,需要分别验证默认与指定版本的解析和调用。

C++ mangled name 没变化能否证明参数和异常兼容?

不能。类布局、标准库、编译器标志、type_info、析构和 unwind 仍可能改变,跨 SO 的 C++ 对象与异常必须结合工具链记录和设备回归。

脚本显示 static-contract-passed 后可以直接发布吗?

不可以。该状态只覆盖 readelf 可见差异,还需验证目标 ABI 的装载、版本解析、调用约定、结构体、回调、JNI、异常和核心业务路径。

申请 SO 符号版本与 ABI 评估需要准备什么?

准备旧版和候选 SO、SHA-256、发布 ABI、工具链与版本脚本、公共契约 manifest、真实调用方、设备矩阵和运行回执,再通过御盾中央平台提交申请。

想用自己的 App 验证?

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

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