欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper/

背景

CDE(Code, Data and Environment)最初用于收集程序运行所需的代码、动态库和资源,帮助用户在另一台机器复现运行环境。它的核心思路是通过运行时跟踪被检查程序,记录进程访问过的文件和动态依赖。

本次适配的对象是 CDE 1.0.0,目标平台是 HarmonyOS PC 的 AArch64 环境。项目最终合入 OpenHarmonyPCDeveloper/build_in_harmonyos,PR 为 #6083。这不是一次简单的编译器或 CMake 参数调整,而是对原始运行模型、平台能力边界和交付方式的一次重新设计。

关键难点:原始 ptrace 模型无法直接迁移

上游 CDE 的动态引擎依赖 ptrace 对目标进程进行运行时跟踪,配套的 strace-4.6 测试也依赖特定系统调用和架构支持。在 HarmonyOS AArch64 目标上下文中,原始动态链路存在两个确定限制:

  1. 目标系统没有可用的 CDE 所需 ptrace 运行时路径;
  2. 嵌入的 strace-4.6 没有 AArch64 支持。

因此,上游 57 项动态测试不能在目标系统上原样执行。这里的 0/57 表示“原始动态行为不可执行”,不是“没有测试”,也不是把测试跳过后伪装成通过。这个边界必须在适配报告中单独说明,否则会混淆平台能力限制和库本身缺陷。

适配方案:交付离线静态分析工具

在无法建立原始动态跟踪链的情况下,PR 采用了一个职责边界清晰的替代实现:cde-static-bundler

该工具使用离线静态分析处理 ELF 文件,不执行被检查程序,也不模拟 ptrace。它负责:

  • 解析 ELF 文件和程序头;
  • 递归收集 DT_NEEDED 动态依赖;
  • 处理资源根目录和输出目录;
  • 生成可审计的 manifest;
  • 拒绝畸形 ELF、越界动态偏移、不安全依赖名、路径逃逸和危险 symlink。

工具采用单文件静态编译,构建输入和 SHA-256 都记录在版本归档中。这样做的价值在于:即使目标系统不能运行原始动态引擎,仍然可以对“环境依赖收集”和“安全打包边界”建立可复现、可验证的离线闭环。

测试如何统计

本次严格区分上游原样测试与替代实现测试:

测试集合数量结果含义
上游动态引擎测试570/57 原样执行受 OHOS ptrace 和 AArch64 能力边界限制
静态语义映射 M01-M575757/57逐项映射上游测试关注的静态语义
静态 Bundler 直接契约1212/12离线输入、输出和错误边界验证
Conan 消费者测试33/3验证安装后的 CLI 可被下游调用
安全回归55/5ELF 越界、路径逃逸、资源碰撞等安全场景

替代实现合计 77 项全部通过,但这 77 项不能被写成“上游 CDE 动态测试 77/77 通过”。它们证明的是 cde-static-bundler 的静态分析契约和安全行为,不证明原始 CDEpack ptrace 引擎在 HarmonyOS 上运行成功。

消费者验证为什么重要

除了库自身构建,还必须从使用者角度验证安装后的包。CDE 的消费者测试调用已安装的 cde-static-bundler,覆盖三个真实入口:

  1. --help:验证命令可启动并输出边界说明;
  2. 正向扫描:输入有效静态 ELF,生成包含完整标记和入口信息的 manifest;
  3. 负向扫描:输入非 ELF 文件,程序返回非零状态并给出拒绝原因。

这类测试证明的是“发布后的包能否被上层组件正常调用”,与编译通过或库内部单元测试是不同维度的证据。

安全修复与可追溯性

静态解析器面对的是外部 ELF 输入,因此安全边界不能依赖调用者自觉。本次回归覆盖了:

  • DT_NEEDED 依赖名和路径逃逸;
  • PT_INTERP 与程序头越界;
  • 动态段偏移整数溢出;
  • 资源复制碰撞;
  • 输出目录 symlink 和不安全路径。

