SIGBUS:非法内存访问的一种典型崩溃
本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
凌晨两点,线上监控报警:应用崩溃,信号是 SIGBUS。
值班同学盯着日志看了半天,心里冒出一个朴素的问题:这是什么"总线"?
SIGBUS 不是公交车,也不是某个中间件的缩写。它来自 Signal Bus,表示程序访问内存时触碰了硬件、地址规则或底层文件对象不允许的区域。在 HarmonyOS C++ 应用的 CppCrash 日志里,SIGBUS 主要看两类错误:
• BUS_ADRALN:内存地址没按要求对齐;
• BUS_OBJERR:底层对象访问错误,比如映射文件被截断。
两类问题都叫"非法内存访问",但排查方向完全不同。
一、BUS_ADRALN:地址没对齐,CPU 拒绝访问
BUS_ADRALN 是内存地址对齐错误。
很多数据类型对地址有要求。一个 4 字节整数通常要放在满足 4 字节对齐的位置;AArch64 环境下,栈指针必须保持 16 字节对齐。地址"歪"了,某些指令访问时就会触发 SIGBUS。
常见原因两种。
第一,结构体被强行压缩,成员落在了不合适的位置。比如 #pragma pack,或者手动算偏移时过于自信,把需要 4 字节对齐的成员塞到了奇怪的位置。
第二,把 char*、void* 强行转换成 int*、long* 等更严格的类型。指针类型变了,地址没跟着变整齐,CPU 不接受。
还有一种更激进的写法:把普通字符串地址转成函数指针直接调用。文档案例里,函数返回字符串地址,代码却把它当函数地址执行,程序跳进一段普通字符数据,那里不是可执行代码,最终触发异常。
如何定位 BUS_ADRALN
看到 CppCrash 日志,先别急着翻代码,先确认三件事。
第一,检查 Reason 字段:
Signal: SIGBUS (BUS_ADRALN)
第二,查看 fault addr。案例中故障地址无法被 4 整除,这是对齐异常的重要线索。
第三,分析调用栈。优先看业务代码栈帧,尤其是最靠近崩溃点的那一帧。如果最上面的 #00 没有映射到有效代码地址,继续检查下一层业务栈帧。
带符号的 so 文件可以用 llvm-addr2line 把地址偏移还原到具体代码行。然后重点检查:
• 是否存在指针强制类型转换;
• 结构体是否用了不合理的对齐或压缩;
• 是否手动改过栈指针;
• 是否把数据地址误当成函数地址;
• 是否存在越界计算导致的异常指针。
DevEco Studio 的 FaultLog、寄存器信息、内存映射也能帮助确认故障地址落在哪里。
二、BUS_OBJERR:地址还在,底层对象没了
BUS_OBJERR 比 BUS_ADRALN 更容易误判。
它不一定是指针本身错了,而是:这个虚拟地址看起来还在,但它依赖的底层对象已经无法正常提供数据。
最常见的场景是 mmap 映射文件被截断。
程序把一个 8 KB 文件映射到内存,另一个线程执行了 ftruncate 把文件缩短成 4 KB。原来的虚拟映射区域还在,但后面 4 KB 已经没有真实文件内容支撑。
如果程序继续访问偏移 5120 的位置,操作系统发现"地址存在,但对象没了",于是报告:
Signal: SIGBUS (BUS_OBJERR)
除了文件被截断,BUS_OBJERR 还可能和内存映射 I/O 错误、硬件寄存器访问方式不当、系统资源限制有关。但对普通应用开发来说,mmap 和文件并发操作是最值得优先排查的方向。
三、一个典型的多线程事故
文档里有一个典型的日志缓存场景。
线程 A 不断向映射区域写日志,线程 B 定期通过 ftruncate 缩小文件清理旧日志。
如果两个线程没有同步措施,事故可能这样发生:
1. 线程 A 检查文件大小,发现偏移 5120 可以写;
2. 线程 B 把文件截断到 4096;
3. 线程 A 按刚才的判断写入偏移 5120;
4. 映射区域已经失去文件支撑;
5. 应用收到 SIGBUS(BUS_OBJERR),退出。
这类问题最麻烦的地方在于:代码单独运行时可能完全正常,只有在特定时序下才崩溃。它不是"每次都错",而是"刚好赶上就错"。开发者很容易误以为是设备、系统或者运气不好。
实际上,运气只是没有被加锁的并发 bug 的另一种说法。
如何定位 BUS_OBJERR
排查按"地址、偏移、文件状态"三步走。
第一步,确认日志中的信号类型和故障地址:
Signal: SIGBUS (BUS_OBJERR) @ 0x...
第二步,根据调用栈和寄存器,确认业务代码实际访问了哪个映射区域。优先分析应用自己的 so,系统库和标准库通常只是事故现场的路人。
第三步,计算访问偏移,检查访问发生时文件的实际大小。重点关注:
• 映射长度是否超过文件真实大小;
• 文件是否可能被其他线程或进程截断;
• 访问偏移加访问长度是否超出当前文件边界;
• mmap、ftruncate、munmap 是否存在并发执行;
• 是否缺少文件锁或互斥锁。
带符号版本的 so 可以继续用 llvm-addr2line 定位到具体源码行,再结合 Hilog 和 FaultLog 还原故障时序。
四、降低 SIGBUS 风险的几个习惯
1. 访问前做边界检查
无论是普通文件读写还是 mmap 访问,不要只相信"之前获取过的文件大小"。
if (offset + length > currentFileSize) {
return false;
}
注意整数溢出,实际代码中最好用安全的加法判断。
2. 把 mmap 封装起来
不要让业务代码到处直接操作裸指针。可以封装一个 SafeMmap 类,统一管理:
• 文件打开与关闭;
• 映射与解除映射;
• 文件大小记录;
• 读写范围检查;
• 失败后的资源释放。
这不仅能减少 SIGBUS,也能顺手解决文件描述符泄漏、异常路径未释放等问题。
3. 并发访问必须同步
写线程访问映射区域时,清理线程不能同时执行 ftruncate。
用 std::mutex、文件锁或更合适的读写锁,确保:
• 写入期间文件不会被截断;
• 截断前不会有线程继续使用旧映射;
• 截断完成后重新检查文件大小。
但加锁不是万能护身符。锁住之后仍然要做边界检查,因为文件可能已经被缩小,映射长度也不会自动变大或变小。
4. 少做危险的指针转换
指针类型转换不是魔法。转换前要确认:
• 地址满足目标类型的对齐要求;
• 对象确实拥有目标类型的生命周期;
• 访问范围没有越界;
• 不会把数据指针误当成函数指针。
如果只是想解析字节流,优先用 memcpy,或者用明确的序列化、反序列化方案。
五、看到 SIGBUS,先别急着甩锅给系统
SIGBUS 看起来很底层,但很多问题最终能回到业务代码:
• 地址计算是否正确;
• 对齐要求是否满足;
• 文件大小是否发生变化;
• 多线程之间是否存在竞态;
• 调用栈中的业务帧是否已经定位;
• 是否保留了带符号的 so 和完整日志。
下次看到 SIGBUS,可以记住几句排查口诀:
ADRALN 查对齐,
OBJERR 查对象;
先看地址和栈,
再查偏移与并发。
内存不会因为代码"看起来合理"就自动守规矩。稳定的程序,靠的是对地址、边界、生命周期和并发关系都做出明确约束。
🔗 官网开发者学堂视频:华为开发者学堂

🔗 社区DFX专题文章: 华为开发者问答 | 华为开发者联盟


【扫码加入 HarmonyOS DFX 技术交流群】
更多推荐

所有评论(0)