
Yang Zhifeng关键字多线程,内存越界,valgrind,电子栅栏,mprotect,libsigsegv,glibc最近位于多线程服务器程序(OceanBaseMergeServer)中,该线程非法篡改了另一个线程的内存,导致程序核心问题. 找到整个问题花了整整一周的时间. 在此期间,我尝试了各种内存调试方法. 通常感觉到,诸如mprotect + backtrace + libsigsegv之类的工具必须成功定位问题. 在整个定位过程中遇到的问题和解决方案是多线程内存跨界问题的典型问题,因此,我将简要总结并与您分享. 对于只对最终组合秘诀感兴趣的学生,请直接阅读最后一节. 其他章节则在此处针对大众科学撰写. 现象核心是在系统集成测试期间发现的. 服务器程序MergeServer具有一个由50个线程组成的线程池. 当使用8个线程的测试程序通过MergeServer读取数据时,后者有时会成为核心. 使用gdb检查核心文件,发现该核心的原因是指针的地址非法. 当进程访问指针指向的地址时,将导致段错误.

请参见下图. 越界指针ptr_位于名为cname_的对象中,并且该对象是动态数组field_columns_的第十个元素的成员. 如下所示. 之后,花了2天的时间终于找到了重现该问题的方法. 重复多次,可以观察到以下现象: 随着并发客户端数量的增加(从8个线程增加到16个线程),核心输出的可能性增加;被篡改的指针总是一半(高4字节)已更改为0,而另一半似乎请参见上一节. 多次重复,每次输出内核时,都是由于field_columns_的cname动态地计数了10个元素data_ [9] _member的ptr_成员已被篡改. 这是一个很难解释的奇怪现象. 在代码中插入检查点,从field_columns_中间顺序有时是核心到检查点之前,有时是核心到检查点之后. 基于上述现象,初步判断这是多线程程序中的内存跨界问题. 使用glibc的MALLOC_CHECK_,因为它是内存问题,请考虑使用一些内存调试工具来定位问题. 由于OB具有自己的存储块缓存,因此需要消除其影响. 修改OB内存分配器,使其每次都直接调用malloc并释放c库,而无需进行缓存. 然后,您可以使用glibc的内置内存块完整性检查功能.
使用此功能,无需重新编译程序,只需在运行时设置环境变量MALLOC_CHECK_(请注意下划线). 每当在程序运行期间为glibc提供可用内存时,glibc都会检查其隐藏元数据的完整性,并在发现错误时立即中止. 使用类似于以下内容的命令行启动服务器程序: export MALLOC_CHECK_ = 2 bin / mergeserver 10.232.36.183:45401-p45441使用MALLOC_CHECK_之后,程序核心转到另一个位置,并调用free glibc来检查检查头内存块前面的错误,并且异常终止. 如下所示. 但是,这个核心只能给我们带来很少的信息. 我们只是找到了另一种稍微更有效地重现该问题的方法. 也许我最初看到的核心现象被延迟了. 实际上,记忆被“更早地”打破了. glibc提供的MALLOC_CHECK_函数太简单了. 是否有更高级的工具不仅可以报告错误,还可以分析问题的原因?我们自然想到了著名的瓦尔格朗德. 使用valgrind检查内存问题,无需重新编译程序,只需使用valgrind即可启动: nohup valgrind --error-limit = no --suppressions =禁止bin / mergeserver 10.232.36.183:45401-p45441> nohup. out&默认情况下,当valgrind发现1000个不同的错误或错误总数超过1000万时,它将停止报告错误.

添加--error-limit = no后,可以禁用此功能. --suppressions用于屏蔽一些无关紧要的误报. 经过一番折腾,valgrind无法复制核心问题. valgrind报告的错误也是与问题无关的误报. 可能是因为运行程序的valgrind会使程序的性能降低10倍以上,这将影响多线程程序的运行时间,从而导致内核无法重现. 这条路无路可走. magicnumber由于MALLOC_CHECK_可以检测程序的内存问题,因此我们实际上想知道是谁(哪段代码)越界了. 这时,我们想到了使用幻数填充来指示数据结构. 如果我们在越界内存中看到某个幻数,那么我们知道是哪个代码. 首先,修改malloc的封装函数,用特殊值(此0xEF)填充返回给用户的内存块,并在开头和结尾处申请额,就可以确定哪个线程超出范围. 代码示例如下. 然后,当用户程序通过我们的自由条目释放内存时,它将检查我们填充到边界的幻数.

