【问题标题】:Of Memory Management, Heap Corruption, and C++内存管理、堆损坏和 C++
【发布时间】:2010-09-05 16:06:25
【问题描述】:

所以,我需要一些帮助。我正在使用 C++ 开发一个项目。但是,我想我已经设法破坏了我的堆。这是基于我向一个类添加了一个std::string 并从另一个std::string 为其分配一个值的事实:

std::string hello = "Hello, world.\n";
/* exampleString = "Hello, world.\n" would work fine. */
exampleString = hello;

我的系统因堆栈转储而崩溃。所以基本上我需要停止并检查我所有的代码和内存管理的东西,找出我搞砸的地方。代码库仍然很小(大约 1000 行),所以这很容易实现。

不过,我对这种东西感到头疼,所以我想我会把它扔在那里。我在 Linux 系统上使用valgrind,虽然不完全了解我在做什么,但它确实报告了std::string 的析构函数是无效的。我不得不承认从谷歌搜索中得到了“堆损坏”这个词;任何关于这类东西的通用文章也将不胜感激。

(在rm -rf ProjectDir 之前,在 C# 中再做一次:D)

编辑: 我还没有说清楚,但我要求的是诊断这类记忆问题的方法。我知道 std::string 的东西是正确的,所以这是我做过的事情(或者是一个错误,但 Select 没有问题)。我确信我可以检查我编写的代码,你们非常聪明的人很快就会发现问题,但我想将这种代码分析添加到我的“工具箱”中。

【问题讨论】:

    标签: c++ memory stack heap-memory


    【解决方案1】:

    这些是可能解决问题的相对便宜的机制:

    1. 请密切关注我的heap corruption question - 我正在更新答案。第一个是平衡new[]delete[],但你已经这样做了。
    2. valgrind更多的机会;这是一个很好的工具,我只希望它在 Windows 下可用。我只将您的程序减慢了大约一半,这与 Windows 等效程序相比已经相当不错了。
    3. 考虑使用 Google Performance Tools 作为替代 malloc/new。
    4. 您是否已清除所有目标文件并重新开始?也许您的 make 文件...“次优”
    5. 您的代码中assert()ing 不够。没看过怎么知道?就像使用牙线一样,assert()s 在他们的代码中还不够。为您的对象添加一个验证函数,并在方法开始和方法结束时调用它。
    6. 你是compiling -wall吗?如果没有,请这样做。
    7. 给自己找一个 lint 工具,比如PC-Lint。像您这样的小应用可能适合PC-lint demo 页面,这意味着您无需购买!
    8. 检查您是否在删除指针后将其清空。没有人喜欢悬空指针。具有已声明但未分配的指针的相同演出。
    9. 停止使用数组。请改用vector
    10. 不要使用原始指针。使用smart pointer。不要使用auto_ptr!那东西……令人惊讶;它的语义很奇怪。相反,请选择Boost smart pointers 之一,或从the Loki library 中选择一个。

    【讨论】:

    • +1,好名单!但是,我对 #8 提出异议 - 虽然它可以防止“错误”访问,但它实际上是一种代码味道,在我的经验中隐藏了糟糕的逻辑或糟糕的对象生命周期管理......
    • 如今,C++ 在标准库中有自己的智能指针,因此不需要 Boost 或 Loki。
    【解决方案2】:

    我们曾经遇到过一个错误,它避开了所有常规技术、valgrind、purify 等。崩溃只发生在具有大量内存的机器上,并且只发生在大型输入数据集上。

    最终我们使用调试器观察点对其进行了跟踪。我将尝试在此处描述该过程:

    1) 找出失败的原因。从您的示例代码中可以看出,“exampleString”的内存已损坏,因此无法写入。让我们继续这个假设。

    2) 在使用或修改“exampleString”没有任何问题的最后一个已知位置设置断点。

    3) 为“exampleString”的数据成员添加一个观察点。使用我的 g++ 版本,字符串存储在 _M_dataplus._M_p 中。我们想知道这个数据成员什么时候改变。用于此的 GDB 技术是:

    (gdb) p &exampleString._M_dataplus._M_p
    $3 = (char **) 0xbfccc2d8
    (gdb)  watch *$3
    Hardware watchpoint 1: *$3
    

    我显然在这里使用带有 g++ 和 gdb 的 linux,但我相信大多数调试器都可以使用内存观察点。

    4) 继续直到触发观察点:

    Continuing.
    Hardware watchpoint 2: *$3
    
    Old value = 0xb7ec2604 ""
    New value = 0x804a014 ""
    0xb7e70a1c in std::string::_M_mutate () from /usr/lib/libstdc++.so.6
    (gdb) where
    

    gdb where 命令将给出一个回溯,显示导致修改的原因。这是一个完全合法的修改,在这种情况下继续 - 或者如果你很幸运,这将是由于内存损坏而导致的修改。在后一种情况下,您现在应该能够查看真正导致问题的代码,并希望能够修复它。

    我们的错误的原因是带有负索引的数组访问。索引是指向“int”的指针以数组大小为模的结果。 valgrind 等人错过了这个错误。因为在这些工具下运行时分配的内存地址永远不会是“> MAX_INT”,因此永远不会导致负索引。

    【讨论】:

    • Linux 的精彩讨论!怀念在那种环境下发展。我自己需要一个 WinDoze 的解决方案...(也是 VS6.0)...(不是我的错!客户使用 VS6.0,客户永远是对的 :)。
    【解决方案3】:

    哦,如果您想知道如何调试问题,那很简单。首先,得到一只死鸡。然后,start shaking it

    说真的,我还没有找到一种一致的方法来追踪这些类型的错误。因为有很多潜在的问题,所以没有一个简单的清单可以通过。但是,我会推荐以下内容:

    1. 熟悉调试器。
    2. 开始在调试器中四处游荡,看看是否能找到任何看起来可疑的东西。尤其要查看exampleString = hello; 行期间发生的情况。
    3. 检查以确保它确实在 exampleString = hello; 行上崩溃,而不是在退出某些封闭块时(这可能导致析构函数触发)。
    4. 检查您可能正在做的任何指针魔术。指针算术、强制转换等。
    5. 检查所有分配和解除分配以确保它们匹配(没有重复分配)。
    6. 确保您没有返回任何指向堆栈上对象的引用或指针。

    还有很多其他的东西可以尝试。我相信其他人也会提出想法。

    【讨论】:

      【解决方案4】:

      一些开始的地方:

      如果你在 Windows 上,并且使用 Visual C++6(我希望上帝这些天还没有人使用它),那么 std::string 的实现不是线程安全的,并且可能导致这种事情。

      Here's an article I found which explains a lot of the common causes of memory leaks and corruption.

      在我以前的工作场所,我们使用 Compuware Boundschecker 来帮助解决这个问题。它是商业化的并且非常昂贵,因此可能不是一个选择。

      这里有几个可能有用的免费库

      http://www.codeguru.com/cpp/misc/misc/memory/article.php/c3745/

      http://www.codeproject.com/KB/cpp/MemLeakDetect.aspx

      希望对您有所帮助。内存损坏是一个糟糕的地方!

      【讨论】:

        【解决方案5】:

        可能是堆损坏,但也很可能是堆栈损坏。吉姆是对的。我们真的需要更多的上下文。这两行来源并不能单独告诉我们太多。导致这种情况的原因可能有很多(这是 C/C++ 的真正乐趣)。

        如果您愿意发布您的代码,您甚至可以将所有代码放到服务器上并发布链接。我相信您会通过这种方式获得更多建议(其中一些无疑与您的问题无关)。

        【讨论】:

          【解决方案6】:

          代码只是我的程序失败的一个例子(它被分配在堆栈上,Jim)。我实际上并不是在寻找“我做错了什么”,而是“我如何诊断我做错了什么”。教一个人钓鱼等等。虽然看了这个问题,但我还没有说得足够清楚。感谢上帝的编辑功能。 :')

          另外,我实际上修复了 std::string 问题。如何?通过用向量替换它,编译,然后再次替换字符串。它一直在那里一直崩溃,即使它...不能修复,它也已修复。那里有一些讨厌的东西,我不确定是什么。不过,我确实想检查一下我在堆上手动分配内存的时间:

           this->map = new Area*[largestY + 1];
           for (int i = 0; i < largestY + 1; i++) {
               this->map[i] = new Area[largestX + 1];
           }
          

          并删除它:

          for (int i = 0; i < largestY + 1; i++) {
              delete [] this->map[i];
          }
          delete [] this->map;
          

          我之前没有使用 C++ 分配二维数组。它似乎有效。

          【讨论】:

            【解决方案7】:

            另外,我实际上修复了 std::string 问题。如何?通过用向量替换它,编译,然后再次替换字符串。它一直在那里崩溃,即使它……也不能修复。那里有一些讨厌的东西,我不确定是什么。

            听起来你真的对它动摇了。如果你不知道为什么它现在可以工作,那么它仍然是坏的,而且几乎肯定会在以后再次咬你(在你增加了更多的复杂性之后)。

            【讨论】:

              【解决方案8】:

              运行净化。

              这是一个近乎神奇的工具,当你破坏你不应该接触的内存、由于不释放东西而泄漏内存、双重释放等时,它会报告。

              它在机器代码级别工作,因此您甚至不必拥有源代码。

              我参加过的最令人愉快的供应商电话会议之一是 Purify 在他们的代码中发现了内存泄漏,我们能够问,“您是否可能没有在函数 foo() 中释放内存”并听到他们声音中的惊讶。

              他们认为我们是在调试神,但后来我们让他们知道了秘密,这样他们就可以在我们不得不使用他们的代码之前运行 Purify。 :-)

              http://www-306.ibm.com/software/awdtools/purify/unix/

              (它相当昂贵,但他们有免费的 eval 下载)

              【讨论】:

                【解决方案9】:

                我经常使用的调试技术之一(除了最奇怪的情况)是分而治之。如果您的程序当前因某些特定错误而失败,则以某种方式将其分成两半,看看它是否仍然有相同的错误。显然,诀窍是决定在哪里划分程序!

                您给出的示例没有显示足够的上下文来确定错误可能在哪里。如果其他人要尝试您的示例,它将正常工作。所以,在你的程序中,尽量删除你没有向我们展示的额外内容,然后看看它是否有效。如果是这样,则一次添加一些其他代码,直到它开始失败。那么,你刚刚添加的东西可能就是问题所在。

                请注意,如果您的程序是多线程的,那么您可能会遇到更大的问题。如果没有,那么您应该能够以这种方式缩小范围。祝你好运!

                【讨论】:

                  【解决方案10】:

                  除了 Boundschecker 或 Purify 之类的工具之外,解决此类问题的最佳选择是真正擅长阅读代码并熟悉正在处理的代码。

                  内存损坏是最难解决的问题之一,通常这些类型的问题可以通过在调试器中花费数小时/天并注意到类似“嘿,指针 X 被删除后正在使用!”来解决。

                  如果它有帮助,那么随着经验的积累,你会变得更好。

                  您为数组分配的内存看起来是正确的,但请确保您也检查了访问数组的所有位置。

                  【讨论】:

                    【解决方案11】:

                    我可以看到您的代码没有错误。如前所述,需要更多上下文。

                    如果您还没有尝试过,请安装 gdb(gcc 调试器)并使用 -g 编译程序。这将编译 gdb 可以使用的调试符号。安装 gdb 后,使用程序 (gdb ) 运行它。 This 是一个有用的 gdb 使用秘籍。

                    为产生错误的函数设置断点,并查看 exampleString 的值是什么。对传递给 exampleString 的任何参数也执行相同的操作。这至少应该告诉你 std::strings 是否有效。

                    我发现this article 的答案是一个很好的指针指南。

                    【讨论】:

                      【解决方案12】:

                      据我所知,您的代码是正确的。假设 exampleString 是具有您描述的类范围的 std::string ,您应该能够以这种方式初始化/分配它。也许还有其他问题?也许实际代码的 sn-p 将有助于将其置于上下文中。

                      问题:exampleString 是指向用 new 创建的字符串对象的指针吗?

                      【讨论】:

                        猜你喜欢
                        • 2015-06-11
                        • 1970-01-01
                        • 2011-04-04
                        • 2020-10-10
                        • 1970-01-01
                        • 2019-09-23
                        • 2011-12-03
                        • 2011-08-03
                        • 2021-03-05
                        相关资源
                        最近更新 更多