每个关键门禁都保留命令、工作目录、退出码和日志哈希。版本归档中的文件清单与 PR 文件列表保持一致,避免出现“本地验证存在、提交中却缺失”的交付问题。

经验总结

这次适配最重要的结论不是“把 CDE 编译出来”,而是先识别原始模型依赖的系统能力,再决定哪些行为可以静态化、哪些行为必须明确标注为不可迁移。

对于依赖特权系统调用、运行时跟踪或特殊架构支持的三方库,建议遵循以下顺序:

  1. 从上游源码和测试入口确认真实依赖,而不是只看构建是否成功;
  2. 在目标系统上验证关键系统接口和架构能力;
  3. 将原始测试、替代测试、消费者测试分别统计;
  4. 对替代实现写清职责边界,不把替代结果冒充上游结果;
  5. 对 ELF、路径和动态依赖处理增加负向安全回归;
  6. 在提交前核对版本归档、测试证据和 PR 文件列表的原子性。

结语

CDE 1.0.0 的 HarmonyOS AArch64 适配说明了一种可复用的方法:当上游动态运行模型受平台能力限制时,不应通过伪造测试通过来掩盖差异,而应构建职责明确的替代工具,保留原始能力边界,并用静态语义、消费者行为和安全回归建立新的可验证闭环。

项目 PR:#6083

实战拆解:问题是怎样一步步解决的

下面按实际适配顺序展开,而不是只描述最终结果。

第一步:先确认上游到底依赖什么

拿到一个“在目标平台运行失败”的库,第一反应不应该是修改编译参数,而是检查上游的运行模型。CDE 的关键依赖不是某个普通头文件,而是运行时跟踪链:启动被检查程序、通过 ptrace 获取进程行为、再结合 strace 记录文件访问。

因此需要检查:

rg -n "ptrace|strace|PTRACE|CDEpack" .
find tests strace-4.6/tests okapi_tests -type f -maxdepth 3

检查结果表明,tests/strace-4.6/tests/okapi_tests/ 中的测试都不是普通的纯函数单元测试,而是依赖动态进程跟踪和目标负载执行。只要 ptrace 运行时能力不存在,修改 CCCFLAGS 或 Conan recipe 都不能让原始测试恢复。

第二步:在 OHOS 上做最小能力探测

随后要把“推测平台不支持”变成可复现证据。探测内容至少包括:

  • ptrace 是否存在并允许当前沙箱调用;
  • 目标架构是否为 AArch64;
  • 内嵌 strace-4.6 是否包含 AArch64 支持;
  • 测试负载是否能以原始方式启动和跟踪。

如果最小探测已经在 ptrace 入口失败,就不能把后续 57 项全部写成“测试代码有问题”。正确结论是:原始动态行为在该目标平台不可执行,必须保留 0/57 的原样执行统计,并把失败归属于平台能力边界。

第三步:确定替代方案的边界

这一步是整个适配的核心。静态 bundler 不能声称自己等价模拟了 ptrace,因为它根本不会运行目标程序。它只承担可以由 ELF 静态信息证明的部分:

输入 ELF
  -> 校验 ELF 头和程序头边界
  -> 读取 DT_NEEDED
  -> 递归定位动态依赖
  -> 复制依赖和资源
  -> 生成 manifest

动态行为无法静态推导的部分必须明确排除,例如运行时 dlopen、条件分支加载、进程执行期间动态生成的文件,以及依赖 ptrace 才能观察到的行为。这个边界写清楚,替代测试才不会被误读为原始引擎测试通过。

第四步:实现静态 ELF bundler

实现时先保持单文件、零外部依赖,降低 OHOS 端构建复杂度。核心实现可以分成几个函数:

  1. 读取并验证 ELF magic、class、data 和 machine 字段;
  2. 根据 program header 的偏移和大小检查整数溢出及文件越界;
  3. 从 dynamic segment 提取 DT_NEEDED 字符串;
  4. 对依赖名做路径和字符校验,拒绝 ../、绝对路径和目录穿越;
  5. 递归解析依赖目录,但限制在允许的 resource root 和 libdir 内;
  6. 以确定性顺序复制文件并生成 manifest;
  7. 对输出目录中的 symlink、同名资源和重复依赖执行拒绝或确定性处理。

