Open
[aarch64] v2.7-h5补丁两处缺陷修复:桩内异常无法unwind(call风格跳板)及IsEqual对无operator==类型编译失败(h4逻辑回归) #2
JankinEEAdd on  Sep 29
JankinEE
JankinEE
Sep 29 Create

背景

hcomm 仓(https://gitcode.com/cann/hcomm)在 aarch64 环境打通全量单元测试时,将 mockcpp 从 v2.7-h4 升级到 v2.7-h5(即本仓 PR #18 合入的 mockcpp-2.7_aarch64.patch,对应 runtime 仓 PR #701)。升级解决了 h4 单条 B 指令 ±128MB 跳转距离限制的问题,但实测暴露出 v2.7-h5 补丁的两处缺陷,已在 hcomm 侧以仓内兼容补丁临时规避,现回馈本仓,建议合入并发布 v2.7-h6。

消费方 hcomm 侧的适配 PR:https://gitcode.com/cann/hcomm/pull/5664 (内含两个临时兼容补丁,本 issue 合入后可删除)

问题一:桩内抛出的 C++ 异常无法穿过 hook,触发 std::terminate

现象

任何"桩内抛异常"的用例(全 mockcpp 生态常见用法)在 aarch64 下全部失败:

terminate called after throwing an instance of 'mockcpp::Exception'
  what(): <业务异常文案>

hcomm 仓实测受影响用例 26 处(异常注入类:.will(throws(...))、invoke 桩内 throw),无一幸免。

根因

v2.7-h5 的 SET_JMP_CODE(src/JmpCodeAARCH64.h)为 call 风格跳板:

stp x29, x30, [sp,#-16]!   // 压栈
movz/movk x30, target      // 装载 hook 地址到 x30
blr x30                    // 调用 hook
ldp x29, x30, [sp],#16     // 弹栈
ret

该跳板是运行时写到被 hook 函数入口的手写机器码,没有任何 .eh_frame/unwind 信息。桩内(或 mockcpp 机制内,如 ChainableMockMethodBase 的 catch 后 MOCKCPP_REPORT_FAILURE 重抛)抛出的异常,unwinder 走到 hook 的返回地址(即跳板缓冲区内的地址)时找不到 unwind 记录,直接 std::terminate。

对照:x86 的 JmpCodeX86 是 5 字节 jmp rel32,不建立任何栈帧,hook 直接返回原调用者,异常沿正常编译帧 unwind 回被测代码的 catch 分支——所以该缺陷为 aarch64 的 h5 跳板特有。

修改方案:br x16 寄存器跳转风格

movz x16, #imm16           // 将 hook 函数完整 64 位地址装入 x16
movk x16, #imm16, lsl #16
movk x16, #imm16, lsl #32
movk x16, #imm16, lsl #48
br    x16                  // 无链接跳转

设计依据(AAPCS64):

  • x16 是 IP0 暂存寄存器,veneer/thunk 专用,可任意破坏,不影响参数寄存器 x0-x7;
  • 不压栈、不改动 x30/sp:hook 以"原函数直接跳转进入"的形态执行,其 ret 直接返回原调用者(x30 仍为原调用者返回地址),SP 保持 16 字节对齐;
  • 因此栈上不存在手写帧,异常 unwind 沿正常编译帧(hook/桩/被测代码均有 .eh_frame)直达被测代码的 catch——行为与 x86 完全等价;
  • 覆写从 32 字节减到 20 字节,仍建议消费方对 UT 代码加 -falign-functions=32 保证不越过函数边界;
  • 返回值经 x0 原样回传,正常(非异常)路径与原跳板行为一致。

编码(br x16 = 0xD61F0200,movz/movk 基址 0xD2800010/0xF2800010,与现行 x30 版本仅寄存器号不同)已在 hcomm 仓验证。

问题二:IsEqual 对无 operator== 的类型编译失败(h4 逻辑回归)

现象

任何无 operator== 的自定义类型进入 IsEqual<T>(典型路径:returnValue(结构体) → ValueHolder<T>::getConstraint() 为虚函数,类实例化即强制实例化 eval())即编译失败:

error: no match for 'operator==' (operand types are 'X' and 'const X')
   return any_cast<T>(val) == expectedValue;

根因

v2.7-h4 的 mockcpp-2.7_py3.patch 中 IsEqual.h 的 eval() 有针对 class/union 类型的 memcmp 分支:

if constexpr (std::is_class<T>::value || std::is_union<T>::value) {
    return !memcmp((void *)&val, (void *)&expectedValue, sizeof(T));
} else {
    return any_cast<T>(val) == expectedValue;
}

v2.7-h5 补丁基于原始 mockcpp-2.7 包重新生成时丢失了这段逻辑(h4/h5 补丁逐文件对比,IsEqual.h 的差异仅有此一处 + 无关的 unique_ptr 特化删除)。实测对比:v2.7-h4 可编译的同批代码,换 v2.7-h5 后 returnValue(Hccl::CollAlgOpReqDef结构体) 等用法全部编译失败。

修改方案

在 v2.7-h5 补丁的 IsEqual.h 段恢复上述 if-constexpr 分支(即 h4 原有逻辑,逐行恢复),并补充 #include <type_traits>。

验证情况

上述两处修改以仓内兼容补丁形式应用于 hcomm 仓(https://gitcode.com/cann/hcomm/pull/5664):

  • aarch64(鲲鹏/GCC13/lld)全量 UT:63 个套件全部通过;
  • x86_64 行为不受影响(x86 路径代码未改动);
  • 异常注入用例 26 处全部恢复通过(问题一修复后);
  • 含结构体返回值的用例全部恢复编译(问题二修复后)。

期望

  1. 将两处修改合入 v2.7.x-h5 分支的补丁文件(或新增 v2.7-h6 补丁);
  2. 发布 v2.7-h6 release,消费方(hcomm/runtime 等)可直接引用;
  3. hcomm 仓将删除本地兼容补丁、改引 v2.7-h6(见关联 PR)。

关联:

  • 本仓 PR #18(引入 v2.7-h5 的 aarch64 跳板,即本 issue 修复对象)
  • runtime 仓 PR #701(v2.7-h5 的另一个消费方)
  • hcomm 仓 PR #5664(消费方适配 + 本 issue 两处修复的临时兼容补丁与验证报告)
likedislike
JankinEEJankinEE
Sep 29 Associated pull request
JankinEEJankinEE
25 days ago Associated pull request
JankinEE
JankinEE
25 days ago Comments:

修复PR已更新:https://gitcode.com/cann-src-third-party/mockcpp/merge_requests/29 (原PR #28因不支持改目标分支已关闭)

应看护人安排,新PR提交至 v2.7.x-h6 分支:新增 mockcpp-2.7-h6.patch(即原修复内容,对h5补丁的两处修复),并删除被取代的历史补丁(mockcpp-2.7.patch / mockcpp-2.7_py3.patch / mockcpp-2.7-h5.patch)与源码包副本(tar.gz / zip,历史分支与既有release中仍可获取);分支保留 Readme.opensource(三方软件合规元数据)与 mockcpp-2.7-h6.patch。

likedislike
JankinEE
JankinEE
25 days ago Comments:

调整说明:源码包 mockcpp-2.7.tar.gz / mockcpp-2.7.zip 保留在 v2.7.x-h6 分支(经确认与 OBS/release 资产逐字节一致,消费方虽从 OBS/release 取包,但按评审意见保留),PR #29 仅清理历史补丁文件(mockcpp-2.7.patch / mockcpp-2.7_py3.patch,h5 补丁重命名为 h6 并纳入修复)。

likedislike