VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)

这篇是调试的最后一篇,也是VS2017的最后一篇。

这一篇主要介绍远程调试。以上两篇介绍的情况都是自己开发自己调试, 而作为软件开发者不可能将系统完全开发到没有bug的程度才去发布。大多数都是一边发布,一边更新。

所以,在我们发布软件到客户手上后,要能够收集用户的崩溃信息,以及能够帮助开发者解决bug的重要数据。

很多软件,包括Windows系统,出现崩溃以后会有个错误汇报窗口,引导你上传错误信息给开发者以便于开发者解决这个bug。这一篇说的就是这种远程调试的方案。

以Windows为例子,假如Windows出现蓝屏或者死机,Windows会保留哪些信息以便于开发者找出崩溃bug?这个是用户可选择的,如下:VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)_第1张图片

上图中的写入调试信息,就是前面提到可以帮助开发者找出bug的文件。这个文件叫dump文件,后缀默认为.DMP。从dump的词义来看,这个文件也是一个副本,至于是什么的副本呢,先看看选择项有哪些:

VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)_第2张图片

这五个选项都是把崩溃时的内存的值写入到硬盘中,只是写入内存的大小不一样而已,所以dmp又叫内存转存储。除了第一个256kb以外,其他的选项都是几百兆或者就是你的内存大小。所以一般都选择使用小内存转储。

为什么需要程序崩溃时的内存信息?在前两章debug过程中很重要的两个信息就是堆栈信息和内存信息,堆栈信息帮我们定位代码崩溃的位置,内存信息帮助我们判断为什么导致崩溃。而小内存转储则包含了程序的堆栈信息,再结合.pdb文件,我们可以马上定位到程序的崩溃位置在代码的哪行。很遗憾的是小内存下几乎没有任何内存信息,但也非常有帮助了。

下面,先介绍怎么让自己的程序崩溃后生成dmp文件。

生成dmp文件只需要两个Windows的API函数:SetUnhandledExceptionFilter和MiniDumpWriteDump。

SetUnhandledExceptionFilter:注册一个没有处理的异常的回调函数。没有处理的异常,Unhandled Exception。这个其实说明异常都是可以被开发者处理的,就是try{}catch{}结构。如果你在catch里面完成了异常的处理,不影响后面程序的运行,那么你的程序是可以不崩溃的。但这也不代表着可以什么都用try{}catch{}来避免程序崩溃。你如果把所有的Exception都处理了,你的程序是不会崩溃,但是前提是你都处理的正确。如果处理不正确有可能出现的情况就是暂时没问题,突然在莫名的地方异常或者状态错乱,这样会严重影响你debug真正的异常。还不如第一时间崩溃然后修复这个bug。除非你们有完整的try catch throw的架构,否则不要轻易使用这个来避免崩溃。修复bug才是王道。详情见:https://msdn.microsoft.com/query/dev15.query?appId=Dev15IDEF1&l=ZH-CN&k=k%28SetUnhandledExceptionFilter%29%3Bk%28DevLang-C%2B%2B%29%3Bk%28TargetOS-Windows%29&rd=true&f=255&MSPPError=-2147217396

MiniDumpWriteDump:这个函数就是根据当前的崩溃信息,把选择的内存信息写入到一个dmp文件当中。这个库需要链接Dbghelp.lib。详情见:https://msdn.microsoft.com/zh-cn/dynamics/ms680360(v=vs.90)

下面是调用代码:

#include //SetUnhandledExceptionFilter
#include //MiniDumpWriteDump
#include //FILE

LONG WINAPI ExceptionHandler(_In_ struct _EXCEPTION_POINTERS *ExceptionInfo)
{
	MessageBoxA(NULL, "Crash", "Error", MB_OK);
	//获取当前进程HANDLE
	HANDLE hproc = GetCurrentProcess();
	//获取当前进程ID
	int proId = GetCurrentProcessId();
	//以Windows API的方式创建一个文件句柄,理解为指向这个新建文件的指针吧。
	//下面的参数可以在MSDN上查询
	//第一个参数是文件名,根据自己需求改,后面参数没什么改的。
	HANDLE hfile = CreateFile("test.DMP", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hfile)
	{
		_MINIDUMP_EXCEPTION_INFORMATION minidumpInfo;
		//If you are accessing local memory (in the calling process) you should not set this member to TRUE.
		//上面MSDN的介绍,如果在同一个进程就设为FALSE
		minidumpInfo.ClientPointers = FALSE;
		//这是传入的参数,保存崩溃信息,ESI几个寄存器的值之类的
		minidumpInfo.ExceptionPointers = ExceptionInfo;
		//当前线程ID
		minidumpInfo.ThreadId = GetCurrentThreadId();
		//上面都没啥改的,实际用的时候根据你需要的内存信息改MiniDumpNormal这个参数就行了
		//同样用的时候可以去MSDN上查询各个传参的值和作用
		MiniDumpWriteDump(hproc, GetCurrentProcessId(), hfile, MiniDumpNormal, &minidumpInfo,NULL,NULL);
		CloseHandle(hfile);
        //这里把.dmp文件发送给服务器
	}
//这里可以加上异常后的处理,重启啊什么的
	
	return 0;
}

int realmain()
{
	FILE*fp;//测试代码,新手经常会出现的打开文件失败,访问空指针崩溃
	fopen_s(&fp, "111", "r");//打开一个不存在的文件
	fprintf_s(fp, "Crash");//这里会空指针崩溃
	return 0;
}

int main()
{
	SetUnhandledExceptionFilter(ExceptionHandler);//设置崩溃的回调
	return realmain();
}

上面代码是我根据MSDN上面的说明写的,如果你不清楚某个WindowsAPI函数该怎么用,最简单的方法把这个函数写出来,鼠标点击选中高亮,然后按F1,会自动到MSDN的对应介绍。

这样我们运行程序就肯定会崩溃,如下:

VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)_第3张图片

其实这里已经说了是空指针异常。这种崩溃是已经被Windows预料到写进了Assertion里,所以会弹出这个框。没有Assertion的时候,就会闪退了。点击重试就是进入异常回调,其他都是直接退出。这时候test.DMP就生成了。先打开看看,这里我们直接把test.DMP拖进VS用VS打开。.DMP文件本来应该被WinDbg解析的,但由于我们用VS编译的用VS解析会更方便。打开如下:

VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)_第4张图片

点击红框内的进行调试,VS就会自动定位到崩溃的那行代码了:

VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)_第5张图片

说明下:因为我的工程信息都没变,DMP里面记录的代码路径信息和.PDB的位置都没改变,所以直接打开就能找到匹配的代码。如果你更改过某个路径,那么它会提示你找不到xxx.pdb,请选择.pdb文件的位置。这时你只要把这个程序一起生成的.pdb文件定位给VS就可以查看崩溃代码了。

还有最关键的一步:有些异常在Debug配置能被捕捉,但是Release下却捕捉不到异常。全部因为下面这个运行库设置,改成多线程调试就行了:

VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下)_第6张图片

这样就没问题了,通过Release配置发布程序提供客户下载(.PDB文件保留在自己这里不要发布)。如果程序崩溃,写minidump文件并且发送给自己的服务器接收。然后下载到本地通过.pdb链接定位到崩溃代码调试崩溃。如果有些崩溃一眼看不出来,那么就相应写一些调试相关的日志发给服务器辅助Debug。

 

 

你可能感兴趣的:(VS2017的C++开发心得(十三)调试——内存溢出和内存泄漏(下))