前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分析两种Dump(崩溃日志)文件生成的方法及比较

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

作者头像
方亮
发布2019-01-16 14:24:25
1.4K0
发布2019-01-16 14:24:25
举报
文章被收录于专栏:方亮方亮

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

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

代码语言:javascript
复制
SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);

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

代码语言:javascript
复制
#include "CreateDump.h"

#include <atlbase.h>
#include <atlstr.h>
#include <strsafe.h>
#include <DbgHelp.h>
#pragma comment(lib,"DbgHelp.lib")
#define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS        (0x00000004)
#define MiniDumpWithThreadInfo 0x1000

typedef BOOL (WINAPI *PGetModuleHandleEx)( DWORD dwFlags, LPCTSTR lpModuleName, HMODULE *phModule );

VOID CreateDump(struct _EXCEPTION_POINTERS *pExceptionPointers) 
{
	//收集信息
	CStringW strBuild;
	strBuild.Format(L"Build: %s %s", __DATE__, __TIME__);
	CString strError;
	HMODULE hModule;
	WCHAR szModuleName[MAX_PATH] = {0};

	PGetModuleHandleEx pFun = (PGetModuleHandleEx)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "GetModuleHandleExW");
	if ( !pFun ) {
		return;
	}

	pFun(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)pExceptionPointers->ExceptionRecord->ExceptionAddress, &hModule);
	GetModuleFileName(hModule, szModuleName, ARRAYSIZE(szModuleName));
	strError.Format(L"%s %d , %d ,%d.", szModuleName,pExceptionPointers->ExceptionRecord->ExceptionCode, pExceptionPointers->ExceptionRecord->ExceptionFlags, pExceptionPointers->ExceptionRecord->ExceptionAddress);

	//生成 mini crash dump
	BOOL bMiniDumpSuccessful;
	WCHAR szPath[MAX_PATH]; 
	WCHAR szFileName[MAX_PATH]; 
	WCHAR* szAppName = L"DumpFile";
	WCHAR* szVersion = L"v1.0";
	DWORD dwBufferSize = MAX_PATH;
	HANDLE hDumpFile;
	SYSTEMTIME stLocalTime;
	MINIDUMP_EXCEPTION_INFORMATION ExpParam;
	GetLocalTime( &stLocalTime );
	GetTempPath( dwBufferSize, szPath );
	StringCchPrintf( szFileName, MAX_PATH, L"%s%s", szPath, szAppName );
	CreateDirectory( szFileName, NULL );
	StringCchPrintf( szFileName, MAX_PATH, L"%s%s//%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp", 
		szPath, szAppName, szVersion, 
		stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay, 
		stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond, 
		GetCurrentProcessId(), GetCurrentThreadId());
	hDumpFile = CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE, 
		FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);

	MINIDUMP_USER_STREAM UserStream[2];
	MINIDUMP_USER_STREAM_INFORMATION UserInfo;
	UserInfo.UserStreamCount = 1;
	UserInfo.UserStreamArray = UserStream;
	UserStream[0].Type = CommentStreamW;
	UserStream[0].BufferSize = strBuild.GetLength()*sizeof(WCHAR);
	UserStream[0].Buffer = strBuild.GetBuffer();
	UserStream[1].Type = CommentStreamW;
	UserStream[1].BufferSize = strError.GetLength()*sizeof(WCHAR);
	UserStream[1].Buffer = strError.GetBuffer();

	ExpParam.ThreadId = GetCurrentThreadId();
	ExpParam.ExceptionPointers = pExceptionPointers;
	ExpParam.ClientPointers = TRUE;

	MINIDUMP_TYPE MiniDumpWithDataSegs = (MINIDUMP_TYPE)(MiniDumpNormal 
		| MiniDumpWithHandleData 
		| MiniDumpWithUnloadedModules 
		| MiniDumpWithIndirectlyReferencedMemory 
		| MiniDumpScanMemory 
		| MiniDumpWithProcessThreadData 
		| MiniDumpWithThreadInfo);

	bMiniDumpSuccessful = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), 
		hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL);

	return;
}

        可以见得,我们生成dump文件必须一个结构体——_EXCEPTION_POINTERS。

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

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

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

代码语言:javascript
复制
#pragma once 

class A;

void fcn( A* );

class A
{
public:
	virtual void f() = 0;
	A() { fcn( this ); }
};

class B : A
{
	void f() { }
};

void fcn( A* p )
{
	p->f();
}

// The declaration below invokes class B's constructor, which
// first calls class A's constructor, which calls fcn. Then
// fcn calls A::f, which is a pure virtual function, and
// this causes the run-time error. B has not been constructed
// at this point, so the B::f cannot be called. You would not
// want it to be called because it could depend on something
// in B that has not been initialized yet.

int PureVirtualFunc()
{
	B b;
	return 0;
}

        这个例子将协助我们研究如何截取这种无法使用SetUnhandledExceptionFilter截取的dump。

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

代码语言:javascript
复制
 LONG WINAPI DumpCallback(_EXCEPTION_POINTERS* excp) {
	CreateDump(excp);
	return EXCEPTION_EXECUTE_HANDLER;   
 }
……
SetUnhandledExceptionFilter(DumpCallback);
int *p = NULL;
*p = 1;

        我们查看调用堆栈

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

代码语言:javascript
复制
LONG WINAPI UnhandledExceptionFilter(
  _In_  struct _EXCEPTION_POINTERS *ExceptionInfo
);

        那么,我们可以猜测,如果我们可以接管该函数,可能可以让我们捕获R6025这样的异常。我使用detours库Hook了这个函数

