空指针解引用未触发段错误 后续free调用崩溃原因排查
空指针解引用未即时触发段错误的原因分析
问题背景
调试程序崩溃问题时涉及如下代码片段:
1184 static void 1185 xyz_delete (<struct type1> *c, <struct type2> **a) 1186 { // 省略若干无关代码 1196 b = *a; 1197 if (!b) { 1198 return; 1199 } // 省略若干无关代码 1203 prev = b->next; 1204 b->next = NULL; // 省略若干无关代码 1245 free_timer(b->active_timer); // 省略若干无关代码 }
对应崩溃的调用栈如下:
#1 0x456789123 in __free [__be___free] (ptr=<optimized out>, saved_caller_pc=0x123456789 , attr=0x0) at free.c:1234 #2 0x345678912 in xyz_delete [__be_xyz_delete...] (c=c@entry=0x234567891, a=a@entry=0x0) at myfile.c:1245 #3 0x455678912 in abc (apple=0x52453545, a=<optimized out>, hello=12) at myfile:1312
从调用栈可确认,传入xyz_delete函数的第二个参数a为NULL。但代码1196行对a执行解引用时没有触发崩溃,1203、1204行对解引用得到的指针b做多次读写也未抛出异常,最终段错误出现在1245行调用free_timer(b->active_timer)的位置,已知free_timer内部会调用free函数。
原因说明
首先纠正一个普遍误区:空指针解引用并不保证会立刻触发段错误。空指针解引用属于C语言标准定义的未定义行为,程序的表现没有任何规范约束,段错误只是最常见的结果之一,且完全可能延迟到后续代码才触发,这个场景下的延迟崩溃有几个非常典型的合理解释:
- 高优化等级下部分访存指令被编译器删除
从调用栈大量<optimized out>标记可以判断,当前运行的二进制是高优化等级编译的。编译器在优化阶段会做数据流分析,如果判断某段访存操作的结果没有被实际使用、不会产生可观测的副作用,会直接删掉对应指令。如果1196行的指针读取、1203/1204行的next指针读写被判定为无效代码,实际运行时根本不会执行对应的访存操作,自然不会触发异常。 - 低地址内存存在有效映射,前序非法访存未触发硬件异常
正常配置下进程虚拟地址0附近的内存页是未映射、无访问权限的,访问该区域会立刻触发页错误抛出段错误。但在部分特殊场景下:比如32位程序兼容运行、进程主动做了低地址内存映射、系统关闭了低地址访问保护,0地址附近的内存页是具备读写权限的。
此时a=NULL时执行b = *a,本质是从地址0位置读取一个指针长度的值作为b,这一步是合法访存不会崩溃;如果读出来的垃圾值b刚好落在进程可读写的内存区间,后续修改b->next的操作也只是改写了某块可访问内存的内容,不会立刻触发异常。直到把垃圾内存中读出来的b->active_timer传给free,堆分配器校验时发现这个指针不属于合法分配的堆块,要么触发内部断言,要么访问到无权限的堆元数据区域,才会触发段错误。 - 未定义行为的崩溃点本身就和出错点无强绑定关系
只要触发了未定义行为,程序的执行状态就已经脱离了标准的约束,不会精准在出错的代码行抛出问题。很多时候非法访存只是悄悄破坏了内存数据,要等到后续逻辑执行到无法兜底的状态才会崩溃,崩溃点和根因点隔了几十上百行、甚至跨函数调用都是C/C++内存问题的常态。
验证提示:如果关闭编译优化重新复现问题,大概率会在1196行解引用
a的位置就直接触发段错误;也可以检查崩溃时进程的内存映射,确认0地址附近是否存在可读写的映射段。
内容的提问来源于stack exchange,提问作者Naman Sharma
相关产品推荐
相关产品推荐

