可以修改帧指针的内容

What can modify the frame pointer?

本文关键字:指针 修改      更新时间:2023-10-16

我现在在一个相当大的C++应用程序中出现了一个非常奇怪的错误(CPU和RAM的使用量以及代码长度都很大,超过100000行)。这是在双核Sun Solaris 10计算机上运行的。该程序订阅股票价格提要,并将其显示在用户配置的"页面"上(页面是用户自定义的窗口结构,该程序允许用户配置此类页面)。这个程序过去工作时没有问题,直到其中一个底层库变成多线程。受此影响的程序部分已相应更改。关于我的问题。

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

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

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

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

堆栈损坏,99.9%肯定。

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

  • "C"数组的使用
  • "C"strcpy样式函数的使用
  • memcpy
  • malloc和free
  • 使用指针的任何事物的线程安全性
  • 未初始化的POD变量
  • 指针算术
  • 试图通过引用返回局部变量的函数

我今天遇到了这个问题,当时我正深陷gdb泥潭,调试了整整一个小时,才想到我只是在C数组的数组边界(我最意想不到的地方)上写了字。

因此,如果可能的话,请使用vectors,因为如果您在调试模式下尝试,任何分散的STL实现都会给出良好的编译器消息(而C数组会用segfault来惩罚您)。

我不确定你所说的"帧指针"是什么:

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

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

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

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

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

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

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

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上,每个线程获得1Mg的堆栈,因此导致堆栈溢出的常见问题要小得多。除非有无休止的递归,否则总是可以通过增加堆栈大小来减轻堆栈溢出,尽管这通常不是最好的答案。

堆栈损坏只是指在当前堆栈帧的边界之外进行写入,从而可能损坏堆栈上的其他数据或返回地址。

最简单的是:-

void foo()
{ 
  char message[10];
  message[10] = '!';  // whoops! beyond end of array
}

这听起来像是一个堆栈溢出问题——有东西在数组的边界之外写入,并践踏堆栈上的堆栈帧(可能还有返回地址)。有大量关于这个主题的文献。《Shell程序员指南》(第二版)中有一些SPARC示例可以帮助您。

使用C++,变量和比赛条件可能会导致间歇性崩溃。

有可能通过Valgrind运行吗?也许Sun提供了类似的工具。"英特尔VTune"(实际上我想到的是"线程检测器")也有一些非常好的线程调试工具等等。

如果你的雇主能为更昂贵的工具买单,他们真的可以让这类问题更容易解决。

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

首先,我要验证所有的库和相关对象都已干净地重建,并且所有的编译器选项都是一致的——我以前也遇到过类似的问题(Solaris 2.5),这是由一个未重建的对象文件引起的。

这听起来就像是一个覆盖——如果只是一个糟糕的偏移,那么在内存周围放置保护块并没有帮助。

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

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

其他细节我不知道了,但"2"是你问题描述中反复出现的主题。)

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

我认为这可能是堆栈损坏。我要补充的是,切换到多线程库让我怀疑所发生的事情是一个潜在的bug已经暴露。缓冲区溢出的排序可能发生在未使用的内存上。现在它正在进入另一个线程的堆栈。还有许多其他可能的情况。

很抱歉,如果这没有给如何找到它提供太多提示。

我试过Valgrind,但不幸的是,它没有检测到堆栈错误:

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

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

还同意0x000002是可疑的。这是崩溃之间唯一的常数。更奇怪的是,这只是在多线程切换时出现的。我认为多个线程导致的堆栈变小是现在出现这种情况的原因,但这纯粹是我的假设。

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

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

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

还同意0x000002是可疑的。这是崩溃之间唯一的常数。更奇怪的是,这只是在多线程切换时出现的。我认为多个线程导致的堆栈变小是现在出现这种情况的原因,但这纯粹是我的假设。

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

您可能可以通过将应用程序强制到单个处理器上来重新编程。我不知道你是怎么对斯帕克那样做的。