调试内存损坏


23

首先,我确实意识到这不是一个具有绝对答案的完美的问与答风格的问题,但是我想不出任何措辞来使它更好地工作。我认为没有绝对的解决方案,这就是为什么我将其发布在此处而不是Stack Overflow的原因之一。

在过去的一个月中,我一直在重写相当旧的服务器代码(mmorpg),以使其更加现代,并且易于扩展/修改。我从网络部分开始,并实现了一个第三方库(libevent)来为我处理事务。通过所有的重构和代码更改,我在某个地方引入了内存损坏,而我一直在努力寻找发生错误的地方。

我似乎无法在我的开发/测试环境中可靠地重现它,即使实现原始机器人来模拟某些负载时,我也不会再崩溃(我修复了会导致某些问题的libevent问题)

到目前为止,我已经尝试过:

摆脱困境-直到事情崩溃(可能需要1天以上的生产时间或仅仅一个小时)崩溃之前,我才真正感到困惑,这之前肯定没有无效的写入,肯定在某个时候它将访问无效的内存并且不会覆盖内容机会?(是否可以“扩展”地址范围?)

代码分析工具,即coverage和cppcheck。尽管他们确实指出了代码中的一些.nastiness和边缘情况,但没有什么严重的。

记录该过程,直到它与gdb崩溃(通过undodb),然后以反向方式工作。/ sounds /这样的声音应该是可行的,但是我要么通过使用自动完成功能最终导致gdb崩溃,要么由于一些可能的分支(一个损坏导致另一个损坏,因此导致内部迷失)在内部libevent结构中丢失上)。我想如果能看到指针最初属于/分配指针的地方,那将消除大多数分支问题,那就太好了。但是我无法使用undodb运行valgrind,而且我正常的gdb记录的速度实在太慢了(如果甚至可以与valgrind结合使用)。

代码审查!我自己(彻底)并让一些朋友来检查我的代码,尽管我怀疑它是否足够彻底。我当时在考虑也许要雇用一名开发人员与我一起进行一些代码审查/调试,但是我付不起太多钱,而且我也不知道该去哪里寻找愿意为小工作而工作的人-如果他没有找到问题或根本没有资格,那就不要钱。

我还应该指出:我通常会得到一致的回溯。在某些地方发生崩溃,主要与套接字类以某种方式损坏有关。它是指向不是套接字的东西的无效指针,还是套接字类本身被乱码覆盖(部分?)。尽管我怀疑它在那里崩溃最多,因为那是最常用的部分之一,因此它是第一个被使用的损坏的内存。

总而言之,这个问题使我忙了将近2个月(无论是开还是关,都是一个业余项目),这确实让我感到沮丧,以至于我变得脾气暴躁,并想放弃。我只是想不出我应该怎么做才能发现问题。

我错过了任何有用的技术吗?你怎么处理那件事呢?(这可能不那么普遍,因为没有太多相关信息。或者我真的是盲人?)

编辑:

一些重要的规格:

通过gcc 4.7使用c ++(11)(版本由debian wheezy提供)

代码库大约有15万行

编辑以回复david.pfx帖子:(抱歉响应缓慢)

您是否在仔细记录崩溃情况以寻找模式?

是的,我仍然遗漏了最近发生的崩溃事件

几个地方真的很相似吗?用什么方式?

好吧,在最新版本中(每当我添加/删除代码或更改相关结构时,它们似乎都会改变),它总是会陷入项目计时器方法中。基本上,一个项目有一个特定的时间,在该时间之后,它会过期,并将更新的信息发送给客户端。无效的套接字指针将在Player类中(据我所知仍然有效),大部分与此有关。在正常关闭后,我还将在清理阶段遇到大量崩溃,该崩溃将破坏所有未明确破坏的静态类(__run_exit_handlers在回溯中)。大多数情况下只涉及std::map一个类,但猜测这只是第一件事。

损坏的数据是什么样的?零?Ascii?模式?

我还没有找到任何模式,对我来说似乎有点随机。很难说,因为我不知道腐败从哪里开始。

它与堆有关吗?

这完全与堆有关(我启用了gcc的堆栈保护,但没有捕获任何东西)。

腐败发生在a之后free()吗?

您将不得不对此进行详细说明。您是说要让已经释放的对象指向周围吗?一旦对象被销毁,我会将每个引用都设置为null,所以除非我在某处错过了东西,否则不行。那应该在valgrind中显示出来,但是没有。

网络流量是否有与众不同的东西(缓冲区大小,恢复周期)?

网络流量由原始数据组成。因此,对于更复杂的事物,使用char数组,(u)intX_t或packed(以除去填充)结构,每个数据包都有一个标头,该标头由id和数据包大小本身组成,并针对预期大小进行了验证。它们大约为10-60字节,最大的(内部“启动”数据包,在启动时触发一次)大小为几Mb。

大量的生产断言。在损坏蔓延之前及早地发生崩溃。

