Windows C++程序运行过程中生成dump文件

发布于:2022-11-09 ⋅ 阅读:(15) ⋅ 点赞:(0) ⋅ 评论:(0)


前言

开发Windows C++程序跟踪异常是比较重要的功能,一般情况都需要进行全局异常捕获,并且生成dump文件。而且C++没有运行时直接获取堆栈信息的方法,返回错误或者异常处理时无法记录到堆栈信息,如果这个时候能够生成dump,对于bug分析是非常有利的。


一、如何实现

1、捕获全局异常

我们使用SetUnhandledExceptionFilter方法注册回调来捕获全局异常。

#include <Windows.h>
//全局异常捕获回调方法
LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS lpExceptionInfo)
{
	printf("发生全局异常\n"));
	return EXCEPTION_EXECUTE_HANDLER;
}
void main()
{
  //设置全局异常捕获回调
  SetUnhandledExceptionFilter(ExceptionFilter);
}

2、基于异常生成dump

我们使用MiniDumpWriteDump方法生成dump文件,此方法依赖于EXCEPTION_POINTERS对象,通常在全局异常捕获回调中可以获取。

#include <Windows.h>
#include <DbgHelp.h>
#pragma comment(lib,"Dbghelp.lib")
/ <summary>
/// 生成dmp文件
/// </summary>
/// <param name="exceptionPointers">异常信息</param>
/// <param name="path">文件路径(包括文件名)</param>
/// <returns></returns>
static int GenerateDump(EXCEPTION_POINTERS* exceptionPointers, const std::string& path)
{
	HANDLE hFile = ::CreateFileA(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (INVALID_HANDLE_VALUE != hFile)
	{
		MINIDUMP_EXCEPTION_INFORMATION minidumpExceptionInformation;
		minidumpExceptionInformation.ThreadId = GetCurrentThreadId();
		minidumpExceptionInformation.ExceptionPointers = exceptionPointers;
		minidumpExceptionInformation.ClientPointers = TRUE;
		bool isMiniDumpGenerated = MiniDumpWriteDump(
			GetCurrentProcess(),
			GetCurrentProcessId(),
			hFile,
			MINIDUMP_TYPE::MiniDumpNormal,
			&minidumpExceptionInformation,
			nullptr,
			nullptr);
		CloseHandle(hFile);
		if (!isMiniDumpGenerated)
		{
			printf("MiniDumpWriteDump failed\n");
		}
	}
	else
	{
		printf("Failed to create dump file\n");
	}
	return EXCEPTION_EXECUTE_HANDLER;
}

3、任意时刻生成dump

在任意时刻生成dump,有一种方法是主动触发异常在异常捕获中获取EXCEPTION_POINTERS对象,继而调用MiniDumpWriteDump生成dump。我们使用RaiseException触发一个异常,并使用msvc的__try、__except进行异常捕获。

#include <Windows.h>
/// <summary>
/// 获取内存快照转储,可以任意时刻记录(包括非崩溃情况下)。
/// </summary>
/// <param name="path">保存文件名</param>
void Snapshot(const std::string& path)
{
	__try
	{
		//通过触发异常获取堆栈
		RaiseException(0xE0000001, 0, 0, 0);
	}
	__except (GenerateDump(GetExceptionInformation(), path)) {}
}

二、完整代码

将上述实现封装在一个对象中。
DumpHelper.h

#pragma once
#include<string>
//
// Created by xin on 2022/9/12.
//
namespace AC {
	class  DumpHelper
	{
	public:
		/// <summary>
		/// 设置是否记录崩溃转储
		/// 默认否
		/// </summary>
		/// <param name="value">是否记录崩溃转储</param>
		static void SetIsDumpCrash(bool value);
		/// <summary>
		/// 设置崩溃转储路径
		/// 默认为".\\"
		/// </summary>
		/// <param name="directory">崩溃转储路径</param>
		static void SetDumpDirectory(const std::string& directory);
		/// <summary>
		/// 崩溃转储文件数量
		/// 超过数量自动删除旧文件
		/// </summary>
		/// <param name="count">最大文件数量,默认500</param>
		static void SetDumpMaxFileCount(int count);
		/// <summary>
		/// 获取内存快照转储,可以任意时刻记录(包括非崩溃情况下)。
		/// </summary>
		/// <param name="path">保存文件名</param>
		static void Snapshot(const std::string& path);
	};
}

DumpHelper.cpp

#include "DumpHelper.h"
#include <Windows.h>
#include "time.h"
#include <vector>
#include <DbgHelp.h>
#pragma comment(lib,"Dbghelp.lib")
#ifdef _WIN32
#include <io.h>
#include <direct.h> 
#else
#include <unistd.h>
#include <sys/stat.h>
#endif
#include <stdint.h>
#include <string>
#define MAX_PATH_LEN 256
#ifdef _WIN32
#define ACCESS(fileName,accessMode) _access(fileName,accessMode)
#define MKDIR(path) _mkdir(path)
#else
#define ACCESS(fileName,accessMode) access(fileName,accessMode)
#define MKDIR(path) mkdir(path,S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
#endif
namespace AC {
	static std::string _directory = ".\\";
	static int _fileCount = 500;
	//获取文件夹下的所有文件名
	static void GetFolderFiles(const std::string& path, std::vector<std::string>& files)
	{
		//文件句柄  
		intptr_t hFile = 0;
		//文件信息  
		struct _finddata_t fileinfo;
		std::string p;
		if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1)
			// "\\*"是指读取文件夹下的所有类型的文件,若想读取特定类型的文件,以png为例,则用“\\*.png”
		{
			do
			{
				//如果是目录,迭代之  
				//如果不是,加入列表  
				if ((fileinfo.attrib & _A_SUBDIR))
				{
					if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)
						GetFolderFiles(p.assign(path).append("\\").append(fileinfo.name), files);
				}
				else
				{
					files.push_back(path + "\\" + fileinfo.name);
				}
			} while (_findnext(hFile, &fileinfo) == 0);
			_findclose(hFile);
		}
	}
	//生成多级目录,中间目录不存在则自动创建
	static bool CreateMultiLevelDirectory(const std::string& directoryPath) {
		uint32_t dirPathLen = directoryPath.length();
		if (dirPathLen > MAX_PATH_LEN)
		{
			return false;
		}
		char tmpDirPath[MAX_PATH_LEN] = { 0 };
		for (uint32_t i = 0; i < dirPathLen; ++i)
		{
			tmpDirPath[i] = directoryPath[i];
			if (tmpDirPath[i] == '\\' || tmpDirPath[i] == '/')
			{
				if (ACCESS(tmpDirPath, 0) != 0)
				{
					int32_t ret = MKDIR(tmpDirPath);
					if (ret != 0)
					{
						return false;
					}
				}
			}
		}
		return true;
	}
	/// <summary>
	/// 生成dmp文件
	/// </summary>
	/// <param name="exceptionPointers">异常信息</param>
	/// <param name="path">文件路径(包括文件名)</param>
	/// <returns></returns>
	static int GenerateDump(EXCEPTION_POINTERS* exceptionPointers, const std::string& path)
	{
		HANDLE hFile = ::CreateFileA(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
		if (INVALID_HANDLE_VALUE != hFile)
		{
			MINIDUMP_EXCEPTION_INFORMATION minidumpExceptionInformation;
			minidumpExceptionInformation.ThreadId = GetCurrentThreadId();
			minidumpExceptionInformation.ExceptionPointers = exceptionPointers;
			minidumpExceptionInformation.ClientPointers = TRUE;
			bool isMiniDumpGenerated = MiniDumpWriteDump(
				GetCurrentProcess(),
				GetCurrentProcessId(),
				hFile,
				MINIDUMP_TYPE::MiniDumpNormal,
				&minidumpExceptionInformation,
				nullptr,
				nullptr);

			CloseHandle(hFile);
			if (!isMiniDumpGenerated)
			{
				printf("MiniDumpWriteDump failed\n");
			}
		}
		else
		{
			printf("Failed to create dump file\n");
		}
		return EXCEPTION_EXECUTE_HANDLER;
	}
	//全局异常捕获回调
	LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS lpExceptionInfo)
	{
		time_t t;
		struct tm* local;
		char path[1024] = { 0 };
		char ext[MAX_PATH] = { 0 };
		t = time(NULL);
		local = localtime(&t);
		//创建目录
		if (!CreateMultiLevelDirectory(_directory))
		{
			printf("Failed to create directory %s\n", _directory.c_str());
			return EXCEPTION_EXECUTE_HANDLER;
		}	
		//清除多出的文件
		std::vector<std::string> files;
		std::vector<std::string> dmpFiles;
		GetFolderFiles(_directory, files);
		for (auto i : files)
		{
			_splitpath(i.c_str(), NULL, NULL, NULL, ext);
			if (strcmp(ext, "dmp")==0)
			{
				dmpFiles.push_back(i);
			}
		}
		if (dmpFiles.size() >= _fileCount)
		{	
			if (strcmp(ext, "dmp") == 0 && !DeleteFileA(dmpFiles.front().c_str()))
			{
				printf("Failed to delete old file %s\n", dmpFiles.front().c_str());
			}
		}
		//生成文件名
		sprintf(path, "%s%d%02d%02d%02d%02d%02d.dmp", _directory.c_str(), 1900 + local->tm_year, local->tm_mon + 1, local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec);
		if (strlen(path) > MAX_PATH)
		{
			printf("File path was too long! %s\n",path);
			return EXCEPTION_EXECUTE_HANDLER;
		}
		//生成dmp文件
		return GenerateDump(lpExceptionInfo, path);
	}
	void DumpHelper::SetIsDumpCrash(bool value)
	{
		if (value)
			SetUnhandledExceptionFilter(ExceptionFilter);
		else
		{
			SetUnhandledExceptionFilter(NULL);
		}
	}
	void DumpHelper::SetDumpDirectory(const std::string& directory)
	{
		_directory = directory;
		if (_directory.size() < 1)
		{
			_directory = ".\\";
		}
		else if (_directory.back() != '\\')
		{
			_directory.push_back('\\');
		}
	}
	void DumpHelper::SetDumpMaxFileCount(int count)
	{
		_fileCount = count;
	}
	void DumpHelper::Snapshot(const std::string& path)
	{
		__try
		{
			//通过触发异常获取堆栈
			RaiseException(0xE0000001, 0, 0, 0);
		}
		__except (GenerateDump(GetExceptionInformation(), path)) {}
	}
}

