分析两种Dump(崩溃日志)文件生成的方法及比较

本文详细分析了两种崩溃日志生成方案及其比较,并提出了一种利用Detours库动态钩接UnhandledExceptionFilter函数的创新方法,以解决标准方案无法拦截特定CRT错误的问题。通过案例研究和代码实现,展示了如何有效捕捉和分析关键崩溃事件,从而提升软件质量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分析两种Dump(崩溃日志)文件生成的方法及比较 - 方亮的专栏 - 博客频道 - youkuaiyun.com
http://blog.youkuaiyun.com/breaksoftware/article/details/23134445  

 做windows产品开发的,永远绕不开一个问题——程序崩溃。如果希望不断提升产品质量,就得不停的收集和分析崩溃日志。但是我们会发现一个问题,我们经常采用的方案无法拦截崩溃。(转载请指明出于breaksoftware的csdn博客)比如会出现如下提示:


        这是一个非常不好的体验,至少说这个是对提升软件质量无益的体验。虽然以上框可以通过如下代码禁用掉,但是仍然只是个掩耳盗铃的做法。

[cpp]  view plain copy
  1. SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);  

        我们先看一种标准的Dump生成方案:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include "CreateDump.h"  
  2.   
  3. #include <atlbase.h>  
  4. #include <atlstr.h>  
  5. #include <strsafe.h>  
  6. #include <DbgHelp.h>  
  7. #pragma comment(lib,"DbgHelp.lib")  
  8. #define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS        (0x00000004)  
  9. #define MiniDumpWithThreadInfo 0x1000  
  10.   
  11. typedef BOOL (WINAPI *PGetModuleHandleEx)( DWORD dwFlags, LPCTSTR lpModuleName, HMODULE *phModule );  
  12.   
  13. VOID CreateDump(struct _EXCEPTION_POINTERS *pExceptionPointers)   
  14. {  
  15.     //收集信息  
  16.     CStringW strBuild;  
  17.     strBuild.Format(L"Build: %s %s", __DATE__, __TIME__);  
  18.     CString strError;  
  19.     HMODULE hModule;  
  20.     WCHAR szModuleName[MAX_PATH] = {0};  
  21.   
  22.     PGetModuleHandleEx pFun = (PGetModuleHandleEx)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetModuleHandleExW");  
  23.     if ( !pFun ) {  
  24.         return;  
  25.     }  
  26.   
  27.     pFun(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)pExceptionPointers->ExceptionRecord->ExceptionAddress, &hModule);  
  28.     GetModuleFileName(hModule, szModuleName, ARRAYSIZE(szModuleName));  
  29.     strError.Format(L"%s %d , %d ,%d.", szModuleName,pExceptionPointers->ExceptionRecord->ExceptionCode, pExceptionPointers->ExceptionRecord->ExceptionFlags, pExceptionPointers->ExceptionRecord->ExceptionAddress);  
  30.   
  31.     //生成 mini crash dump  
  32.     BOOL bMiniDumpSuccessful;  
  33.     WCHAR szPath[MAX_PATH];   
  34.     WCHAR szFileName[MAX_PATH];   
  35.     WCHAR* szAppName = L"DumpFile";  
  36.     WCHAR* szVersion = L"v1.0";  
  37.     DWORD dwBufferSize = MAX_PATH;  
  38.     HANDLE hDumpFile;  
  39.     SYSTEMTIME stLocalTime;  
  40.     MINIDUMP_EXCEPTION_INFORMATION ExpParam;  
  41.     GetLocalTime( &stLocalTime );  
  42.     GetTempPath( dwBufferSize, szPath );  
  43.     StringCchPrintf( szFileName, MAX_PATH, L"%s%s", szPath, szAppName );  
  44.     CreateDirectory( szFileName, NULL );  
  45.     StringCchPrintf( szFileName, MAX_PATH, L"%s%s//%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp",   
  46.         szPath, szAppName, szVersion,   
  47.         stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,   
  48.         stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond,   
  49.         GetCurrentProcessId(), GetCurrentThreadId());  
  50.     hDumpFile = CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE,   
  51.         FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);  
  52.   
  53.     MINIDUMP_USER_STREAM UserStream[2];  
  54.     MINIDUMP_USER_STREAM_INFORMATION UserInfo;  
  55.     UserInfo.UserStreamCount = 1;  
  56.     UserInfo.UserStreamArray = UserStream;  
  57.     UserStream[0].Type = CommentStreamW;  
  58.     UserStream[0].BufferSize = strBuild.GetLength()*sizeof(WCHAR);  
  59.     UserStream[0].Buffer = strBuild.GetBuffer();  
  60.     UserStream[1].Type = CommentStreamW;  
  61.     UserStream[1].BufferSize = strError.GetLength()*sizeof(WCHAR);  
  62.     UserStream[1].Buffer = strError.GetBuffer();  
  63.   
  64.     ExpParam.ThreadId = GetCurrentThreadId();  
  65.     ExpParam.ExceptionPointers = pExceptionPointers;  
  66.     ExpParam.ClientPointers = TRUE;  
  67.   
  68.     MINIDUMP_TYPE MiniDumpWithDataSegs = (MINIDUMP_TYPE)(MiniDumpNormal   
  69.         | MiniDumpWithHandleData   
  70.         | MiniDumpWithUnloadedModules   
  71.         | MiniDumpWithIndirectlyReferencedMemory   
  72.         | MiniDumpScanMemory   
  73.         | MiniDumpWithProcessThreadData   
  74.         | MiniDumpWithThreadInfo);  
  75.   
  76.     bMiniDumpSuccessful = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),   
  77.         hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL);  
  78.   
  79.     return;  
  80. }  
        可以见得,我们生成dump文件必须一个结构体——_EXCEPTION_POINTERS。

        这个结构体自然不是我们自己构造的,而是系统给我们的。我们该从哪个接口接收系统给我们的该信息呢?

        一般情况下,我们使用SetUnhandledExceptionFilter来设置一个回调函数。当软件即将崩溃时,我们设置的回调函数理论上会被调用。然而,实际并非如此。我们看一个报错的例子。


        如果你也见过这个错误,我想你的截取dump方案应该是被绕过了。我专门查了一下该错误,MSDN上有相关例子

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #pragma once   
  2.   
  3. class A;  
  4.   
  5. void fcn( A* );  
  6.   
  7. class A  
  8. {  
  9. public:  
  10.     virtual void f() = 0;  
  11.     A() { fcn( this ); }  
  12. };  
  13.   
  14. class B : A  
  15. {  
  16.     void f() { }  
  17. };  
  18.   
  19. void fcn( A* p )  
  20. {  
  21.     p->f();  
  22. }  
  23.   
  24. // The declaration below invokes class B's constructor, which  
  25. // first calls class A's constructor, which calls fcn. Then  
  26. // fcn calls A::f, which is a pure virtual function, and  
  27. // this causes the run-time error. B has not been constructed  
  28. // at this point, so the B::f cannot be called. You would not  
  29. // want it to be called because it could depend on something  
  30. // in B that has not been initialized yet.  
  31.   
  32. int PureVirtualFunc()  
  33. {  
  34.     B b;  
  35.     return 0;  
  36. }  
        这个例子将协助我们研究如何截取这种无法使用SetUnhandledExceptionFilter截取的dump。

        我们构造一个SetUnhandledExceptionFilter可以截获dump的例子

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  LONG WINAPI DumpCallback(_EXCEPTION_POINTERS* excp) {  
  2.     CreateDump(excp);  
  3.     return EXCEPTION_EXECUTE_HANDLER;     
  4.  }  
  5. ……  
  6. SetUnhandledExceptionFilter(DumpCallback);  
  7. int *p = NULL;  
  8. *p = 1;  
        我们查看调用堆栈

        可以见得,在调用我们回调函数之前,调用了系统的UnhandledExceptionFilter函数,这个函数的入参也是_EXCEPTION_POINTERS指针。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. LONG WINAPI UnhandledExceptionFilter(  
  2.   _In_  struct _EXCEPTION_POINTERS *ExceptionInfo  
  3. );  
        那么,我们可以猜测,如果我们可以接管该函数,可能可以让我们捕获R6025这样的异常。我使用detours库Hook了这个函数

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include "AutoDump.h"  
  2. #include <windows.h>  
  3. #include "../detours/detours.h"  
  4. #include "CreateDump.h"  
  5.   
  6. LONG WINAPI NewUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *ExceptionInfo ){  
  7.     OutputDebugString(L"NewUnhandledExceptionFilter\n");  
  8.   
  9.     CreateDump(ExceptionInfo);  
  10.     return EXCEPTION_EXECUTE_HANDLER;  
  11. }  
  12.   
  13. CAutoDump::CAutoDump(void)  
  14. {  
  15.     m_lpUnhandledExceptionFilter = NULL;  
  16.     do {  
  17.         SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);  
  18.   
  19.         m_lpUnhandledExceptionFilter = DetourFindFunction( "KERNEL32.DLL""UnhandledExceptionFilter" );  
  20.   
  21.         if ( NULL == m_lpUnhandledExceptionFilter ) {  
  22.             break;  
  23.         }  
  24.         LONG lRes = NO_ERROR;  
  25.         lRes = DetourTransactionBegin();  
  26.         if ( NO_ERROR != lRes ) {  
  27.             break;  
  28.         }  
  29.   
  30.         lRes = DetourAttach( &m_lpUnhandledExceptionFilter, NewUnhandledExceptionFilter );  
  31.         if ( NO_ERROR != lRes ) {  
  32.             break;  
  33.         }  
  34.   
  35.         lRes = DetourTransactionCommit();  
  36.         if ( NO_ERROR != lRes ) {  
  37.             break;  
  38.         }  
  39.     } while (0);  
  40. }  
  41.   
  42.   
  43. CAutoDump::~CAutoDump(void)  
  44. {  
  45.     if ( m_lpUnhandledExceptionFilter ) {  
  46.         do {  
  47.             LONG lRes = NO_ERROR;  
  48.             lRes = DetourTransactionBegin();  
  49.             if ( NO_ERROR != lRes ) {  
  50.                 break;  
  51.             }  
  52.   
  53.             lRes = DetourDetach( &m_lpUnhandledExceptionFilter, NewUnhandledExceptionFilter );  
  54.             if ( NO_ERROR != lRes ) {  
  55.                 break;  
  56.             }  
  57.   
  58.             lRes = DetourTransactionCommit();  
  59.             if ( NO_ERROR != lRes ) {  
  60.                 break;  
  61.             }  
  62.         } while (0);  
  63.     }  
  64. }  
        结果,这种方式,便可以截获R6025这样的CRT错误。

        现在,我们开始分析,为什么SetUnhandledExceptionFilter无法截获这些CRT错误。从上面可以分析出,当出现异常时,流程会进入UnhandledExceptionFilter,但是我们设置的回调函数没被调用。那么可以猜测,应该是系统的UnhandledExceptionFilter函数内部走了其他的流程。我查看下UnhandledExceptionFilter函数的逆向结果,此时我不会将其列出来,因为我们要知道其内部是在哪儿调用了我们通过SetUnhandledExceptionFilter设置的回调函数。我们先看下SetUnhandledExceptionFilter的实现,用IDA查看的逆向结果比较杂乱,我就以ReactOS的代码作为例子来讲解,其核心思想是一致的

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. LPTOP_LEVEL_EXCEPTION_FILTER  
  2. WINAPI  
  3. SetUnhandledExceptionFilter(IN LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter)  
  4. {  
  5.     PVOID EncodedPointer, EncodedOldPointer;  
  6.   
  7.     EncodedPointer = RtlEncodePointer(lpTopLevelExceptionFilter);  
  8.     EncodedOldPointer = InterlockedExchangePointer((PVOID*)&GlobalTopLevelExceptionFilter,  
  9.                                             EncodedPointer);  
  10.     return RtlDecodePointer(EncodedOldPointer);  
  11. }  
        从上述代码中,我们可以见到,系统通过原子操作保存了我们设置的回调函数。然后在UnhandledExceptionFilter函数内部,是这样调用我们设置的回调函数的(依然以ReactOs为例)

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ……  
  2.    RealFilter = RtlDecodePointer(GlobalTopLevelExceptionFilter);  
  3.    if (RealFilter)  
  4.    {  
  5.       LONG ret = RealFilter(ExceptionInfo);  
  6.       if (ret != EXCEPTION_CONTINUE_SEARCH)  
  7.          return ret;  
  8.    }  
  9. ……  
        找到这个锚点,我们便可以动态调试,找出回调函数没有被调用的原因。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. 75BF76D3  mov         dword ptr [ebp-20h],6    
  2. 75BF76DA  xor         esi,esi    
  3. 75BF76DC  mov         dword ptr [ebp-1Ch],esi    
  4. 75BF76DF  mov         dword ptr [ebp-24h],esi    
  5. 75BF76E2  mov         dword ptr [ebp-28h],esi    
  6. 75BF76E5  mov         ebx,dword ptr [ebp+8]    
  7. 75BF76E8  mov         eax,dword ptr [ebx]    
  8. 75BF76EA  test        byte ptr [eax+4],10h    
  9. 75BF76EE  jne         _UnhandledExceptionFilter@4+29h (75BF7934h)    
  10. 75BF76F4  mov         dword ptr [ebp-2Ch],1    
  11. 75BF76FB  cmp         dword ptr [eax],0C0000409h    
  12. 75BF7701  je          _UnhandledExceptionFilter@4+3Fh (75BF8146h)    
  13. 75BF7707  push        ebx    
  14. 75BF7708  call        _CheckForReadOnlyResourceFilter@4 (75BF78B9h)    
  15. 75BF770D  cmp         eax,0FFFFFFFFh    
  16. 75BF7710  je          _UnhandledExceptionFilter@4+91h (75BF793Bh)    
  17. 75BF7716  call        _BasepIsDebugPortPresent@0 (75BF7831h)    
  18. 75BF771B  test        eax,eax    
  19. 75BF771D  jne         _UnhandledExceptionFilter@4+29h (75BF7934h)    
  20. 75BF7723  mov         esi,75CA030Ch    
  21. 75BF7728  push        esi    
  22. 75BF7729  call        dword ptr [__imp__RtlAcquireSRWLockExclusive@4 (75BD034Ch)]    
  23. 75BF772F  push        dword ptr ds:[75CA0074h]    
  24. 75BF7735  call        dword ptr [__imp__RtlDecodePointer@4 (75BD0670h)]    
  25. 75BF773B  mov         edi,eax    
  26. 75BF773D  test        edi,edi   
        调试时,需要注意:当运行到75BF771D时,我们要将执行路径指向75BF7723。因为我们是debug状态,要跳过这个检测。然后我们继续执行,会发现75BF7735处执行的结果是0,即我们获取的回调函数执行为空。这样便分析出,为什么SetUnhandledExceptionFilter方法设置的回调没有被执行。但是一个新的问题又被抛了出来——何时这个回调被设置成空了?可以这样设计下:Hook函数NtQueryInformationProcess,使其返回调试端口号一直未0,。然后针对GlobalTopLevelExceptionFilter下硬件断点。或许,这样便可以找到元凶。

        最后附上工程。

        百度云下载地址:http://pan.baidu.com/s/1qWG14BE 。密码:w5o5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值