我们在开发过程中经常碰到coredump问题,其中有一类是内存数据被异常改写导致的,对于这一类问题处理起来其实是比较困难的。根据实际调试经验针对这类问题进行归纳总结,方便后续问题定位。
内存改写导致coredump问题根据问题内存是否已被释放可以分为2类:低位内存越界改写和内存已被释放导致改写。
低位内存越界改写
对于这类情况,我们很好理解,就是出问题的堆内存对其下面的低地址部分内存越界改写导致自身数据异常。针对这类场景,通常被破坏的堆内存块的头结构被破坏,且较多连续的内存数据都被异常改写。我们可以根据被改写数据前后的特征字符串查找主动改写内存块的相关流程,相关方法可参考《疑难问题定位案例复盘(一)_局部变量越界_sy4331的博客-CSDN博客》相关章节。
内存已被释放导致改写
当内存已经被释放后原流程还在继续使用将存在意想不到的结果,因此内存改写的第二种场景就是问题堆内存已经被释放了导致相关数据被改写。对于这种场景,根据问题内存是否已经被再次分配,可以分为两种类型:被释放的内存没有被再次分配出去和被释放的内存已经被再次分配出去。
被释放的内存没有被再次分配出去
当我们使用free接口把堆内存释放后,glibc的ptmalloc内存分配器将使用双向链表维护相关的空闲内存。具体措施是在分配给用户的堆内存的前8个字节处放置两个指针,分别指向空闲链表的前后节点。堆内存的头结构数据具体如下:
根据上面分析可知,当堆内存被释放后,其原有用户数据部分的前8个字节将被改写为空闲双向链表的前后指针,此时若原有流程还在继续使用将产生意想不到的结果。
这时,使用该空闲内存块有两方:原有业务流程和glibc的ptmalloc分配器,它们都有可能因为改写数据造成对方产生coredump。也即这时有2种可能后果:
- 由于A模块继续使用该堆内存,特别是使用堆内存的前8个字节时(如当指针使用),将造成模块自身异常甚至产生coredump。
- A模块继续使用该堆内存,且改写了其前8个字节数据,即改写了空闲链表的前后指针。这样下次某个模块申请内存时,ptmalloc遍历打算使用该内存块前,因校验错误,ptmalloc将发送abort信号,申请内存模块产生coredump。
这类问题的特点是内存块的头结构数据正常,而用户可用内存的前8个字节看起来是两个堆内存地址,而其他部分内存数据基本正常。上述第2种场景可参考案例《疑难问题定位案例复盘(二)_sy4331的博客-CSDN博客》。
被释放的内存已经被再次分配出去
当问题内存被释放又再次被分配出去时,由于新申请模块也会改写内存数据,从而导致流程异常。
如上图所示,假设A模块先申请了堆内存,此后B模块释放了该内存,后续该内存又被重新分配给C模块。此时,A模块和C模块都在使用该部分堆内存,也即使用该堆内存有两方:A模块和C模块,它们都有可能因为改写数据造成对方产生coredump。这时也有2种可能后果:
- A模块继续使用该堆内存并改写了数据,造成C模块访问时数据异常,C模块产生coredump。
- C模块使用新申请的堆内存并改写了数据,造成A模块访问时数据异常,A模块产生coredump。
由于通常C模块是后获得内存的一方,其会覆盖原有A模块填充的数据,因此该堆内存出问题时往往填充的数据基本都是C模块的数据。所以我们在解析coredump时通常会看到2种场景。
- 如果最终是上面的第1种情况,即A模块改写数据造成C模块产生coredump,此时我们解析coredump将看到该内存只有极少数部分数据被改写,大部分数据都正常,符合C模块预期。此时给我们的感觉似乎是有人在跳跃式改写。所以当我们碰到跳跃式改写时可以考虑这种场景。
- 如果最终是上面的第2种情况,即C模块使用新申请的堆内存并改写了数据,造成A模块产生coredump,此时我们解析coredump将看到该内存绝大部分数据都被改写,基本都不符合A模块的预期。相较于上面场景,这种情况我们可能更容易联想到是内存被释放后又被分配出去改写造成coredump。
上述第2种场景可参考案例《疑难问题定位案例复盘(一)_局部变量越界_sy4331的博客-CSDN博客》。