三、使用示例

1、全局异常捕获

#include"DumpHelper.h"
int main(int argc, char** argv) {
	//启动全局异常捕获生成dump
	AC::DumpHelper::SetIsDumpCrash(true);
	//设置dump文件保存目录
	AC::DumpHelper::SetDumpDirectory("dmp");
	//设置最大dump文件数,超过会自动删除最旧的文件
	AC::DumpHelper::SetDumpMaxFileCount(30);
	//触发一个异常
	int a = 0;
	int b = 5 / a;
	return 0;
}

运行之后就会产生一个dump文件了
在这里插入图片描述
使用vs打开dump并使用本机进行调试就能看到异常代码和堆栈了。(需要注意pdb与exe以及代码相匹配)
在这里插入图片描述

2、生成内存快照

调用Snapshot生成当前内存快照

#include"DumpHelper.h"
int main(int argc, char** argv) {
	AC::DumpHelper::Snapshot("current.dmp");
	return 0;
}

在这里插入图片描述
使用vs调试dump,异常点在我们生成dump的代码中。

在这里插入图片描述
通过堆栈找到上一层代码
在这里插入图片描述


总结

以上就是今天要讲的内容,生成dump其实是比较基本的Windows开发所需具备的技能,当然通常都会用于全局异常捕获,但有时使用了try-catch也能在catch生成dump而不让程序奔溃。总的来说,dump文件还是很有用的当然也是有着一些限制比如要有相应pdb以及和代码和可执行程序版本必须对应。