调试堆栈破坏问题

5
我正在调试一个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库的systemc\kernel\sc_simcontext.cppsystemc\communication\sc_prim_channel.h的摘录。

在上述代码的多次迭代后,出现了错误。调用m_prim_channel_registry->perform_update()会抛出0xC0000005: Access violation writing location 0x0F4CD9E9.异常。
只有在Release配置下构建应用程序时才会发生这种情况。

查看汇编代码,我发现函数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库无关,可能是在其他地方早些时候发生的。
  • 有哪些调试技术可以解决这种破坏?

更新

我相信我找到了问题,但我不能说我完全理解这个问题。
在同一个函数 (sc_simcontext::crunch) 中调用外部的 perform_update() 的地方,也会调用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返回,从而在每次调用时缩小堆栈帧,直到发生上述描述的损坏。

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

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


使用这种代码时,有时会出现_reentry_问题。也就是说,虚拟的perform_update()有时会在前一个堆栈帧正在循环遍历m_update_array时修改它(添加或删除值),从而导致不确定的结果。您可以通过日志文件轻松调试此问题。 - rodrigo
2个回答

3
您可能在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字节指针。

编译器有控制此行为的方法:

附注:我无法重现此问题,但您也可以检查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种方法可以对成员函数指针进行解引用,而且每种方法都必须处理虚拟和非虚拟调用。

很有趣,但我不确定它是否与我的特定问题有关。当函数不再是虚拟的时候,我看到的问题消失了,而不是类缺少定义。你有什么想法这如何与虚拟有关? - Amir Gonnen
对于8字节的情况和非虚函数成员函数指针是一个普通的函数指针,但如果你取一个虚函数的指针,它会生成一个小的存根函数来执行虚拟调用并返回其地址。至于24字节的变量 - 我不确定,但它很可能在vtable中存储函数的索引,这使得可以从该类(或某个其他类的虚函数)调用另一个虚函数,如果读取到垃圾而不是索引。现在,如果函数是非虚拟的,它可能会因为执行调用的代码更简单而偶然工作。 - user1143634
这很可能是问题的根本原因。根据SystemC的“INSTALL”文档,MSVC需要/vmg - pah

0

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

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

检查导致破坏的函数perform_update()。它是否定义了一个大数组作为本地变量?如果是,则可能超出堆栈并覆盖返回数据和其他重要数据。这是我遇到的最常见的堆栈破坏问题。

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


谢谢您的建议。在执行之前,perform_update()就会导致损坏。调用本身会导致堆栈损坏。perform_update是一个函数指针,它指向sc_fifo<T>::update(),这是一个非常简单的SystemC函数,没有任何局部变量。我不认为问题出在那里。 - Amir Gonnen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接