4

我现在在一个相当庞大的 C++ 应用程序中出现了一个非常奇怪的错误(在 CPU 和 RAM 使用以及代码长度方面非常大 - 超过 100,000 行)。这是在双核 Sun Solaris 10 机器上运行的。该程序订阅股票价格馈送并将它们显示在用户配置的“页面”上(页面是用户自定义的窗口结构 - 该程序允许用户配置此类页面)。在其中一个底层库变为多线程之前,该程序一直可以正常工作。受此影响的程序部分已相应更改。关于我的问题。

大约在每三次执行中,程序将在启动时出现段错误。这不一定是硬性规则——有时它会连续崩溃 3 次,然后连续工作 5 次。有趣的是段错误(阅读:痛苦)。它可能以多种方式表现出来,但最常见的情况是函数 A 调用函数 B,进入函数 B 时,帧指针会突然设置为 0x000002。功能一:

   result_type emit(typename type_trait<T_arg1>::take _A_a1) const
     { return emitter_type::emit(impl_, _A_a1); }

这是一个简单的信号实现。impl_ 和 _A_a1 在崩溃时的框架内定义良好。在实际执行该指令时,我们最终到达程序计数器 0x000002。

这并不总是发生在该功能上。事实上,它发生在很多地方,但这是最简单的情况之一,不会留下太大的错误空间。有时会发生的情况是堆栈分配的变量会突然无缘无故地坐在垃圾内存上(总是在 0x000002 上)。其他时候,相同的代码会运行得很好。所以,我的问题是,什么可以如此严重地破坏堆栈?什么实际上可以改变帧指针的值?我当然从来没有听说过这样的事情。我能想到的唯一一件事就是在数组上越界写入,但我已经使用堆栈保护器构建了它,它应该会出现这种情况的任何实例。我在这里的堆栈范围内也很好。我也不 看不到另一个线程如何覆盖第一个线程的堆栈上的变量,因为每个线程都有自己的堆栈(这都是 pthreads)。我已经尝试在 linux 机器上构建它,虽然我没有在那里遇到段错误,但大约有三分之一的时间它会冻结在我身上。

4

14 回答 14

9

堆栈损坏,绝对是 99.9%。

您应该仔细寻找的气味是:-

  • 使用“C”数组
  • 使用“C”strcpy 风格的函数
  • 内存
  • malloc 和免费
  • 使用指针的任何东西的线程安全
  • 未初始化的 POD 变量。
  • 指针算术
  • 试图通过引用返回局部变量的函数
于 2008-10-30T23:36:39.730 回答
4

我今天遇到了这个确切的问题,并且在泥泞中深陷gdb泥潭并连续调试了一个小时,然后我才想到我只是在 C 数组的数组边界(我没想到它的地方)上写了。

因此,如果可能,请使用vectors 代替,因为如果您在调试模式下尝试,任何 decend STL 实现都会提供良好的编译器消息(而 C 数组会用段错误惩罚您)。

于 2008-10-30T23:02:31.250 回答
3

堆栈溢出堆栈损坏之间存在一些混淆。

堆栈溢出是一个非常具体的问题,原因是尝试使用比操作系统分配给您的线程更多的堆栈。三个正常的原因是这样的。

void foo()
{
  foo();  // endless recursion - whoops!
}

void foo2()
{
  char myBuffer[A_VERY_BIG_NUMBER];  // The stack can't hold that much.
}

class bigObj
{
  char myBuffer[A_VERY_BIG_NUMBER];  
}

void foo2( bigObj big1)  // pass by value of a big object - whoops!
{
}

在嵌入式系统中,线程堆栈的大小可能以字节为单位,即使是简单的调用序列也可能导致问题。默认情况下,在 Windows 上,每个线程获得 1 Meg 的堆栈,因此导致堆栈溢出的常见问题要少得多。除非您有无限递归,否则总是可以通过增加堆栈大小来缓解堆栈溢出,即使这通常不是最佳答案。

堆栈损坏只是意味着在当前堆栈帧的边界之外写入,因此可能会损坏其他数据 - 或堆栈上的返回地址。

最简单的:-

void foo()
{ 
  char message[10];

  message[10] = '!';  // whoops! beyond end of array
}
于 2008-10-31T17:16:21.913 回答
3

正如您所说,我不确定您所说的“帧指针”是什么:

在实际执行该指令时,我们最终到达程序计数器 0x000002

这听起来像是返回地址被破坏了。帧指针是指向当前函数调用上下文的堆栈位置的指针。它很可能指向返回地址(这是一个实现细节),但帧指针本身并不是返回地址。

我认为这里没有足够的信息来真正给你一个好的答案,但一些可能是罪魁祸首的事情是:

  • 不正确的调用约定。如果您使用与函数编译方式不同的调用约定来调用函数,则堆栈可能会损坏。

  • 内存命中。通过错误指针写入的任何内容都可能导致垃圾最终进入堆栈。我不熟悉 Solaris,但大多数线程实现都将线程放在同一个进程地址空间中,因此任何线程都可以访问任何其他线程的堆栈。一个线程可以获得指向另一个线程堆栈的指针的一种方法是,如果将局部变量的地址传递给最终处理不同线程上的指针的 API。除非您正确同步事物,否则最终会导致指针访问无效数据。鉴于您正在处理“简单的信号实现”,似乎一个线程可能正在向另一个线程发送信号。也许该信号中的一个参数有一个指向本地的指针?

