调试堆栈损坏问题



我正在调试一个"访问违规";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;
}

(这些摘录自systemckernelsc_simcontext.cppsystemccommunicationsc_prim_channel.h,是SystemC库的一部分)

该错误发生在通过上面的代码进行多次迭代之后。对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库无关,可能是之前在其他地方发生的事情
  • 调试这种损坏的技术是什么

更新

我相信我发现了这个问题,但我不能说我完全理解这个问题
在调用外部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。就是这样。

您可能在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)=%llun", 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)=%llun", sizeof(baseptr_t));
(obj->*ptr)();
}

在x86_64上,这会打印:

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

在因访问违规而崩溃之前。

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

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

  • https://msdn.microsoft.com/en-us/library/yad46a6z.aspx
  • https://msdn.microsoft.com/en-us/library/83cch5a6.aspx

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种方法来取消引用成员函数指针,并且每种方法都必须处理虚拟和非虚拟调用。

你似乎知道自己在做什么。

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

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

这是一个狡猾的问题,因为它取决于本地数组的大小和堆栈的数量。这种情况随系统而变化。

最新更新