在线客服:
亚博电子竞技 亚博电子竞技
全国服务热线:010-36808908
您的位置:首页 > 新闻中心 >

解决多线程内存问题的实践总结

浏览 145次 来源:【jake推荐】 作者:-=Jake=-    时间:2021-02-18 20:36:09
[摘要] 整个定位过程遇到的问题和解决办法对于多线程内存越界问题都很典型,简单总结一下和综合以上现象,初步判断这是一个多线程程序中内存越界的问题。至此,我们知道某个时间段内某个变量的内存被其他线程非法修改了,但是却无法定位到是哪个线程哪段代码。用来定位一类内存越界的问题。

最近,我在一个多线程服务器程序(OceanBaseMergeServer)中发现了一个问题,其中一个线程非法篡改了另一个线程的内存定位多线程内存越界问题实践总结 csdn,并导致程序核心掉线。找到这个问题

这个问题花了整整一周的时间,经历了曲折的过程,在此期间我尝试了各种内存调试方法。通常感觉天空的意志会变得清晰,但是他们发现自己已经进入了另一个死胡同。最后,使用

强大的工具,例如mprotect + backtrace + libsigsegv已成功找到问题所在。在整个定位过程中遇到的问题和解决方案通常是多线程内存超出范围的问题。简短摘要和

与所有人共享。对于仅对最终组合秘诀感兴趣的学生,请直接阅读最后一节。其他各章也写在这里供大众科学使用。

现象

核心是在系统集成测试期间发现的。服务器程序MergeServer具有一个由50个工作线程组成的线程池。当使用具有8个线程的测试程序时,它可以通过

通过MergeServer读取数据时,后者有时会成为核心。使用gdb检查核心文件,发现核心的原因是指针的地址非法。当进程访问指针所指向的地址时定位多线程内存越界问题实践总结 csdn,将导致分段。

错误(segmentfault)。见下文。

越界指针ptr_位于名为cname_的对象中,并且该对象是动态数组field_columns_的第十个元素的成员。如下所示。

重现问题

此后亚博体彩 ,花了2天的时间终于找到了重现该问题的方法。重复多次,可以观察到以下现象:

1.随着并发客户端数量的增加(从8个线程增加到16个线程),核心的可能性也随之增加;

2.减少服务器端线程池中的线程数(从50个减少到2个),并且无法复制内核。

3.被篡改的指针总是将一半(高4个字节)更改为0,而另一半似乎是正确的。

4.请参见上一节。重复多次。每次输出内核时,这是因为data_ [9]的cname_成员的ptr_成员(field_columns_动态数组的第十个元素)已被篡改。这是一个糟糕的解决方案

奇怪的现象解释了。

5.在代码中插入一个检查点,从最初生成的field_columns_内容到代码序列中的“埋入点”,使用二进制搜索来查找并越界,从而导致越界篡改cname _

的代码位

设置。事实证明,该程序有时核心位于检查点之前,有时核心位于检查点之后。

基于上述现象,初步判断这是一个多线程程序中的内存越界问题。

使用glibc的MALLOC_CHECK _

由于这是内存问题,请考虑使用一些内存调试工具来查找问题。由于OB具有自己的存储块缓存,因此需要消除其影响。修改OB内存分配器,

让它每次都直接调用malloc并释放c库而不进行缓存。然后,您可以使用glibc的内置内存块完整性检查功能。

使用此功能,无需重新编译程序,只需在运行时设置环境变量MALLOC_CHECK_(请注意下划线)。程序运行时都会释放可用内存

在glibc中,glibc将检查其隐藏的元数据的完整性,并在发现错误时立即终止。

使用类似于以下内容的命令行启动服务器程序:

exportMALLOC_CHECK_ = 2

bin / mergeserver-z45447-r1 0. 23 2. 3 6. 183:45401-p45441

使用MALLOC_CHECK_之后,程序核心转到了另一个位置。调用free时,glibc会检查内存块前面的check header错误并中止它。如下所示。

但是这个核心不能给我们带来太多信息。我们只是找到了另一种稍微更有效地重现该问题的方法。也许我最初看到的核心现象被延迟出现,但实际上“更多