我曾经有一次与std::map腐败相关的崩溃,每个实体都有其“视图”的地图,每个实体都可以看到它,反之亦然。我在前面和后面添加了200byte的缓冲区,将其填充为0x33,并在每次访问之前对其进行了检查。腐败刚刚消失了,我必须搬走一些东西使它腐败了。

战略性日志记录,因此您可以准确地知道之前发生了什么。当您更接近答案时,将其添加到日志记录中。

它的工作..扩展。

无奈之下,您可以保存状态并自动重启吗?我可以想到一些实现此目的的生产软件。

我有点做。该软件由一个主要的“高速缓存”进程和一些其他工作进程组成,它们均访问高速缓存以获取并保存内容。因此,每次崩溃我都不会失去太多进展,它仍然会断开所有用户的连接,依此类推,这绝对不是解决方案。

并发:线程,竞争条件等

有一个mysql线程可以执行“异步”查询,尽管这一切都未曾动过,并且仅通过具有所有锁定功能的函数才与数据库类共享信息。

中断

有一个中断计时器可以阻止它锁定,如果它在30秒内没有完成一个周期,它只会中止运行,但是该代码应该是安全的:

if (!tics) {
    abort();
} else
    tics = 0;

volatile int tics = 0;每次循环完成时增加tic 。也是旧代码。

事件/回调/异常:意外损坏状态或堆栈

正在使用许多回调(异步网络I / O,计时器),但它们不应做任何不好的事情。

异常数据:异常输入数据/时序/状态

我有一些与此相关的案例。在仍在处理数据包时断开套接字会导致访问nullptr等,但是到目前为止,这些值很容易发现,因为在告诉类本身完成后立即清除了每个引用。(破坏本身由循环处理,每个循环删除所有被破坏的对象)

对异步外部过程的依赖。

关心详细吗?上面提到的高速缓存过程就是这种情况。我唯一能想到的就是无法足够快地完成工作并使用垃圾数据,但是事实并非如此,因为那也使用了网络。相同的数据包模型。


7
可悲的是,这在非平凡的C ++应用程序中很常见。如果您使用的是源代码管理,则测试各种变更集以缩小导致问题的代码变更可能会有所帮助,但在这种情况下可能不可行。
Telastyn 2014年

是的,在我看来,这确实是不可行的。我基本上从工作到彻底彻底中断了2个月,然后进入调试阶段,那里有一些可以工作的代码。旧系统确实不允许我在不破坏所有内容的情况下实现一种新的灵活网络代码。
罗宾

2
此时,您可能必须尝试隔离每个部分。采取解决方案的每个类/子集,围绕它进行模拟,以便它可以起作用,并测试其中存在的问题,直到找到失败的部分。
2014年

首先注释掉部分代码,直到不再崩溃为止。
cpp81

1
除了Valgrind,Coverity和cppcheck外,您还应将Asan和UBsan添加到您的测试方案中。如果您的代码是corss-platofrm,则还要添加Microsoft的Enterprise Analysis(/analyze)以及Apple的Malloc和Scribble保护器。您还应该使用尽可能多的编译器,并使用尽可能多的标准,因为编译器警告是一种诊断,随着时间的推移它们会变得越来越好。没有灵丹妙药,而且一种尺寸并不适合所有人。您使用的工具和编译器越多,覆盖面就越完整,因为每种工具都有其优点和缺点。

Answers:


21

这是一个具有挑战性的问题,但我怀疑在您已经看到的崩溃中可以找到更多线索。

  • 您是否在仔细记录崩溃情况以寻找模式?
  • 几个地方真的很相似吗?用什么方式?
  • 损坏的数据是什么样的?零?Ascii?模式?
  • 是否涉及任何多线程?可能是比赛条件吗?
  • 它与堆有关吗?腐败会在free()之后发生吗?
  • 它与堆栈有关吗?堆栈是否损坏?
  • 悬挂参考是可能的吗?神秘变化的数据值?
  • 网络流量是否有与众不同的东西(缓冲区大小,恢复周期)?

我们在类似情况下使用过的东西。

  • 大量的生产断言。在损坏蔓延之前及早地发生崩溃。
  • 很多很多的警卫。局部变量,对象和mallocs()之前和之后的多余数据项设置为一个值,然后经常检查。
  • 战略性日志记录,因此您可以准确地知道之前发生了什么。当您更接近答案时,将其添加到日志记录中。

无奈之下,您可以保存状态并自动重启吗?我可以想到一些实现此目的的生产软件。

如果我们可以提供帮助,请随时添加详细信息。


我能补充一下,这样的不确定性错误并不是很常见,并且没有很多东西(通常)会导致它们。它们包括:

  • 并发:线程,竞争条件等
  • 中断/事件/回调/异常:不可预期地破坏状态或堆栈
  • 异常数据:异常输入数据/时序/状态
  • 对异步外部过程的依赖。

这些是代码中要关注的部分。


+1所有好的建议,尤其是断言,警卫和记录。
andy256

为了回应您的回答,我在问题中编辑了更多信息。实际上,这让我想到了关闭时尚未完全查看的崩溃,所以我想现在就继续讨论。
罗宾