同时调用mprobe以强制glibc对内存块执行完整性检查. 最后,将magicnumber添加到程序中所有可疑的关键数据结构中,以便在调试器中检查内存时可以识别它们. 例如,如果完成,则全部累加起来. 使用MALLOC_CHECK_重新运行. 如我们所愿,程序核心再次下降. 在越界位置检查内存: 如上图所示,红色部分是我们自己填充的越界检查标头,我们可以看到它没有损坏. 确定存储在第二行中的线程号确实等于我们当前线程的线程号. 蓝色部分是前一个动态内存分配的结尾,该分配也已完成(24字节0xdc). 两行0x44afb60 0x44afb68中显示的内存是glibc malloc存储其自己的元数据的地方. 程序核心的原因是它在检查两行的完整性时发现错误. 由此推断,非法篡改的内容小于16个字节. 仔细观察这16个字节的内容,我们看不到熟悉的幻数,因此无法通过错误推断代码. 我们最初发现的核心现象相互证实了这一点. 非法修改的内容可能只有4个字节(int32_t大小). 此,但该程序仍将以glibc malloc的元数据为核心,而不是我们添加的范围.
此栏(称为efence)在这里. 使用MALLOC_CHECK_或magicnumber进行检测的最大问题是此检查是“事后”. 在复杂的多线程环境中,如果在无法发生损坏的情况下首次对站点进行检查,则往往找不到罪魁祸首的线索. 电子围栏使用底层硬件提供的机制(CPU提供的虚拟内存管理)来保护内存区域. 实际上,它使用了mprotect系统调用,我们将在下一部分中编写代码.
修改了受保护的内存后,程序将立即进入内核. 通过检查核心文件的回溯,可以轻松找到问题代码. 该库的版本有点混乱,容易出错. 当我搜索并下载该库时,发现电子围栏的作者也是著名的busybox的作者. 原始作者的官方网站上的下载地址为. 但是,此版本在编译并链接到Linux上的程序时将报告警告,并且在以后执行时也会出错. 后来,我找到了由debian提供的更高版本的库. 据估计,社区在Linux上有所改进. 我使用的最后一个版本是2.2.4:. 要使用efence,您需要重新编译程序. 编译后,efence提供了一个静态库libefence.a,其中包含一组可以代替glibc的malloc,free和其他库函数的实现. 编译时需要一些技巧. 首先,在编译命令行上将-lefence放在其他库之前;其次,使用-umalloc强制g ++从libefence中查找最初包含在glibc中的库函数,例如malloc: -umalloc–lefence使用字符串来检查生成的程序是否为真. 使用过的efence: 与许多工具类似,efence也可以修改其运行时行为通过设置环境变量.
通常,efence在每个内存块的末尾放置一个不可访问的页面,当程序超出范围访问内存块后面的内存时,它将被检测到. 如果设置EF_PROTECT_BELOW = 1,则在内存块之前插入一个不可访问的页面. 通常,efence仅检测分配的内存块. 分配一个块后,将在释放之后对其进行缓存,直到下一次分配时才会再次检测到它. 如果设置了EF_PROTECT_FREE = 1,将不会再次分配所有可用内存,并且efence会检测到是否非法使用了释放的内存(这是我们目前怀疑的位置). 但是由于不重用内存,因此内存可能会大大扩展. 我使用以上2个标记的4种组合来运行我们的程序. 不幸的是,该问题无法重现,efence未报告任何错误. 另外,当EF_PROTECT_FREE = 1时,在运行一段时间后,MergeServer的虚拟内存迅速膨胀到140 G,使得无法继续测试. 它再次陷入了死胡同. libsigsegvelectric-fence的神奇功能实际上是使用mprotect系统调用来实现的. mprotect的原型非常简单,intmprotect(const void * addr,size_t len,int prot); mprotect可以使内存的[addr,addr + len-1]节变得不可读,只读,读写和其他模式. 如果发生非法访问,程序将收到分段错误信号SIGSEGV. 但是mprotect
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-294454-1.html
建议先不升
中国的实体经济说到底就是你马云搞垮的
所以眼不见并不代表没有