前言
最近在维护 HarmonyOS 的 SSH 相关库时,将依赖的 OpenSSL 从 1.1.1 升级到了 3.5.0,本以为是个常规的版本升级,结果却遇到了一个诡异的崩溃:程序在 FFRT 异步线程中执行加密操作时,偶现 SIGILL(非法指令)崩溃! 💥
更奇怪的是,崩溃指令是 ARM SVE2 的 xar 指令,但设备的 CPU 明明不支持 SVE2!OpenSSL 的 CPU 特性检测怎么会出错?🤔
经过一番深入排查,我发现这是 OpenSSL 的 SIGILL 探测机制与 HarmonyOS FFRT 框架的一次"不兼容碰撞"。这个问题非常隐蔽,值得分享给大家。
第一章:问题现象 - 崩溃来得猝不及防
1.1 崩溃日志分析
升级 OpenSSL 后,程序在连接 SSH 服务器时偶现崩溃,日志如下:
Reason:Signal:SIGILL(ILL_ILLOPC)@000000000000000000
Fault thread info:
Tid:19402, Name:OS_FFRT_2_12
调用栈(关键部分):
#00 pc 000000000028a570 libcrypto.so.3 ← 崩溃点
#01 pc 0000000000517934 libcrypto.so.3
#02 pc 00000000005ac4c4 libcrypto.so.3
#09 pc 0000000000372c1c libcrypto.so.3(EVP_DecryptUpdate+652)
#12 pc 000000000005ce64 libssh.so.4 ← SSH 解密数据
#25 pc 0000000000035508 libssh.so.4(ssh_connect+672)
#28 pc 0000000000075c04 libace_napi.z.so(AsyncWorkCallback+488)
#30 pc 00000000000b3114 libffrt.so(UVTask::ExecuteImpl+228) ← FFRT异步任务
关键信息:
- 崩溃信号:
SIGILL(ILL_ILLOPC)- 非法操作码 - 崩溃线程:
OS_FFRT_2_12- HarmonyOS 的 FFRT 异步任务线程 - 崩溃位置:
libcrypto.so.3的偏移0x28a570 - 业务场景:SSH 连接时
1.2 反汇编分析:揪出元凶
使用 llvm-objdump 查看崩溃地址的指令:
llvm-objdump -d -C --start-address=0x28a550 --stop-address=0x28a5f0 libcrypto.so.3
0000000000289cc0 <ChaCha20_ctr32_sve>:
28a560: 04a10000 add z0.s, z0.s, z1.s ← SVE指令
28a564: 04a50084 add z4.s, z4.s, z5.s
28a568: 04a90108 add z8.s, z8.s, z9.s
28a56c: 04ad018c add z12.s, z12.s, z13.s
28a570: 04703403 xar z3.s, z3.s, z0.s, #16 ← 💥 崩溃点!
28a574: 04703487 xar z7.s, z7.s, z4.s, #16
...
真相大白:
- 崩溃函数:
ChaCha20_ctr32_sve()- ChaCha20 加密算法的 SVE 优化版本 - 崩溃指令:
0x04703403 xar z3.s, z3.s, z0.s, #16 - 指令类型:SVE2 专属指令(异或 + 循环移位)
问题来了:为什么 CPU 不支持 SVE2,程序却执行了 SVE2 指令?
第二章:深入探查 - OpenSSL 的 CPU 特性检测
2.1 OpenSSL 的 CPU 检测机制
回到 armcap.c 文件,这是 OpenSSL 检测 ARM CPU 特性的核心代码。
三种检测方式
OpenSSL 3.x 在 ARM 平台上支持三种检测方式:
// 方式1: Apple 平台 - 使用 sysctl
#if defined(__APPLE__)
OPENSSL_armcap_P |= sysctl_query("hw.optional.armv8_2_sha512", ARMV8_SHA512);
}
// 方式2: Android/Linux - 使用 getauxval()
#elif defined(OSSL_IMPLEMENT_GETAUXVAL)
if (getauxval(OSSL_HWCAP2) & OSSL_HWCAP2_SVE2)
OPENSSL_armcap_P |= ARMV8_SVE2;
}
// 方式3: 其他平台 - SIGILL 探测
#else
OPENSSL_armcap_P |= arm_probe_for(_armv8_sve2_probe, ARMV8_SVE2);
}
#endif
我们的环境使用哪种方式?
因为 OpenSSL 没有专门适配 OHOS 使用 getauxval(),所以退回到了 SIGILL 探测方式 —— 而这正是问题的根源!
2.2 SIGILL 探测机制:聪明但危险
原理剖析
SIGILL 探测的思路很"聪明":尝试执行一条 SVE2 指令,如果 CPU 不支持就会触发 SIGILL 信号,捕获后返回"不支持"。
// 探测包装函数
static unsigned int arm_probe_for(void (*probe)(void), volatile unsigned int value)
{
if (sigsetjmp(ill_jmp, 1) == 0) { // 设置信号跳转点
probe(); // 执行测试指令(可能触发 SIGILL)
return value; // 没有 SIGILL → 返回 value(支持)
} else {
return 0; // SIGILL → 返回 0(不支持)
}
}
// SVE2 探测汇编代码(crypto/armv8-sve2-test.S)
.globl _armv8_sve2_probe
_armv8_sve2_probe:
.inst 0x04e03400 // xar z0.d, z0.d, z0.d ← SVE2指令
ret
正常流程:
┌──────────────────────────────────────────────────┐
│ _armv8_sve2_probe() │
│ └─> 执行 xar 指令 │
│ └─> CPU 触发 SIGILL │
│ └─> ill_handler() 被调用 │
│ └─> siglongjmp(ill_jmp, sig) │
│ └─> sigsetjmp 返回非0 │
│ └─> 返回 0(不支持) │
└──────────────────────────────────────────────────┘
为什么在 OHOS 环境下失败?
这里需要理解 OpenSSL 的初始化时机:
# if defined(__GNUC__) && __GNUC__>=2
void OPENSSL_cpuid_setup(void) __attribute__ ((constructor));
# endif
__attribute__((constructor)) 让 OPENSSL_cpuid_setup() 成为构造函数,会在:
- 动态库加载时自动执行
- 在任何其他代码之前执行(包括 main 函数)
- 由动态链接器触发执行
在我们的场景中:
libssh.so 加载
└─> libcrypto.so.3 被依赖加载
└─> 动态链接器执行 OPENSSL_cpuid_setup()
└─> 执行 SIGILL 探测
问题根源:
OHOS 的 FFRT(Function Flow Runtime)框架 在执行任务时可能会发生协程切换,把任务调度到另一个线程上,而不同线程可能运行在不同的 CPU 核心:
- 主线程加载 SO 时:在大核(支持 SVE2)上执行 SIGILL 探测,检测到 CPU 支持 SVE2
- FFRT 执行任务时:任务被调度到小核(不支持 SVE2)执行
- 运行加密代码时:尝试执行 SVE2 指令,触发 SIGILL 崩溃
这就是典型的大小核异构 CPU 导致的 CPU 特性不一致问题!
异常流程图:
❌ 异常流程(FFRT 跨核调度):
┌──────────────────────────────────────────────────┐
│ 主线程(大核) │
│ └─> 加载 libcrypto.so.3 │
│ └─> OPENSSL_cpuid_setup() │
│ └─> _armv8_sve2_probe() 在大核执行 │
│ └─> 探测成功,标记支持 SVE2 ✓ │
├──────────────────────────────────────────────────┤
│ FFRT 任务(小核) │
│ └─> SSH 连接任务被调度到小核 │
│ └─> 执行 ChaCha20_ctr32_sve() │
│ └─> 尝试执行 xar 指令(SVE2) │
│ └─> 小核不支持 SVE2 │
│ └─> 💥 SIGILL 崩溃 │
└──────────────────────────────────────────────────┘
第三章:解决方案
强制使用 getauxval()
解决方案很明确:确保编译时正确定义 __OHOS__ 宏,让 OpenSSL 使用可靠的 getauxval() 方式。
getauxval() 会返回内核检测到的所有核心共有的 CPU 特性,因此不会受到大小核调度的影响。
适配 OHOS
在 armcap.c 中添加 OHOS 平台的适配代码:
# elif defined(__OHOS__)
# include <sys/auxv.h>
# define OSSL_IMPLEMENT_GETAUXVAL
# pragma message("OHOS: Using getauxval for CPU capability detection")
验证是否生效
编译后查看日志,应该看到:
OHOS: Using getauxval for CPU capability detection
第四章:深度理解 - Constructor 与信号处理
4.1 Constructor 的执行时机
void OPENSSL_cpuid_setup(void) __attribute__((constructor));
这个属性让函数在非常早的时机执行:
进程启动流程:
1. 加载器启动
2. 加载所有依赖库(递归)
3. ⭐ 执行所有库的 .init_array(constructor)
4. 执行 main() 函数前的初始化
5. 执行 main()
潜在问题
在 constructor 中执行的代码:
- ❌ 不能依赖其他库的初始化状态
- ❌ 不能假设信号处理器已正确设置
- ❌ 不能使用线程局部存储(可能未初始化)
- ❌ 在动态加载场景下可能在异步线程执行
4.2 SIGILL 探测的局限性
SIGILL 探测本质上是一种 Hack 手段,依赖于:
-
信号处理器能正常工作
sigaction(SIGILL, &ill_act, &ill_oact); // 必须成功设置 -
setjmp/longjmp 机制正常
sigsetjmp(ill_jmp, 1); // 保存上下文 siglongjmp(ill_jmp, sig); // 恢复上下文 -
信号不会被屏蔽或重定向
sigprocmask(SIG_SETMASK, &ill_act.sa_mask, &oset);
4.3 为什么 getauxval() 更可靠?
getauxval() 读取的是 内核提供的辅助向量(Auxiliary Vector),这是内核在进程启动时传递给用户空间的 CPU 信息:
// 内核在 execve() 时构建 aux vector
AT_HWCAP = CPU 硬件特性位(由内核检测,所有核心共有特性)
AT_HWCAP2 = 扩展特性位
核心优势:
- ✓ 由内核直接提供,100% 可靠
- ✓ 无需信号处理,无副作用
- ✓ 返回所有核心的共有特性,不受线程调度影响
- ✓ 性能开销极小(一次系统调用)
第五章:排查工具与技巧
5.1 必备工具
LLVM 工具链
# 查看符号表
llvm-nm libcrypto.so.3 | grep ChaCha20
# 反汇编特定函数
llvm-objdump -d -C --symbol=ChaCha20_ctr32_sve libcrypto.so.3
# 查看段信息
llvm-readelf -S libcrypto.so.3
地址转符号
# 方法1: addr2line(简单)
llvm-addr2line -e libcrypto.so.3 0x28a570
# 方法2: objdump(详细)
llvm-objdump -d -C --start-address=0x28a550 --stop-address=0x28a5f0 libcrypto.so.3
第六章:总结与最佳实践
6.1 问题回顾
这次崩溃的根本原因链:
编译时未定义 __OHOS__
↓
OpenSSL 退回到 SIGILL 探测方式
↓
libcrypto.so 在主线程(大核)中被加载
↓
OPENSSL_cpuid_setup() 在大核中执行
↓
SIGILL 探测机制在大核检测到 CPU 支持 SVE2
↓
全局变量标记:CPU 支持 SVE2 ✓
↓
FFRT 将任务调度到小核执行
↓
小核尝试执行 SVE2 指令
↓
💥 SIGILL 崩溃(小核不支持 SVE2)
关键教训: 在大小核异构 CPU 平台上,不能使用基于信号探测的方式检测 CPU 特性,必须使用内核提供的 getauxval() API。
总结
这次问题排查让我深刻体会到:
-
版本升级不只是换个版本号 📦
OpenSSL 3.x 的 CPU 检测机制比 1.1.1 复杂得多,需要仔细评估平台兼容性 -
SIGILL 探测在大小核架构下不可靠 ⚔️
看似聪明的 Hack 手段,在异构 CPU 平台上会产生误判 -
优先使用系统 API 🎯
对于 CPU 特性检测这类关键功能,应该依赖内核提供的标准 API,而不是自己探测
希望这篇文章能帮助遇到类似问题的同学快速定位和解决!如果你也在做 HarmonyOS 的底层适配工作,欢迎交流~ 🤝
参考资料
- OpenSSL 3.x EVP API 文档
- ARM Architecture Reference Manual
- Linux getauxval() 手册
- HarmonyOS FFRT 开发指南
- Signal-safe functions (POSIX)