linux下定位多线程内存越界问题实践总结( 三 )


g++ -umalloc –lefence …用strings来检查产生的程序是否真的使用了efence:
linux下定位多线程内存越界问题实践总结文章插图
和很多工具类似 , efence也通过设置环境变量来修改它运行时的行为 。 通常 , efence在每个内存块的结尾放置一个不可访问的页 , 当程序越界访问内存块后面的内存时 , 就会被检测到 。 如果设置EF_PROTECT_BELOW=1 , 则是在内存块前插入一个不可访问的页 。 通常情况下 , efence只检测被分配出去的内存块 , 一个块被分配出去后free以后会缓存下来 , 直到一下次分配出去才会再次被检测 。 而如果设置了EF_PROTECT_FREE=1 , 所有被free的内存都不会被再次分配出去 , efence会检测这些被释放的内存是否被非法使用(这正是我们目前怀疑的地方) 。 但因为不重用内存 , 内存可能会膨胀地很厉害 。
我使用上面2个标记的4种组合运行我们的程序 , 遗憾的是 , 问题无法复现 , efence没有报错 。 另外 , 当EF_PROTECT_FREE=1时 , 运行一段时间后 , MergeServer的虚拟内存很快膨胀到140多G , 导致无法继续测试下去 。 又进入了一个死胡同 。
终极神器mprotect + backtrace + libsigsegvelectric-fence的神奇能力实际上是使用系统调用mprotect实现的 。 mprotect的原型很简单 ,
int mprotect(const void *addr, size_t len, int prot);mprotect可以使得[addr,addr+len-1]这段内存变成不可读写 , 只读 , 可读写等模式 , 如果发生了非法访问 , 程序会收到段错误信号SIGSEGV 。
但mprotect有一个很强的限制 , 要求addr是页对齐的 , 否则系统调用返回错误EINVAL 。 这个限制和操作系统内核的页管理机制相关 。
linux下定位多线程内存越界问题实践总结文章插图
如图 , 我们已经知道这个动态数组的第10个元素会被非法越界修改 。 review了代码 , 发现从这个数组内容初始化完毕以后 , 到使用这个数组内容这段时间 , 不应该再有修改操作 。 那么 , 我们就可以在数组内容被初始化之后 , 立即调用mprotect对其进行只读保护 。
尝试一因为mprotect要求输入的内存地址页对齐 , 所以我修改了动态数组的实现 , 每次申请内存块的时候多分配一个页大小 , 然后取页对齐的地址为第一个元素的起始位置 。
linux下定位多线程内存越界问题实践总结文章插图
如上图 , 浅蓝色部分为为了对齐内存地址而做的padding 。 代码见下
linux下定位多线程内存越界问题实践总结文章插图
动态数组申请的最小内存块的大小为64KB 。 这里 , 动态数组中每个元素的大小为80字节 , 我们只需要从第1个元素开始保护一个页的大小即可:
linux下定位多线程内存越界问题实践总结文章插图
既然这个保护区域是程序中自动插入的 , 需要在内存释放给系统前回复它为可读写 , 否则必然会因mprotect产生段错误 。
linux下定位多线程内存越界问题实践总结文章插图
好了 , 编译、重启、运行重现脚本 。 悲剧了 。 程序运行了很久都不再出core了 , 无法复现问题 。 我们在分配动态数组内存时 , 为了对齐在内存块前添加的padding导致程序运行时的内存分布和原来产生core的运行环境不同了 。 这可能是无法复现的原因 。 要想复现 , 我们不能破坏原来的内存分配方式 。
尝试二不改变动态数组的内存块申请方式 , 又要满足mprotect保护的地址必须页对齐的要求 , 怎么做呢?我们换一个思路 , 从第10个元素向前 , 找到包含它且离它最近的页对齐的内存地址 。 如下图
linux下定位多线程内存越界问题实践总结文章插图
但这样会造成一个问题 。 图中浅蓝色部分本不是这个动态数组对象所拥有的内存 , 它可能被其他任何线程的任何数据结构在使用 。 我们使用这种方式保护红色区域 , 会有很多无关的落入蓝色区域的修改操作导致mprotect产生段错误 。
实验了一下 , 果然 , 程序跑起来不久就在其他无关的代码处产生了段错误 。 这种保护方式的代码如下:
linux下定位多线程内存越界问题实践总结文章插图
成功在上一节的保护方式下 , 我们因为保护了无关内存区域 , 会导致程序过早产生SIGSEGV而退出 。 我们能否截获信号 , 不让程序在非法访问mprotect保护区域后仍然能继续执行呢?当然 。 我们可以定制一个SIGSEGV段错误信号的处理函数 。 在这个处理函数中 , 如果能打印段错误时候的当前调用栈 , 就可以找到罪魁祸首了 。