编译阶段使用静态单文件命令,确保发布包不依赖构建机运行库:

clang -Wall -Wextra -O2 -static \
  -o cde-static-bundler \
  tools/cde-static-bundler/cde-static-bundler.c

在 Conan recipe 中,package_type 应声明为 application,package() 将可执行文件安装到 bin/package_info() 只导出公开的 PATH 使用方式。对于这种本地源模型,不能凭空填写一个不存在的远程 tarball SHA;应记录实际编译输入和其哈希,并将来源边界写入 manifest 和 notes。

第五步:补齐正向和负向测试

只验证一个正常 ELF 不够。静态解析器最容易在异常输入上出问题,所以测试至少分三层:

上游语义映射:M01-M57
直接契约回归:T01-T12
安全回归:5 项

正向测试使用真实静态 ELF,检查依赖收集和 manifest.json 的完整字段。负向测试分别构造:

  • 非 ELF 文本;
  • 截断的 ELF header/program header;
  • 动态偏移超出文件范围;
  • 不安全依赖名和路径逃逸;
  • 输出资源碰撞或 symlink。

负向用例的“通过”含义是程序正确拒绝输入并返回可观察错误,而不是命令必须返回 0。测试日志要同时记录 subject exit code 和外层断言结果,避免把预期拒绝误判为测试失败。

第六步:验证发布包能被消费者使用

适配完成后还要模拟真实用户,而不是只在源码目录执行二进制。消费者测试应从 Conan 安装包中获取 bin/cde-static-bundler,然后执行:

cde-static-bundler --help
cde-static-bundler scan --entry <valid-static-elf> --output <output-dir>
cde-static-bundler scan --entry <non-elf-file> --output <output-dir>

需要记录:安装包路径、实际调用的公开命令、退出码、manifest 结果和拒绝错误。这样才能证明“别人拿到发布包以后能编译、链接、调用并运行”,而不是证明当前 worktree 中某个临时文件能执行。

第七步:正确汇报测试统计

本案例最容易被误读的地方是测试分母。报告必须写清:

  • 上游原样测试:0/57,原因是 OHOS ptrace/AArch64 能力限制;
  • 静态语义映射:57/57,是替代工具映射,不是上游原样执行;
  • 直接契约:12/12
  • Conan 消费者:3/3
  • 安全回归:5/5

不能把 57+12+3+5=77 写成“上游测试 77/77”,也不能用替代实现的通过率掩盖原始动态模型在目标平台不可执行的事实。

第八步:提交前做原子性审计

最后检查提交是否只包含当前库和真实证据:

git diff --cached --name-only
git diff --check
git diff HEAD^ --stat

应确认版本目录、bundler 源码、测试入口、消费者测试和成对知识草稿都在同一个 PR;临时日志、缓存、其他库修改和本地绝对路径不能进入提交。对本案例而言,PATH_C_ISSUE_BODY.mdPR_BODY.md 同时保留了平台限制、替代边界、测试统计和恢复条件,保证评审者能从 PR 文件反向追溯整个技术决策。

什么时候不能采用这个方案

静态替代不是万能方案。如果库的核心功能本身就是运行时调试、系统调用拦截或进程行为观测,那么静态分析只能作为受限替代,不能宣称完全兼容。此时应:

  • 保留原始测试不可执行的事实;
  • 明确列出不覆盖的动态语义;
  • 只将替代工具支持的行为作为新能力交付;
  • 等待 OHOS 提供所需系统接口,或由上游增加目标平台后再恢复原始测试。

这也是本次适配能够通过复审的关键:解决的是可静态证明、可安全交付的部分,没有把平台限制隐藏成虚假的“全量通过”。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