5

使用malloc / free的调试版本。包装它们,并在需要时编写自己的包装。很有意思!

我使用的版本在每次分配之前和之后都添加了保护字节,并维护了一个“已分配”列表,可以自由检查释放的块。这捕获了大多数缓冲区溢出和多个或恶意的“空闲”错误。

最隐蔽的腐败根源之一是在释放大块后继续使用它。Free应该以已知的模式(传统上为0xDEADBEEF)填充释放的内存,这有助于分配的结构包含“幻数”元素,并在使用结构之前自由地包含对适当幻数的检查。


1
Valgrind应该获得两次免费/使用免费数据,不是吗?
罗宾2014年

为new / delete编写这种重载已帮助我找到了许多内存损坏问题。特别是在删除后经过验证的保护字节,并导致程序触发了断点,该断点自动将我带入调试器。
艾米丽·

3

为了解释您在问题中所说的话,不可能给出明确的答案。我们能做的最好的事情就是建议要寻找的东西以及工具和技术。

有些建议会显得幼稚,其他的建议可能会更适用,但希望有人会引发您可以跟进的想法。我必须说david.pfx答案有合理的建议。

从症状

  • 对我来说,这听起来像是缓冲区溢出。

  • 一个相关的问题是使用未经验证的套接字数据作为下标或键等。

  • 您是否有可能在某个地方使用了全局变量,或者使用了具有相同名称的全局变量和局部变量,还是某个玩家的数据干扰了另一个玩家的数据?

与许多错误一样,您可能在某处做出了无效的假设。或可能不止一个。很难检测到多个相互作用的错误。

  • 每个变量都有描述吗?并且可以定义有效性声明吗?
    如果未添加,请浏览代码以查看每个变量是否正确使用。在合理的地方添加该断言。

  • 添加很多断言的建议是一个很好的建议:将它们放在首位的是每个函数入口点。验证参数和任何相关的全局状态。

  • 我使用大量日志记录来调试长时间运行的/异步/实时代码。
    同样,在每个函数调用上插入日志写入。
    如果日志文件太大,则日志记录功能可以包装/切换文件/等。
    如果日志消息以函数调用深度为缩进量,这将非常有用。
    日志文件可以显示错误的传播方式。当一段代码执行某项不完全正确的事情而成为延迟动作炸弹时,此命令很有用。

许多人都有自己的本地记录代码。我在某处有旧的C宏日志系统,也许还有C ++版本...


3

其他答案中所说的一切都很相关。ddyer部分提到的一件事是包装malloc / free有好处。他提到了一些,但我想在此添加一个非常重要的调试工具:您可以将每个malloc / free以及几行调用栈(如果需要的话,也可以是完整的调用栈)登录到外部文件中。如果您小心一点,则可以轻松快速地完成此操作,并在生产中使用它。

根据您的描述,我个人的猜测是,您可能在某个指针的引用指向释放内存的位置,并且最终可能会释放不再属于您的指针或对其进行写入。如果您可以推断出使用上述技术进行监视的大小范围,则应该能够大大缩小日志记录的范围。否则,一旦找到什么内存已损坏,就可以从日志中很容易地找出导致该内存的malloc / free模式。

重要说明是,正如您提到的,更改内存布局可能会掩盖问题。因此,非常重要的是,您的日志记录不分配(如果可以!)或分配尽可能少的分配。如果与内存相关,这将有助于重现性。如果问题是与多线程相关的,它也将尽可能快地提供帮助。

捕获来自第三方库的分配也很重要,这样也可以正确记录它们。您永远不知道它可能来自哪里。

最后一种选择是,您还可以创建一个自定义分配器,在其中为每个分配分配至少2个页面,并在空闲时取消映射(将分配对齐到页面边界,在之前分配页面并将其标记为不可访问或对齐)。在页面末尾分配,然后在和标记为不可访问之后分配页面)。确保至少在一段时间内不要将这些虚拟内存地址重新用于新分配。这意味着您需要自己管理虚拟内存(保留并根据需要使用它)。请注意,这将降低性能,并且最终可能会使用大量的虚拟内存,具体取决于您为它分配的分配数量。为了减轻这种情况,如果可以以64位运行和/或减少需要分配的范围(基于大小),这将有所帮助。Valgrind可能已经很好地做到了这一点,但是对于您来说,抓住它的问题可能太慢了。仅对一些大小或对象执行此操作(如果知道,则只能对那些对象使用特殊的分配器),以确保对性能的影响最小。


0

尝试在崩溃的内存地址上设置监视点。GDB将在导致无效内存的指令处中断。然后使用回溯,您可以看到导致损坏的代码。这可能不是造成腐败的根源,但是对每个腐败行为重复监视点可能会导致问题的根源。

顺便说一句,由于该问题被标记为C ++,请考虑使用共享指针,该共享指针通过维护引用计数来维护所有权,并在指针超出范围后安全删除内存。但是请谨慎使用它们,因为它们可能在罕见的循环依赖关系使用中导致死锁。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.