代码语言:javascript
复制
#include "AutoDump.h"
#include <windows.h>
#include "../detours/detours.h"
#include "CreateDump.h"

LONG WINAPI NewUnhandledExceptionFilter( struct _EXCEPTION_POINTERS *ExceptionInfo ){
	OutputDebugString(L"NewUnhandledExceptionFilter\n");

	CreateDump(ExceptionInfo);
	return EXCEPTION_EXECUTE_HANDLER;
}

CAutoDump::CAutoDump(void)
{
	m_lpUnhandledExceptionFilter = NULL;
	do {
		SetErrorMode(SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);

		m_lpUnhandledExceptionFilter = DetourFindFunction( "KERNEL32.DLL", "UnhandledExceptionFilter" );

		if ( NULL == m_lpUnhandledExceptionFilter ) {
			break;
		}
		LONG lRes = NO_ERROR;
		lRes = DetourTransactionBegin();
		if ( NO_ERROR != lRes ) {
			break;
		}

		lRes = DetourAttach( &m_lpUnhandledExceptionFilter, NewUnhandledExceptionFilter );
		if ( NO_ERROR != lRes ) {
			break;
		}

		lRes = DetourTransactionCommit();
		if ( NO_ERROR != lRes ) {
			break;
		}
	} while (0);
}


CAutoDump::~CAutoDump(void)
{
	if ( m_lpUnhandledExceptionFilter ) {
		do {
			LONG lRes = NO_ERROR;
			lRes = DetourTransactionBegin();
			if ( NO_ERROR != lRes ) {
				break;
			}

			lRes = DetourDetach( &m_lpUnhandledExceptionFilter, NewUnhandledExceptionFilter );
			if ( NO_ERROR != lRes ) {
				break;
			}

			lRes = DetourTransactionCommit();
			if ( NO_ERROR != lRes ) {
				break;
			}
		} while (0);
	}
}

        结果,这种方式,便可以截获R6025这样的CRT错误。

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

代码语言:javascript
复制
LPTOP_LEVEL_EXCEPTION_FILTER
WINAPI
SetUnhandledExceptionFilter(IN LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter)
{
    PVOID EncodedPointer, EncodedOldPointer;

    EncodedPointer = RtlEncodePointer(lpTopLevelExceptionFilter);
    EncodedOldPointer = InterlockedExchangePointer((PVOID*)&GlobalTopLevelExceptionFilter,
                                            EncodedPointer);
    return RtlDecodePointer(EncodedOldPointer);
}

        从上述代码中,我们可以见到,系统通过原子操作保存了我们设置的回调函数。然后在UnhandledExceptionFilter函数内部,是这样调用我们设置的回调函数的(依然以ReactOs为例)

代码语言:javascript
复制
……
   RealFilter = RtlDecodePointer(GlobalTopLevelExceptionFilter);
   if (RealFilter)
   {
      LONG ret = RealFilter(ExceptionInfo);
      if (ret != EXCEPTION_CONTINUE_SEARCH)
         return ret;
   }
……

        找到这个锚点,我们便可以动态调试,找出回调函数没有被调用的原因。

代码语言:javascript
复制
75BF76D3  mov         dword ptr [ebp-20h],6  
75BF76DA  xor         esi,esi  
75BF76DC  mov         dword ptr [ebp-1Ch],esi  
75BF76DF  mov         dword ptr [ebp-24h],esi  
75BF76E2  mov         dword ptr [ebp-28h],esi  
75BF76E5  mov         ebx,dword ptr [ebp+8]  
75BF76E8  mov         eax,dword ptr [ebx]  
75BF76EA  test        byte ptr [eax+4],10h  
75BF76EE  jne         _UnhandledExceptionFilter@4+29h (75BF7934h)  
75BF76F4  mov         dword ptr [ebp-2Ch],1  
75BF76FB  cmp         dword ptr [eax],0C0000409h  
75BF7701  je          _UnhandledExceptionFilter@4+3Fh (75BF8146h)  
75BF7707  push        ebx  
75BF7708  call        _CheckForReadOnlyResourceFilter@4 (75BF78B9h)  
75BF770D  cmp         eax,0FFFFFFFFh  
75BF7710  je          _UnhandledExceptionFilter@4+91h (75BF793Bh)  
75BF7716  call        _BasepIsDebugPortPresent@0 (75BF7831h)  
75BF771B  test        eax,eax  
75BF771D  jne         _UnhandledExceptionFilter@4+29h (75BF7934h)  
75BF7723  mov         esi,75CA030Ch  
75BF7728  push        esi  
75BF7729  call        dword ptr [__imp__RtlAcquireSRWLockExclusive@4 (75BD034Ch)]  
75BF772F  push        dword ptr ds:[75CA0074h]  
75BF7735  call        dword ptr [__imp__RtlDecodePointer@4 (75BD0670h)]  
75BF773B  mov         edi,eax  
75BF773D  test        edi,edi 

        调试时,需要注意:当运行到75BF771D时,我们要将执行路径指向75BF7723。因为我们是debug状态,要跳过这个检测。然后我们继续执行,会发现75BF7735处执行的结果是0,即我们获取的回调函数执行为空。这样便分析出,为什么SetUnhandledExceptionFilter方法设置的回调没有被执行。但是一个新的问题又被抛了出来——何时这个回调被设置成空了?可以这样设计下:Hook函数NtQueryInformationProcess,使其返回调试端口号一直未0,。然后针对GlobalTopLevelExceptionFilter下硬件断点。或许,这样便可以找到元凶。

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2014年04月08日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档