【问题标题】:Debugging Stack Corruption issue调试堆栈损坏问题
【发布时间】:2017-06-07 16:57:18
【问题描述】:

我正在 C++ (Visual Studio 2015) 中的大型应用程序上调试“访问冲突”异常。该应用程序是由多个库构建的,并且问题出现在其中一个(SystemC)上,尽管我怀疑问题的根源在其他地方。

我看到的是一个函数调用,它破坏了调用者的成员函数的地址。

m_update_phase = true;
m_prim_channel_registry->perform_update();
m_update_phase = false;

inline
void
sc_prim_channel_registry::perform_update()
{
    for( int i = m_update_last; i >= 0; -- i ) {
    m_update_array[i]->perform_update();
    }
    m_update_last = -1;
}

(这些摘自systemc\kernel\sc_simcontext.cppsystemc\communication\sc_prim_channel.h,是SystemC 库的一部分)

上述代码在多次迭代后发生错误。对 m_prim_channel_registry->perform_update() 的调用会引发 0xC0000005: Access violation writing location 0x0F4CD9E9. 异常。
仅在以发布配置构建应用程序时才会发生这种情况。

查看汇编代码,我看到函数 sc_prim_channel_registry::perform_update() 已内联,而内部函数调用 m_update_array[i]->perform_update() 似乎破坏了调用函数的堆栈帧。
m_update_last = -1;被执行时,&m_update_last 不再指向一个有效的内存位置并抛出异常。
m_update_lastsc_prim_channel_registry 类的简单本地成员,类型为 int

    m_update_phase = true;
    m_prim_channel_registry->perform_update();
1034D99E  mov         eax,dword ptr [esi+10h]  
1034D9A1  mov         byte ptr [esi+0A3h],1  
1034D9A8  mov         dword ptr [ebp-18h],eax  
1034D9AB  mov         ebx,dword ptr [eax+28h]  
1034D9AE  test        ebx,ebx  
1034D9B0  js          $LN163+0FEh (1034D9D0h)  
1034D9B2  mov         esi,eax  
1034D9B4  mov         eax,dword ptr [esi+20h]  
1034D9B7  mov         edi,dword ptr [eax+ebx*4]  
1034D9BA  mov         ecx,edi  
1034D9BC  mov         eax,dword ptr [edi]  
1034D9BE  call        dword ptr [eax+14h]  
1034D9C1  sub         ebx,1  
1034D9C4  mov         byte ptr [edi+1Ch],0  
1034D9C8  jns         $LN163+0E2h (1034D9B4h)  
1034D9CA  mov         esi,dword ptr [this]  
1034D9CD  mov         eax,dword ptr [ebp-18h]  
1034D9D0  mov         dword ptr [eax+28h],0FFFFFFFFh  
    m_update_phase = false;

在地址1034D9D0处抛出异常 所以最后执行的指令是

0F97D9CD  mov         eax,dword ptr [ebp-18h]  
0F97D9D0  mov         dword ptr [eax+28h],0FFFFFFFFh  

m_prim_channel_registry地址在[ebp-18h]和eax中,[eax+28h]是m_update_last

在内部调用perform_update() 之前查看 esp 和 ebp 的监视窗口,我看到了:

    ebp-18h 0x0022fd5c  unsigned int
    esp 0x0022fd60  unsigned int

这很奇怪。它们之间的区别只有4个字节,下一次压栈会使它们相等并覆盖[ebp-18h]!
[ebp-18h] 持有this->m_prim_channel_registry 的副本。调用1034D9BE call dword ptr [eax+14h] 在压栈时会破坏ebp-18h 的内容。看起来堆栈已经(向下)增长太多,并且破坏了前一帧。

我的问题是:

  • 我是否正确分析了问题?我错过了什么吗?
  • 什么会导致这种损坏?我认为这个问题与编译器或 SystemC 库无关,可能是之前在其他地方发生的事情。
  • 调试此类损坏的技术有哪些?

更新

我相信我找到了问题所在,但我不能说我完全理解这一点。
在调用外部 perform_update() 的同一函数 (sc_simcontext::crunch) 中,调用 systemc 方法

    // execute method processes

    sc_method_handle method_h = pop_runnable_method();
    while( method_h != 0 ) {
    try {
        method_h->execute();
    }
    catch( const sc_exception& ex ) {
        cout << "\n" << ex.what() << endl;
        m_error = true;
        return;
    }
    method_h = pop_runnable_method();
    }

这些方法是之前注册的延迟函数调用。
其中一种方法是通过执行ret 4 来返回,因此每次调用它时都会缩小堆栈帧,直至发生上述损坏。