于 2008-10-30T23:09:10.283 回答
1

破坏帧指针并不难——如果你查看例程的反汇编,你会发现它在例程开始时被压入并在结束时被拉出——所以如果有任何东西覆盖堆栈,它可能会丢失。堆栈指针是堆栈当前所在的位置 - 帧指针是它开始的位置(对于当前例程)。

首先,我将验证所有库和相关对象都已重建干净并且所有编译器选项都是一致的 - 我之前遇到过类似的问题(Solaris 2.5),这是由尚未重建的目标文件引起的.

这听起来完全像一个覆盖 - 如果它只是一个错误的偏移量,那么在内存周围放置保护块将无济于事。

在每次核心转储后,检查核心文件以尽可能多地了解故障之间的相似之处。然后尝试确定被覆盖的内容。我记得帧指针是最后一个堆栈指针 - 所以在当前堆栈帧中不应修改帧指针之前的任何逻辑内容 - 所以可能会记录它并将其复制到其他地方并在返回时进行比较。

于 2008-10-31T01:30:50.203 回答
1

这听起来像一个堆栈溢出问题 - 一些东西正在超出数组的范围并践踏堆栈上的堆栈帧(也可能是返回地址)。有大量关于这个主题的文献。“The Shell Programmer's Guide”(第 2 版)包含可以帮助您的 SPARC 示例。

于 2008-10-30T22:58:51.867 回答
1

使用 C++ 统一变量和竞争条件可能会导致间歇性崩溃。

于 2008-10-30T23:38:31.787 回答
1

是否有可能通过 Valgrind 运行这个东西?也许 Sun 提供了类似的工具。Intel VTune(实际上我在考虑线程检查器)也有一些非常好的线程调试工具等。

如果你的雇主愿意为更昂贵的工具付出代价,他们真的可以让这类问题更容易解决。

于 2008-10-31T00:03:56.820 回答
0

我支持它可能是堆栈损坏的概念。我要补充一点,切换到多线程库让我怀疑发生的事情是一个潜伏的错误已经暴露。缓冲区溢出的排序可能发生在未使用的内存上。现在它正在访问另一个线程的堆栈。还有许多其他可能的情况。

抱歉,如果这并没有给出如何找到它的提示。

于 2008-10-30T23:21:22.990 回答
0

不可能知道,但这里有一些我能想到的提示。

  • 在 pthreads 中,您必须分配堆栈并将其传递给线程。你分配够了吗?没有像单线程进程那样的自动堆栈增长。
  • 如果您确定不会通过写入过去的堆栈分配数据来破坏堆栈,请检查 rouge 指针(主要是未初始化的指针)。
  • 其中一个线程可能会覆盖其他人所依赖的一些数据(检查您的数据同步)。
  • 在这里调试通常不是很有帮助。我会尝试创建大量日志输出(每个函数/方法调用的进入和退出的跟踪),然后分析日志。
  • 错误在 Linux 上以不同方式表现出来的事实可能会有所帮助。您在 Solaris 上使用什么线程映射?确保将每个线程映射到它自己的 LWP 以简化调试。
于 2009-04-13T01:17:24.210 回答
0

将值 2 分配给变量但将其地址分配给 2 是否有意义?

我丢失了其他细节,但“2”是您的问题描述中反复出现的主题。;)

于 2008-10-30T22:59:39.523 回答
0

我认为这听起来肯定是由于数组或缓冲区写入超出限制而导致的堆栈损坏。只要写入是顺序的,而不是随机的,堆栈保护器就会很好。

于 2008-10-30T23:06:41.913 回答
0

我在上面尝试了 Valgrind,但不幸的是它没有检测到堆栈错误:

“除了性能损失之外,Valgrind 的一个重要限制是它无法检测使用静态或堆栈分配数据时的边界错误。”

我倾向于同意这是一个堆栈溢出问题。棘手的事情是追踪它。就像我说的,这个东西有超过 100,000 行代码(包括内部开发的自定义库——其中一些可以追溯到 1992 年)所以如果有人有任何捕捉这类东西的好技巧,我会感激的。到处都在处理数组,该应用程序使用 OI 作为其 GUI(如果您还没有听说过 OI,请不胜感激)所以仅仅寻找逻辑谬误是一项艰巨的任务,我的时间很短。

也同意 0x000002 是可疑的。它是崩溃之间唯一的常数。更奇怪的是,这只出现在多线程开关上。我认为由于多线程而导致的较小堆栈是现在出现这种情况的原因,但这纯粹是我的假设。

没有人问这个,但我用 gcc-4.2 构建。另外,我可以在这里保证 ABI 的安全,所以这也不是问题。至于 RAM 命中的“堆栈末尾的垃圾”,它普遍为 2(尽管在代码中的不同位置)的事实让我怀疑垃圾往往是随机的。

于 2008-10-31T00:49:40.987 回答
0

也同意 0x000002 是可疑的。它是崩溃之间唯一的常数。更奇怪的是,这只出现在多线程开关上。我认为由于多线程而导致的较小堆栈是现在出现这种情况的原因,但这纯粹是我的假设。

如果您通过引用或地址传递堆栈上的任何内容,那么如果另一个线程在从函数返回的第一个线程之后尝试使用它,这肯定会发生。

您可以通过将应用程序强制到单个处理器上来重现这一点。我不知道您如何使用 Sparc 做到这一点。

于 2008-10-31T20:52:57.653 回答