c/c++ linux后台服务器开发学习地址:https://ke.qq.com/course/417774?flowToken=1013300
最近定位了在一个多线程服务器程序(OceanBase MergeServer)中,一个线程非法篡改另一个线程的内存而导致程序core掉的问题。定位这个问题历经曲折,尝试了各种内存调试的办法。往往感觉就要柳暗花明了,却发现又进入了另一个死胡同。最后,使用强大的mprotect+backtrace+libsigsegv等工具成功定位了问题。整个定位过程遇到的问题和解决办法对于多线程内存越界问题都很典型,简单总结一下和大家分享。
现象
core是在系统集成测试过程中发现的。服务器程序MergeServer有一个50个工作线程组成的线程池,当使用8个线程的测试程序通过MergeServer读取数据时,后者偶尔会core掉。用gdb查看core文件,发现core的原因是一个指针的地址非法,当进程访问指针指向的地址时引起了段错误(segment fault)。见下图。
发生越界的指针ptr_位于一个叫做cname_的对象中,而这个对象是一个动态数组field_columns_的第10个元素的成员。如下图。
复现问题
之后,花了2天的时间,终于找到了重现问题的方法。重现多次,可以观察到如下一些现象:
1. 随着客户端并发数的加大(从8个线程到16个线程),出core的概率加大;
2. 减少服务器端线程池中的线程数(从50个到2个),就不能复现core了。
3. 被篡改的那个指针,总是有一半(高4字节)被改为了0,而另一半看起来似乎是正确的。
4. 请看前一节,重现多次,每次出core,都是因为field_columns_这个动态数组的第10个元素data_[9]的cname_成员的ptr_成员被篡改。这是一个不好解释的奇怪现象。
5. 在代码中插入检查点,从field_columns_中内容最初产生到读取导致越界的这段代码序列中“埋点”,既使用二分查找法定位篡改cname_的代码位置。结果发现,程序有时core到检查点前,有时又core到检查点后。
综合以上现象,初步判断这是一个多线程程序中内存越界的问题。
使用glibc的MALLOC_CHECK_
因为是一个内存问题,考虑使用一些内存调试工具来定位问题。因为OB内部对于内存块有自己的缓存,需要去除它的影响。修改OB内存分配器,让它每次都直接调用c库的malloc和free等,不做缓存。然后,可以使用glibc内置的内存块完整性检查功能。
使用这一特性,程序无需重新编译,只需要在运行的时候设置环境变量MALLOC_CHECK_(注意结尾的下划线)。每当在程序运行过程free内存给glibc时,glibc会检查其隐藏的元数据的完整性,如果发现错误就会立即abort。用类似下面的命令行启动server程序:
export MALLOC_CHECK_=2
bin/mergeserver -z 45447 -r 10.232.36.183:45401 -p45441
使用MALLOC_CHECK_以后,程序core到了不同的位置,是在调用free时,glibc检查内存块前面的校验头错误而abort掉了。如下图。