我是如何管理注册损坏的 systemc 方法的?
显然,当 f 是模块的虚函数时,使用 SC_METHOD(f) 是个坏主意。 这样做会导致不同的、不相关的要调用的“随机”函数。
我不确定为什么会发生这种情况以及为什么存在这种限制。此外,我不记得看到任何关于使用虚拟成员函数作为 systemc 方法的警告,但这绝对是问题所在。在调试 SC_METHOD 调用本身中的方法注册时,我可以看到内部的函数指针指向的函数与赋予 SC_METHOD 宏的函数不同。

为了解决我调用SC_METHOD(wrapper_f) 的问题,其中wrapper_f 是模块的一个简单的非虚拟 成员函数,它所做的只是调用原始虚函数f。就是这样。

【问题讨论】:

  • 使用这种代码,有时reentry会出现问题。也就是说,虚拟 perform_update() 有时会修改(添加或删除一个值) m_update_array 而前一个堆栈帧正在循环它,并且您有未定义的结果。您可以使用日志文件轻松调试。

标签: c++ callstack corruption systemc


【解决方案1】:

您可能对 MSVC 上的成员函数指针有问题。

考虑以下代码,文件 main.cpp:

#include <cstdio>

struct base;
typedef void (base::*baseptr_t)();

struct base {
    void foo() { }
};

void callfoo(base *obj, baseptr_t ptr);

int main()
{
    base obj;
    std::printf("sizeof(baseptr_t)=%llu\n", sizeof(baseptr_t));
    callfoo(&obj, &base::foo);
}

和文件 callfoo.cpp:

#include <cstdio>

struct base;
typedef void (base::*baseptr_t)();

void callfoo(base *obj, baseptr_t ptr)
{
    std::printf("sizeof(baseptr_t)=%llu\n", sizeof(baseptr_t));
    (obj->*ptr)();
}

在 x86_64 上打印:

sizeof(baseptr_t)=8
sizeof(baseptr_t)=24

在因访问冲突而崩溃之前。

这是因为 MSVC 为具有已知定义的类生成 8 字节指针,但如果类定义不可用,则必须生成 24 字节指针。

编译器有办法控制这种行为:

PS:我无法重现此内容,但您也可以检查 SystemC 中的 sc_process.h 标头,它有以下几行:

#if defined(_MSC_VER)
#if ( _MSC_VER > 1200 )
#   define SC_USE_MEMBER_FUNC_PTR
#endif
#else
#   define SC_USE_MEMBER_FUNC_PTR
#endif

您可以尝试为您的构建取消定义此宏,在这种情况下,SystemC 在调用进程函数时会尝试使用不同的策略。

PS2:成员函数指针的大小可以是 8、16 和 24 字节,具体取决于其层次结构,因此应该有 3 种方法来取消引用成员函数指针,而且每种方法都必须处理虚拟和非虚拟调用。

【讨论】:

  • 这很有趣,但我不确定它是否与我的具体问题有关。一旦函数不再是虚拟的,我看到的问题就消失了,而不是当类缺少定义时。知道这与 virtual 有什么关系吗?
  • 对于 8 字节的情况和非虚函数成员函数指针是指向函数的普通指针,但如果您将指针指向虚函数,它会生成执行虚调用并返回其地址的小存根函数反而。使用 24 字节变体 - 我不确定,但它很可能将函数索引存储在 vtable 中,如果读取垃圾而不是索引,则可以从此类(或其他类的虚拟函数)调用另一个虚拟函数.现在如果函数是非虚拟的,它可能会碰巧工作,因为执行调用的代码要简单得多。
  • 这很可能是问题的根本原因。根据 SystemC INSTALL 文档,MSVC 需要 /vmg
【解决方案2】:

看来你知道自己在做什么。

我可以给你一个建议,而不是一个解决方案,但这是我多次遇到的问题,它破坏了堆栈。

检查导致损坏的函数perform_update()。它是否将大数组定义为局部变量?如果是这样,它可能会超出堆栈并覆盖那里的返回数据和其他重要数据。这是我遇到的堆栈损坏最常见的问题。

这是一个偷偷摸摸的问题,因为它取决于本地数组的大小和您拥有的堆栈数量。这会因系统而异。

【讨论】:

  • 感谢您的建议。 perform_update() 在开始执行之前导致损坏。调用本身会导致堆栈损坏。 perform_update 是一个指向 sc_fifo&lt;T&gt;::update() 的函数指针,这是一个非常简单的 SystemC 函数,根本没有局部变量。我认为问题不存在。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-07-03
  • 2012-04-06
  • 1970-01-01
  • 1970-01-01
  • 2016-03-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多