【问题标题】:How can you catch memory corruption in C++?如何在 C++ 中发现内存损坏?
【发布时间】:2022-01-20 11:30:10
【问题描述】:

我有一个 std::string 似乎以某种方式损坏。有时字符串析构函数会触发访问冲突,有时通过 std::cout 打印会导致崩溃。

如果我如下填充结构中的字符串,则 back_padding 在我的代码中相对一致的点处会稍微损坏:

struct Test {
    int front_padding[128] = {0};
    std::string my_string;
    int back_padding[128] = {0};
};

有没有办法保护前后填充数组,以便写入它们会导致异常或什么?或者也许有一些工具可以用来捕捉写入该内存的罪魁祸首?

平台:使用 MSVC 构建的 Windows x64。

【问题讨论】:

  • 您需要的工具是边界检查器。如果您使用的是 C++ 标准库容器,则可以打开检查迭代器。
  • asan/valgrind 可能有帮助吗?
  • 如果你使用调试器,你可以在填充上抛出一个观察点。您的代码中可能存在与字符串不直接相关的 UB。
  • 自 2019 v16.9 起,MSVC 应该有一个地址清理程序——它会自动完成你的要求。文档here.

标签: c++ memory crash


【解决方案1】:

一般来说,您必须解决代码卫生问题,这是一个相当广泛的话题。听起来您可能有越界写入,或使用悬空指针,甚至在使用指针时出现竞争条件,但在后一种情况下,错误的可见性会受到观察的影响,就像众所周知的量子叠加态的猫一样。

调试此类恶意写入源的一种肮脏方法是创建数据断点。如果错误似乎是确定性的并且不是“heisenbug”,则它特别有效。在调试会话期间在 MSVS 中是可能的。在 gdb 中,可以使用监视断点。

您可以指向 std::string 存储,或者在您的实验案例中,指向前填充数组以尝试触发发生写入操作的断点。

【讨论】:

  • 数据断点是解决方案,谢谢。我在一个不断变化的填充数组索引上设置了一个数据断点,并在修改数据的确切时刻捕获了应用程序。这个调用堆栈给了我解决问题所需的线索。对于那些感兴趣的人,调用堆栈位于 Boost Signal 库的深处,这让我意识到我已经将 lambda 作为回调传递给使用 boost 信号的对象,但是这个 lambda 超出了范围,所以当它被调用时,未定义行为以堆栈损坏的形式发生。
【解决方案2】:

如何在 C++ 中发现内存损坏?

现代编译器的最佳方式是使用address sanitizer 进行编译。这会插入您在自动(堆栈)和动态(堆)分配周围描述的那种保护区域,并检测它们何时被践踏。它内置在 Clang、GCC 和 MSVC 中。

如果您没有编译器支持,或者需要在不重新编译的情况下诊断现有二进制文件中的问题,您可以使用Valgrind

经过清理的可执行文件全速运行,尽管它正在做更多的工作,并且故意使用对缓存不太友好的内存布局;预计它会比同等的非仪器构建慢约 2 倍。

在 valgrind 下运行要慢得多(memcheck 预计为 10x-30x),但会捕获更多类型的错误,如果您无法重新编译,这是您唯一的选择。

【讨论】:

  • 这是一个很好的建议,但是对于这个特殊的问题没有结果。无论如何,谢谢。
  • 哪一个?消毒剂?瓦尔格林?您的警戒区域很大,并且您没有说明后填充损坏发生的位置,因此您可能需要指示您尝试留下更大排水沟的任何工具,和/或更积极地跟踪动态分配(尽管您没有说你的对象是如何分配的)
  • 我在 VS2019 中尝试了地址清理程序,但启用此功能后问题不再出现。不过,我并没有尝试使用 Valgrind。我还尝试使用 GFlags 来检测堆分配错误,但没有成功。问题出在堆栈分配的 std::string 中,我看到的损坏是 back_padding 数组的索引 380 周围的 8 个字节的乱码,实际上是 char[1024]。
  • 380 字节是如此遥远,以至于地址清理程序可能将其视为对完全不同对象的合法写入。我不知道你是否可以选择在分配周围使用更大的红区——即使是 valgrind 也可能需要一些配置才能捕捉到这一点。
猜你喜欢
  • 1970-01-01
  • 2011-06-04
  • 1970-01-01
  • 1970-01-01
  • 2010-09-06
  • 1970-01-01
  • 2012-01-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多