早期,记忆被破坏了。

valgrind

glibc提供的MALLOC_CHECK_函数太简单了。是否有更高级的工具不仅可以报告错误,还可以分析问题的原因?我们自然想到了著名的

Valgrind。使用valgrind来检查内存问题,不需要重新编译程序,只需使用valgrind来启动:

nohupvalgrind--错误限制=否--suppressions = suppressbin / mergeserver-z45447-r1 0. 23 2. 3 6. 183:45401-p45441> nohup.out&

默认情况下,当valgrind发现1000个不同的错误或错误总数超过1000万时,它将停止报告错误。添加--error-limit = no后,可以禁用此功能。 -

抑制作用用于屏蔽一些无关紧要的误报。

经过反复折腾,valgrind无法再现核心问题。 valgrind报告的错误也是与问题无关的误报。可能是因为valgrind大约运行程序以提高程序的性能

它慢了10倍以上,这将影响多线程程序在运行时的计时,并且无法复制内核。这条路无处。

魔术数

由于MALLOC_CHECK_可以检测程序的内存问题,因此我们实际上想知道是谁(哪段代码)越过了线。此时,我们想到了使用magicnumber填充来标记数字

根据结构方法。如果我们在越界内存中看到某个幻数,那么我们知道是哪段代码。

首先,修改malloc的封装函数,使用特殊值(此处为0xEF)填充返回给用户的内存块yobo官网 ,并在开头和结尾处另外申请24个字节,并填写

作为特殊值计费(从0xBA开始,到0xDC结束)。另外,我们使用保留的内存块的头的后8个字节来存储当前线程的ID,以便一旦发现它超出范围,就可以基于此进行判断

肯定是哪个线程越过边界。代码示例如下。

然后,当用户程序通过自由条目释放内存时,它将检查我们填充到边界的幻数。同时,调用mprobe强制glibc对内存块执行完整性检查。

最后,将magicnumber添加到程序中所有可疑的键数据结构中,以便在调试器中检查内存时可以识别它们。例如

好的,一切都添加了。使用MALLOC_CHECK_重新运行。程序核心再次下降,检查内存在越界位置:

如上图所示,红色部分是我们自己填补的越界检查头,我们可以看到它没有损坏。确定存储在第二行中的线程号确实等于我们当前线程的线程号。

蓝色部分是上一个动态内存分配的结尾,该分配也已完成(24字节0xdc)。两行0x44afb60和0x44afb68中显示的内存是glibcmalloc存储自己的元数据的地方。

序列核心掉线的原因是它在检查两行的完整性时发现了一个错误。由此推断,非法篡改的内容小于16个字节。仔细观察这16个字节的内容,我们还不熟悉。

如果您知道magicnumber,就不可能用该错误来推断代码。我们最初发现的核心现象相互证实了这一点。非法修改的内容很可能只有4个字节(int32_t大

小)。

此外,尽管我们扩大了检查范围,但该程序仍将以glibcmalloc元数据为核心,而不是我们添加的范围。而且,我们总是可以观察到前面的内存(图片

末尾的

以蓝色显示)完整且完整。这表明这不仅仅是内存访问超出边界所导致的越界。我们可以做出大胆的猜测:要么其中一部已经发行

内存被非法重用;还是内存修改通过通配指针“空投”了。

如果我们的猜测是正确的,那么我们添加内存边界以检查内存问题的方法几乎肯定是无效的。

电子围栏,一种与怪物战斗的武器

这时,我们知道某个变量的内存在一定时间内已被其他线程非法修改,但是我们无法找到哪个线程和哪个代码段。就像您将来知道某个时候一样

段将在某个地方发生谋杀案,但无法看到凶手。很沮丧。

是否有办法检测到内存地址已被非法写入?有。另一个著名的内存调试库电子围栏(称为efence)在这里。

使用MALLOC_CHECK_或magicnumber进行检查的最大问题是此检查是“事后”。在复杂的多线程环境中,如果第一次无法损坏

检查现场,通常找不到罪魁祸首。

