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

深入祭坛的内存调试器-找出多线程内存问题的实践总结

浏览 99次 来源:【jake推荐】 作者:-=Jake=-    时间:2021-02-19 13:34:00
[摘要] 最近定位了在一个多线程服务器程序(OceanBaseMergeServer)中,一个线程非法篡改另一个线程的内存而导致程序core掉的问题。整个定位过程遇到的问题和解决办法对于多线程内存越界问题都很典型,简单总结一下和大家分享。综合以上现象,初步判断这是一个多线程程序中内存越界的问题。

最近,我在一个多线程服务器程序(OceanBaseMergeServer)中发现了一个问题yabo网页登入 ,其中一个线程非法篡改了另一个线程的内存,并导致程序核心掉线。找到整个问题花了整整一周的时间。在此期间,我尝试了各种内存调试方法。通常感觉天空的意志会变得清晰亚博直播 ,但是他们发现自己已经进入了另一个死胡同。最后,使用功能强大的工具(例如mprotect + backtrace + libsigsegv)成功定位问题。在整个定位过程中遇到的问题和解决方案是多线程内存跨边界问题的典型问题,我将简要总结并与您分享。对于仅对最终组合秘诀感兴趣的学生,请直接阅读最后一节。其他各章也写在这里供大众科学使用。

现象

核心是在系统集成测试期间发现的。服务器程序MergeServer具有一个由50个工作线程组成的线程池。当使用8个线程的测试程序通过MergeServer读取数据时,后者有时会成为核心。使用gdb检查核心文件,发现核心的原因是指针的地址非法,并且当进程访问指针所指向的地址时导致了段错误(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的内置内存块完整性检查功能。

数组越界问题_c内存越界检测工具_定位多线程内存越界问题实践总结 csdn

使用此功能,无需重新编译程序,只需在运行时设置环境变量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来启动:

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

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

经过反复折腾,valgrind无法再现核心问题。 valgrind报告的错误也是与问题无关的误报。可能是因为运行程序的valgrind会使程序的性能降低10倍以上,这将影响多线程程序的运行时间,从而导致内核无法重现。这条路无处。

魔术数

由于MALLOC_CHECK_可以检测程序的内存问题,因此我们实际上想知道是谁(哪段代码)越过了线。在这一点上,我们考虑使用magicnumber填充来指示数据结构。如果我们在越界内存中看到某个幻数,那么我们知道是哪段代码。

首先,修改malloc的封装函数,使用特殊值(此处为0xEF)填充返回给用户的内存块,并在开头和结尾处申请额外的24个字节,并将其填充具有特殊值(从0xBA开始,从0xDC结束)。此外,我们使用保留内存块的标头的后8个字节来存储当前线程的ID,以便一旦观察到它超出范围,就可以确定哪个线程超出范围。代码示例如下。

数组越界问题_定位多线程内存越界问题实践总结 csdn_c内存越界检测工具

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

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

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

如上图所示,红色部分是我们自己填补的越界检查头,我们可以看到它没有损坏。确定存储在第二行中的线程号确实等于我们当前线程的线程号。蓝色部分是前一个动态内存分配的结尾,该分配也已完成(24字节0xdc)。两行0x44afb60和0x44afb68中显示的内存是glibcmalloc存储自己的元数据的地方。程序核心掉线的原因是它在检查这两行的完整性时发现了一个错误。由此推断,非法篡改的内容小于16个字节。仔细观察这16个字节的内容,我们看不到熟悉的magicnumber,因此无法推断出带有错误的代码。我们最初发现的核心现象相互证实了这一点。非法修改的内容可能只有4个字节(int32_t大小)。

此外,尽管我们扩大了检查范围,但该程序仍将以glibcmalloc元数据为核心,而不是我们添加的范围。此外,我们始终可以观察到前一个内存块的末尾(图中蓝色显示)是完整的,没有被破坏。这表明这不仅仅是内存访问超出边界所导致的越界。我们可以大胆地猜测:要么已释放的一段内存被非法重用;要么被释放。还是这是通过野指针“空投”的内存修改。

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

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

这时,我们知道某个变量的内存在一定时间内已被其他线程非法修改,但是我们无法找到哪个线程和哪个代码段。就像您知道将来会有谋杀案发生在某个特定时间,但是您看不到凶手。很沮丧。

是否有办法检测到内存地址已被非法写入?有。另一个著名的内存调试库电子围栏(称为efence)在这里。使用MALLOC_CHECK_或magicnumber进行检测的最大问题是此检查是“事后”。在多线程的复杂环境中,如果您在无法发生损坏的情况下立即检查现场,通常将无法找到罪魁祸首的线索。

电子围栏使用底层硬件提供的机制(CPU提供的虚拟内存管理)来保护内存区域。实际上,它使用了mprotect系统调用,我们将在下一部分中对其进行编码。修改受保护的内存后,程序将立即被内核化。通过检查核心文件的回溯,可以轻松找到问题代码。

该库的版本有点混乱,容易出错。在搜索和下载该库时,我发现电子围栏的作者也是著名的busybox的作者。原始作者的官方网站上的下载地址为。但是,此版本在编译并连接到Linux上的程序时将报告“警告”,并且在以后执行时也会出错。后来,我找到了Debian提供的更高版本的库。据估计,社区已经对Linux进行了改进。我最终使用了此2. 2. 4版本:。

使用efence需要重新编译程序。编译后,efence提供了一个静态库libefence.a,其中包含一组可以替代glibc的malloc定位多线程内存越界问题实践总结 csdn,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要求页面对齐输入内存地址,所以我修改了动态数组的实现。每次我申请一个内存块时,都会分配一个额外的页面大小亚博电子竞技 ,然后将页面对齐的地址作为第一个元素的起始位置。

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

动态数组请求的最小内存块的大小为64KB。在这里定位多线程内存越界问题实践总结 csdn,动态数组中每个元素的大小为80个字节,我们只需要保护第一个元素的页面大小即可:

定位多线程内存越界问题实践总结 csdn_c内存越界检测工具_数组越界问题

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

好的,编译,重新启动并运行复制脚本。悲剧。该程序已经运行了很长时间,并且没有内核了,该问题无法重现。当我们分配动态数组内存时,为了对齐添加在内存块前面的填充,运行时程序的内存分配与内核的原始操作环境不同。这可能是无法复制的原因。为了重现,我们不能破坏原始的内存分配方法。

尝试两个

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

但这会引起问题。图片的浅蓝色部分不是此动态数组对象拥有的内存,任何其他线程的任何数据结构都可以使用它。我们使用这种方法来保护红色区域,并且会有许多无关的修改落入蓝色区域,这将导致mprotect生成分段错误。

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

成功

在上一节的保护模式下,我们保护无关的存储区,这将导致程序过早生成SIGSEGV并退出。在非法访问mprotect保护区后,我们可以拦截信号并阻止程序继续执行吗?当然。我们可以自定义SIGSEGV段错误信号处理功能。在此处理功能中,如果您可以在出现分段错误时打印当前的调用堆栈,则可以找到罪魁祸首。

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

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

c内存越界检测工具_数组越界问题_定位多线程内存越界问题实践总结 csdn

在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 ++程序员警告新手。

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

推荐阅读

最新评论