前言
- 本文章主要介绍了如何生成Dump文件,包括两种方式,通过代码生成和通过注册表生成。并且介绍了WinDbg工具的下载和使用,以及如何使用WinDbg工具去静态分析Dump文件,从而找到程序的崩溃位置。
生成Dump文件
-
通过调用WinAPI生成 SetUnhandledExceptionFilter
-
接口说明
*cLPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( [in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
-
参数
- [in] lpTopLevelExceptionFilter
- 指向顶级异常筛选器函数的指针,每当 UnhandledExceptionFilter 函数得到控制时,都会调用该函数,并且进程不会被调试。 此参数的 NULL 值指定 UnhandledExceptionFilter 中的默认处理。
- 筛选器函数的语法与 UnhandledExceptionFilter 的语法类似:它采用类型 为 LPEXCEPTION_POINTERS 的单个参数,具有 WINAPI 调用约定,并返回 LONG 类型的值。 筛选器函数应返回以下值之一。
Value 含义 EXCEPTION_EXECUTE_HANDLER 0x1 从 UnhandledExceptionFilter 返回并执行关联的异常处理程序。 这通常会导致进程终止 EXCEPTION_CONTINUE_EXECUTION 0xffffffff 从 UnhandledExceptionFilter 返回,并从异常点继续执行。 请注意,筛选器函数可通过修改通过其 LPEXCEPTION_POINTERS 参数提供的异常信息来自由修改延续状态。 EXCEPTION_CONTINUE_SEARCH 0x0 继续执行 UnhandledExceptionFilter 的正常执行。 这意味着遵守 SetErrorMode 标志,或调用应用程序错误弹出消息框。 -
返回值
- SetUnhandledExceptionFilter 函数返回使用函数建立的上一个异常筛选器的地址。 NULL 返回值表示没有当前的顶级异常处理程序。
-
注解
- 发出 SetUnhandledExceptionFilter 会替换调用过程中所有现有线程和所有未来线程的现有顶级异常筛选器。
- lpTopLevelExceptionFilter 指定的异常处理程序在导致错误的线程上下文中执行。 这可能会影响异常处理程序从某些异常(如无效堆栈)恢复的能力。
-
测试代码
c#include <windows.h> #include <stdio.h> #include <DbgHelp.h> #pragma comment(lib, "dbghelp.lib") long __stdcall callback(_EXCEPTION_POINTERS* excp) { //创建dump文件 HANDLE hDumpFile = CreateFile(L"fileDump_20230902.dmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); MINIDUMP_EXCEPTION_INFORMATION dumpinfo; dumpinfo.ExceptionPointers = excp; dumpinfo.ThreadId = GetCurrentThreadId(); dumpinfo.ClientPointers = true; //写入dump文件 MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &dumpinfo, NULL, NULL); CloseHandle(hDumpFile); return EXCEPTION_CONTINUE_SEARCH; } void myfunc() { printf("join to myfunc...\n"); //这里程序会崩溃 char* p = NULL; memcpy(p, "hello word", strlen("hello word")); printf("end myfunc"); } void myMath(int a, int b) { int sum = 0; sum = a + b; printf("sum = %d\n", sum); myfunc(); int minus = 0; minus = a - b; printf("sum = %d\n", sum); } int main() { // 捕获异常 SetUnhandledExceptionFilter(callback); myMath(10, 20); system("pause"); return 0; }
- 直接执行程序,就会在你指定的目录下生成对应的dump文件。我是在当前目录下,创建了一个fileDump_20230902.dmp的文件,可以看到,执行程序后,就自动生成了。
-
-
通过注册表生成
-
除了通过代码来生成dump文件,我们还可以直接修改注册表,来生成dump文件。
-
在注册表以下位置HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps,创建一个项 DumpFile.exe,项名字和你可执行程序名字保持一致就行。 然后在对应的项中创建三个值,值的类型和说明参考下图。然后执行以上程序,就会在 DumpFolder 值指定的目录下生成一个dump文件。
-
可以参考下我写的,执行程序崩溃后,就会在这里DumpFolder指定的目录下生成对应的dump文件。
-
测试代码
c#include <windows.h> #include <stdio.h> void myfunc(){ printf("join to myfunc...\n"); //这里程序会崩溃 char* p = NULL; memcpy(p, "hello word", strlen("hello word")); printf("end myfunc"); } void myMath(int a, int b){ int sum = 0; sum = a + b; printf("sum = %d\n", sum); myfunc(); int minus = 0; minus = a - b; printf("sum = %d\n", sum); } int main(){ myMath(10, 20); system("pause"); return 0; }
- 执行程序,可以看到,在指定目录下也生成了一个dump文件
-
WinDbg下载与安装
- 下载地址
- 点击 Transferir o instalador 下载,下载完成后是一个 winsdksetup.exe 安装包。双击进行安装。
- 上面是在线安装,选择下面,先把WinDbg安装包下载到本地。
- 在这个界面,只选择 Debugging Tools for Windows 就可以了。
- 下载完成后,对应目录下会有一个 Installers 目录,进去可以选择32位的安装包进行安装 X86 Debuggers And Tools-x86_en-us.msi。
- 这个安装包是没有安装向导的,直接双击就安装好了。在windows开始菜单栏,找到Windows Kits,下面就有安装好的WinDbg
- 双击打开后,是这个界面。安装就完成了。
WinDbg静态分析dump
- 我们打开winDbg,直接把dump文件托进来,就可以进行分析了。
- 分析异常类型
- 首先可以先分析是哪种异常,主要有以下三种
- Access violation: 内存访问违规
- Integer divided by zero: 除0异常
- Stack overflow: 线程栈溢出
- 直接把dump文件托进来,就可以看到异常类型,是内存访问违规
- 首先可以先分析是哪种异常,主要有以下三种
- 使用.ecxr命令查看异常
- 在下面命令框,输入.ecxr 命令, 可以切换刀发生异常的线程中,并且可以查看发生异常的汇编指令。
- 这里分析需要懂一些汇编的知识,大概可以分析出,MOV是传送的意思,把指针指向一个寄存器EDI中,寄存器的地址是000000,这个地址是不允许用户访问的,因此程序会崩溃。不懂汇编也没关系,可以跳过这一步,继续往下分析。
- 在下面命令框,输入.ecxr 命令, 可以切换刀发生异常的线程中,并且可以查看发生异常的汇编指令。
- 使用kn/kv/kp 查看函数调用堆栈
- kn命令表示把发生崩溃的函数堆栈打印出来。
- 这里感叹号前面代表的是模块名,可以看到最上面是崩溃到了 vcruntime140d这个模块中了,这个大家应该都知道,是c的一个运行时库,memcpy函数就在这个库中,后面还会显示具体的行号信息。
- 可以点击前面的序号,直接可以看到变量值
- 这里还可以通过 File->Source File Path将源代码路径加载进去。双击后面的文件名,就可以在WinDbg中看到对应的源代码了。点击不同的序号,可以跳到对应的位置
- kv和kp命令可以将函数的参数信息也打印出来,可以对比看下
- kv和kp的区别是,kv的参数是以十六进制显示的,且最多显示三位,没有的参数位置,值是无效的
- 我们如果要查看参数,一般直接用kp命令就可以了。
- kn命令表示把发生崩溃的函数堆栈打印出来。
- 使用lm vm命令查看pdb文件
- 使用 lm vm 模块名 命令可以看到对应模块的pdb文件。如果找不到pdb文件,我们要手动加进来。
- 可以通过File->Symbol Search Path 将pdb文件所在路径加载进来。
- 上面的例子,因为是在开发机上调试的,默认是已经加载了pdb文件,如果不是开发机,我们需要手动添加下pdb文件路径,这样才能看到具体的行号信息。
- 比如我们换一个非开发机,查看堆栈信息,测试程序的函数名和行号信息已经看不到了
- 使用lm vm 模块名命令,看不到pdb文件。
- 这个时候就需要将我们程序的pdb文件拷贝到非开发机上,然后把pdb文件所在路径加载进去,重新执行 .ecxr,再执行kn命令,就可以重新显示函数和行号信息了。
- 使用 lm vm 模块名 命令可以看到对应模块的pdb文件。如果找不到pdb文件,我们要手动加进来。