电子围栏使用底层硬件提供的机制(CPU提供的虚拟内存管理)来保护内存区域。实际上,它将在下一节中使用,我们必须自己编写代码

mprotect系统调用。修改受保护的内存后,程序将立即被内核化。通过检查核心文件的回溯,可以轻松找到问题代码。

该库的版本有点混乱,容易出错。在搜索和下载该库时,我发现电子围栏的作者也是著名的busybox的作者。原始作者的官员

Internet上的下载地址是。但是,此版本在编译并连接到Linux上的程序时将报告警告,并将在以后执行

也会有错误。后来,我找到了Debian提供的更高版本的库。据估计,社区已经对Linux进行了改进。我最终使用了2. 2.版本4

定位多线程内存越界问题实践总结 csdn_ios 数组越界问题_定位多线程内存越界问题实践总结 csdn

此:。

使用efence需要重新编译程序。编译后,efence提供了一个静态库libefence.a,其中包含一组可以替代glibc的malloc,free和其他库函数的实现。编译时必填

一些提示。首先,在编译命令行中将-lefence放在其他库之前;其次,使用-umalloc强制g ++从libefence查找最初包含在glibc中的malloc和其他库函数:

g ++-umalloc–lefence ...

使用字符串检查生成的程序是否确实使用了效率:

类似于许多工具,efence还通过设置环境变量来修改其运行时行为。通常,当程序越过边界访问内存时,efence会在每个内存块的末尾放置一个无法访问的页面

将检测块后面的存储器。如果设置了EF_PROTECT_BELOW = 1,则在内存块之前插入一个不可访问的页面。通常,efence仅检测分配的那些

内存块。分配一个块后,将在释放之后对其进行缓存,直到下一次分配它时,才会再次对其进行检查。如果设置了EF_PROTECT_FREE = 1,则不会释放所有可用内存

再次分配

,efence将检测释放的内存是否被非法使用(这是我们目前怀疑的位置)。但是由于不重用内存,因此内存可能会大大扩展。

我使用以上2个标记的4种组合来运行我们的程序。不幸的是,该问题无法重现,并且efence没有报告错误。此外,当EF_PROTECT_FREE = 1时,运行一段时间

此后,MergeServer的虚拟内存迅速膨胀到140G以上,使其无法继续测试。再次进入死胡同。

终极神器mprotect + backtrace + libsigsegv

电子围栏的神奇功能实际上是使用mprotect系统调用来实现的。 mprotect的原型非常简单,

intmprotect(constvoid * addr,size_tlen,intprot);

mprotect可以使内存的[addr,addr + len-1]节变得不可读,只读,可读和可写等。如果发生非法访问,程序将收到分段错误信号SIGSEGV。

但是mprotect有一个严格的限制,要求addr进行页面对齐,否则系统调用将返回错误EINVAL。此限制与操作系统内核的页面管理机制有关。

如图所示,我们已经知道该动态数组的第十个元素将被非法修改。查看代码后,我发现从数组内容的初始化到使用数组内容的时间

不应再进行任何修改。然后,我们可以在初始化数组内容之后立即调用mprotect,以使其免受只读攻击。

尝试一个

因为mprotect要求页面对齐输入内存地址,所以我修改了动态数组的实现。每次我申请一个内存块时,都会分配一个额外的页面大小,然后将页面对齐地址作为第一个。

起始位置

元素。

定位多线程内存越界问题实践总结 csdn_ios 数组越界问题_定位多线程内存越界问题实践总结 csdn

如上图所示,浅蓝色部分被填充以对齐内存地址。参见下面的代码

动态数组请求的最小内存块的大小为64KB。在这里,动态数组中每个元素的大小为80个字节,我们只需要保护第一个元素的页面大小即可:

由于此保护区是自动插入程序中的,因此在将内存释放给系统之前,需要将其恢复为可读写状态,否则由于mprotect不可避免地会发生分段错误。

好的,编译,重新启动并运行复制脚本。悲剧。该程序已经运行了很长时间,并且没有内核了,该问题无法重现。当我们分配动态数组内存时,我们添加

在内存块前面用于对齐

导致程序运行时的内存分配与产生内核的原始运行环境不同。这可能是无法复制的原因。为了重现,我们不能破坏原始的内存分配方法。

尝试两个

如何在不更改动态数组的内存块应用方法且不满足必须将mprotect保护的地址进行页面对齐的情况下执行此操作?让我们改变思维,从第十个要素开始yaboapp ,找到包含物

它与最接近页面的内存地址对齐。如下图所示

但这会引起问题。图片的浅蓝色部分不是此动态数组对象拥有的内存,任何其他线程的任何数据结构都可以使用它。我们使用这种方法

The

type保护红色区域亚博lol ,会有许多不相关的修改落入蓝色区域,这将导致mprotect生成分段错误。

经过实验,结果发现程序运行后不久,其他不相关的代码中出现了分段错误。此保护方法的代码如下:

成功

在上一节的保护模式下,我们保护无关的存储区,这将导致程序过早生成SIGSEGV并退出。我们能否拦截信号并防止程序被非法访问

在保护区域后,mprotect可以继续执行吗?当然。我们可以自定义SIGSEGV段错误信号处理功能。在此处理功能中,如果您可以在以下情况下打印细分错误

在调用堆栈之前,您可以找到罪魁祸首。

代码如上所示。请注意,在处理SIGSEGV的处理函数时有一些技巧(很多陷阱):

定位多线程内存越界问题实践总结 csdn_定位多线程内存越界问题实践总结 csdn_ios 数组越界问题

1. SIGSEGV通常由内核处理(页面错误)。使用libsigsegv库可以简化在用户空间中编写处理函数的难度。

在2.处理函数中,您无法调用任何可能重新分配内存的函数,否则将导致双重故障。例如,在此处理功能中,使用open系统调用打开文件,而您不能使用

使用fopen; buff是从堆栈分配的,不能从堆中申请; backtrace_symbols不能使用,它将动态地从glibc申请内存,但是要使用安全性

完整的backtrace_symbols_fd直接将backtrace写入文件。

3.最重要的是,在SIGSEGV的处理功能中,我们需要恢复导致分段错误可读写的存储块。这样,当处理函数返回中断的代码以继续执行时,就无法重复

造成段错误。

重新编译代码并运行复制脚本。查看记录回溯的文件sigsegv.bt,我们看到了被篡改的熟悉的指针地址(其中一半是0):

此段错误最终将导致程序被内核化,因为mprotect的保护未生成SIGSEGV信号。检查核心文件,可以找出内存(即ptr _)

的地址。从sigsegv.bt文件中搜索,我发现了非法访问:

使用addr2line检查上面的调用堆栈中的地址,我们终于找到了它。在再次检查和验证代码之后,最终确定了错误原因。有新动态出现

对象的指针在两个相关线程之间共享。在某些极端情况下,其中一个删除对象后,另一个线程会修改该对象。

摘要

总而言之,如果遇到困难的内存越界问题,可以按以下顺序逐一尝试:

1. codereview分析代码。

2. valgrind是最容易使用的,几乎是愚蠢的。尽可能多地使用。

3. glibc的MALLOC_CHECK_非常易于使用,不需要复制已编译的代码。它可以用于发现问题,但不能定位问题本身。结合magicnumber,您可以

用于定位一种类型的内存越界问题。

4.和电子围栏还具有称为dmalloc的内存调试库。尽管未在解决问题的过程中使用此库,但此库对于检测内存泄漏和其他问题非常有用。推送

我建议每个人都进行研究并将其放入您自己的工具库中。

5.电子围栏是用于定位“野生指针”访问问题类型的强大工具,因此强烈建议使用。

6.如果上述工具都不能帮助您,那么您必须基于对代码逻辑的熟悉程度使用最终武器。

7. codereview。通过尝试从代码库中从不同版本编译的程序来重现错误,并使用二分法查找最早引入错误的代码提交。

除非您要创建性能特别苛刻的低级系统,或者在项目中使用Java,否则必须强制C ++程序员警告新手。

来自:

老王
本文标签:多线程,编译程序,valgrind

推荐阅读

最新评论