如何使用内存转储来跟踪 bug






4.75/5 (46投票s)
2003年9月10日
6分钟阅读

275943
如何使用内存转储来跟踪 bug
引言
我们的软件刚刚发布了1.0版本。我的客户经常遇到异常和蓝屏导致程序终止。大多数时候,我们在自己的系统上无法重现该 bug。因此,捕获和分析内存转储文件非常重要。本文将教您如何使用实际操作工具来捕获和读取内存转储文件。
实际操作的工具包括:
- Dr. Watson,几乎所有 Windows 系统都包含它。
- VC6 和 VC7 编译器,我们需要重新编译项目以生成 Map 文件和 COM 文件用于跟踪 bug。
- Dumpchk 可以在您发送转储文件之前对其进行检查。
- Windbg 和 kd 可以提供异常发生时点的详细信息。符号文件对于调试转储文件总是必需的。
详细说明
在重现 bug 之前,必须安装并运行 Dr. Watson 以创建 srwtsn32.log。我们还需要配置项目以生成调试信息、MAPFILE 和 COD 文件,然后重新编译项目。
配置 Dr. Watson
要启动 Dr. Watson 并将其设置为默认调试器,请转到命令行,键入
drwtsn32 –I
如果您想更改设置,可以随时在命令行运行 drwtsn32。默认设置是可以的。然后,如果程序崩溃,Dr. Watson 会写入 drwtsn32.log 并将内存转储到 user.dmp。
在 Visual C++ 环境中重新配置项目设置
您可以准备好源代码,以便 Dr. Watson 和内存转储可以使用。您需要按照以下三个简单步骤进行操作。
- 您需要为 .EXE 和/或 .DLL 文件的 Win32 Release 版本生成调试符号,在项目的设置配置中,对于链接器,打开“项目设置”窗口(在项目菜单中单击设置项),选择链接选项卡,勾选生成 Map 文件,并勾选生成调试信息。对于编译器,在 C/C++ 选项卡下的常规选项中,勾选调试信息:程序数据库。
- 创建并保留应用程序的 .MAP 和 .COD 中间文件的副本以供将来参考。在链接选项卡的项目选项编辑框中,添加开关“/mapinfo:exports”(开关 /mapinfo:exports 会使链接器描述二进制文件的导出函数。)添加开关“/mapinfo:lines”(开关 /mapinfo:lines 会在 mapfile 中添加行号)。.COD 文件包含源代码、汇编代码和内存地址的混合体,它将帮助您通过 .MAP 文件识别出的函数中的出错代码行。在 C/C++ 选项卡常规部分,在项目选项中添加开关“/FAcs”。
- 为您的 .EXE 和/或 .DLL 文件指定固定的加载基址十六进制地址。在链接选项卡的输出下,在基址中输入一个十六进制地址,例如 0x602f0000。
示例
使用 Visual C++ 应用程序向导创建一个名为“MapDemo
”的 MFC Win32 可执行文件。在 CMapDemoApp
类的 InitInstance
函数中添加以下行。
char * pch1 = 0; char * pch2 = 0; pch1 = (char *)malloc( 20 ); pch2 = (char *)malloc( 20 ); pch1 = pch2; free( pch1 ); free( pch2 );
此代码最终会失败,因为应用程序将尝试两次释放同一块内存。运行程序,它会崩溃。打开文件 drwtsn32.log。
*----> Module List <----*
(0000000000400000 - 000000000040b000:
C:\WORK\CODEPROJ\MapDemo\Debug\MapDemo.exe
(0000000010200000 - 000000001026c000: C:\WINDOWS\System32\MSVCRTD.dll
(000000005f400000 - 000000005f4e5000: C:\WINDOWS\System32\MFC42D.DLL
(00000000605d0000 - 00000000605d8000: C:\WINDOWS\System32\mslbui.dll
(0000000070a70000 - 0000000070ad4000: C:\WINDOWS\system32\SHLWAPI.dll
(0000000074720000 - 0000000074764000: C:\WINDOWS\System32\MSCTF.dll
(0000000077120000 - 00000000771ab000: C:\WINDOWS\system32\OLEAUT32.DLL
(00000000771b0000 - 00000000772d1000: C:\WINDOWS\system32\OLE32.DLL
(0000000077c00000 - 0000000077c07000: C:\WINDOWS\system32\VERSION.dll
(0000000077c10000 - 0000000077c63000: C:\WINDOWS\system32\msvcrt.dll
(0000000077c70000 - 0000000077cb0000: C:\WINDOWS\system32\GDI32.dll
(0000000077d40000 - 0000000077dc6000: C:\WINDOWS\system32\USER32.dll
(0000000077dd0000 - 0000000077e5d000: C:\WINDOWS\system32\ADVAPI32.dll
(0000000077e60000 - 0000000077f46000: C:\WINDOWS\system32\kernel32.dll
(0000000077f50000 - 0000000077ff7000: C:\WINDOWS\System32\ntdll.dll
(0000000078000000 - 000000007807f000: C:\WINDOWS\system32\RPCRT4.dll
*----> State Dump for Thread Id 0x3274 <----*
eax=00000001 ebx=7ffdf000 ecx=10261558 edx=00010000
esi=0012febc edi=7ffdf000
eip=10213638 esp=0012fe5c ebp=0012fe6c iopl=0
nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000
efl=00000246
function: MSVCRTD!_free_dbg_lk
10213616 7521 jnz MSVCRTD!_free_dbg_lk+0xc9
(10213639)
10213618 6814732510 push 0x10257314
1021361d 6a00 push 0x0
1021361f 6814040000 push 0x414
10213624 68f4712510 push 0x102571f4
10213629 6a02 push 0x2
1021362b e810180000 call MSVCRTD!_CrtDbgReport (10214e40)
10213630 83c414 add esp,0x14
10213633 83f801 cmp eax,0x1
10213636 7501 jnz MSVCRTD!_free_dbg_lk+0xc9
(10213639)
FAULT ->;10213638 cc int 3
10213639 33c0 xor eax,eax
1021363b 85c0 test eax,eax
1021363d 75c9 jnz MSVCRTD!_free_dbg_lk+0x98
(10213608)
1021363f 8b4d08 mov ecx,[ebp+0x8]
10213642 83e920 sub ecx,0x20
10213645 894dfc mov [ebp-0x4],ecx
10213648 8b55fc mov edx,[ebp-0x4]
1021364b 8b4214 mov eax,[edx+0x14]
1021364e 25ffff0000 and eax,0xffff
10213653 83f804 cmp eax,0x4
*----> Stack Back Trace <----*
*** WARNING: Unable to verify checksum for
C:\WORK\CODEPROJ\MapDemo\Debug\MapDemo.exe
*** ERROR: Symbol file could not be found. Defaulted to
export symbols for C:\WINDOWS\system32\kernel32.dll -
WARNING: Stack unwind information not available. Following
frames may be wrong.
ChildEBP RetAddr Args to Child
0012fe6c 10213541 00324708 00000001 7ffdf000 MSVCRTD!_free_dbg_lk+0xc8
0012fea0 102134ce 00324708 00000001 0012fee8 MSVCRTD!_free_dbg+0x41
0012feb0 00401360 00324708 7ffde000 00324238 MSVCRTD!free+0xe
0012fee8 5f4335c3 7ffdf000 7ffde000 7ffdf000
MapDemo!CMapDemoApp__InitInstance+0x132
0012ff08 00402248 00400000 00000000 001423ac MFC42D!AfxWinMain+0x83
0012ff20 00402153 00400000 00000000 001423ac MapDemo!WinMain+0x18
0012ffc0 77e814c7 7ffdf000 7ffde000 7ffdf000
MapDemo!WinMainCRTStartup+0x1b3
0012fff0 00000000 00401fa0 00000000 78746341
kernel32!GetCurrentDirectoryW+0x44
由于代码在内存释放函数“_free_dbg_lk
”中出错,该函数由 Initinstance()
函数调用,因此我们搜索堆栈跟踪部分,在第四行,它表明子函数是由 InitInstance
函数调用,偏移量为 0x132。
因此,查看 .COD 中间文件“MapDemo.COD”,搜索模块“InitInstance()
”,模块开头的偏移地址为 0x00ae,加上地址(0x132+0xae = 0x01e0),我们就知道程序在源文件“MapDemo.cpp”的第 79 行崩溃了。
另一个示例 - 如何跟踪 DLL 文件中的故障(进程内和进程外 DLL 文件)。
使用 Visual Studio 中的应用程序向导生成一个名为 MapDemoDll 的 DLL 库项目,创建一个导出函数 DllSquareRoot()
,并在该函数中放置一个故障代码:
char* pChar = NULL; *pChar = 'a'; // This is fault line 70
程序将抛出访问无效内存地址(0x0000)的异常。使用应用程序向导生成一个调用 MapDemoDll.dll 中 DllSquareRoot
函数的客户端程序。在运行带有 DLL 库的可执行程序之前,我们需要设置 MapDemoDll 项目的设置。生成 MAP、COD 文件,指定加载基址为 0x60f20000。编译项目并运行程序。
程序崩溃,屏幕显示类似“DemoDllClient.exe 已生成错误,将被 Windows 关闭。您需要重新启动程序。”的信息。以下是 Dr. Watson 生成的 drwsnt32.log。
State Dump for Thread Id 0x7b0
eax=00000000 ebx=005e4b68 ecx=00135dd0 edx=60f35918
esi=0012f27c edi=0012f26c
eip=60f21320 esp=0012f204 ebp=0012f26c iopl=0
nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000
efl=00000246
function: DllSquareRoot
60f212f9 8d7da4 lea edi,[ebp+0xa4]
ss:00c8c83e=????????
60f212fc b917000000 mov ecx,0x17
60f21301 b8cccccccc mov eax,0xcccccccc
60f21306 f3ab rep stosd
es:0012f26c=0012f2cc
60f21308 e8f3020000 call AfxGetStaticModuleState
(60f21600)
60f2130d 50 push eax
60f2130e 8d4df8 lea ecx,[ebp+0xf8]
ss:00c8c83e=????????
60f21311 e8a4010000 call
AFX_MAINTAIN_STATE2::AFX_MAINTAIN_STATE2 (60f214ba)
60f21316 c745f400000000 mov dword ptr [ebp+0xf4],0x0
ss:00c8c83e=????????
60f2131d 8b45f4 mov eax,[ebp+0xf4]
ss:00c8c83e=????????
FAULT ->;60f21320 c60078 mov byte ptr [eax],0x78
ds:00000000=??
60f21323 dd4508 fld qword ptr [ebp+0x8]
ss:00c8c83e=????????????????
60f21326 dc1d4841f360
ds:60f34148=0000000000000000
fcomp qword ptr [_real (60f34148)]
60f2132c dfe0 fstsw
60f2132e f6c401 test ah,0x1
60f21331 7520 jnz _enc$textbss$begin+0x5f43
(60f29e53)
60f21333 8b4d0c mov ecx,[ebp+0xc]
ss:00c8c83e=????????
60f21336 51 push ecx
60f21337 8b5508 mov edx,[ebp+0x8]
ss:00c8c83e=????????
60f2133a 52 push edx
60f2133b e848060000 call sqrt (60f21988)
60f21340 83c408 add esp,0x8
程序在地址 0x60f21230 处崩溃,这是 mapfile 的一部分:
Address Publics by Value Rva+Base Lib:Object
0001:00000050 ?_GetBaseMessageMap@CMapDemoDllApp@@KGPBUAFX_MSGMAP@@XZ
60f21050 f MapDemoDll.obj
0001:00000080 ?GetMessageMap@CMapDemoDllApp@@MBEPBUAFX_MSGMAP@@XZ
60f21080 f MapDemoDll.obj
0001:000000c0 ??0CMapDemoDllApp@@QAE@XZ 60f210c0 f MapDemoDll.obj
0001:00000120 ??_GCMapDemoDllApp@@UAEPAXI@Z 60f21120 f i MapDemoDll.obj
0001:00000120 ??_ECMapDemoDllApp@@UAEPAXI@Z 60f21120 f i MapDemoDll.obj
0001:00000190 ??1CMapDemoDllApp@@UAE@XZ 60f21190 f i MapDemoDll.obj
0001:000002f0 _DllSquareRoot 60f212f0 f MapDemoDll.obj
0001:000003b2 ?WinHelpA@CWinApp@@UAEXKI@Z 60f213b2
f mfc42d:MFC42D.DLL
0001:000003b8 ?OnDDECommand@CWinApp@@UAEHPAD@Z 60f213b8
f mfc42d:MFC42D.DLL
0001:000003be ?DoWaitCursor@CWinApp@@UAEXH@Z 60f213be
f mfc42d:MFC42D.DLL
0001:000003c4 ?DoMessageBox@CWinApp@@UAEHPBDII@Z 60f213c4
f mfc42d:MFC42D.DLL
0001:000003ca ?SaveAllModified@CWinApp@@UAEHXZ 60f213ca
f mfc42d:MFC42D.DLL
0001:000003d0 ?InitApplication@CWinApp@@UAEHXZ 60f213d0
f mfc42d:MFC42D.DLL
小于崩溃地址 0x60f21320 的最大函数地址是 0x60f212f0,它属于 MapDemoDll
对象中的 Dll
SquareRoot 函数。我们也可以通过以下计算:crash_address - preferred_load_address - 0x1000。为什么我们需要减去 0x1000,因为二进制文件的第一部分是可执行文件(PE),长度为 0x1000 字节。因此,计算为:0x60f21320 – 0x60f20000 – 0x1000 = 0x320。
Line numbers for .\Debug\MapDemoDll.obj
(D:\work\MapDemoDll\MapDemoDll.cpp) segment .text
44 0001:00000050 44 0001:00000080
55 0001:000000c0 58 0001:000000f0
63 0001:00000220 66 0001:000002f0
67 0001:00000308 69 0001:00000316
70 0001:0000031d 72 0001:00000323
73 0001:00000333 74 0001:00000353
75 0001:00000361 76 0001:0000037a
以上是 MAPFILE 的信息部分,例如:44 0001:00000050,第一个数字是行号,第二个数字是从该行发生的代码段开始的偏移量。因此,我们将知道程序在 MapDemoDll.cpp 的第 70 行崩溃。该行试图将一个数字写入无效地址。
如何读取和调试内存转储文件
- 获取符号表或符号服务器地址。为了准备调试转储文件,您应该获取 Microsoft 符号服务器地址或从 Microsoft 下载符号(http://www.microsoft.com/whdc/ddk/debugging/symbolpkg.mspx#Windows%20symbol%20packages)。
- 下载 Windows 调试工具。这些调试工具可以读取 Windows 内存转储并模拟原始内存环境。
- 需要从原始安装 CD 安装支持工具。在使用内存转储进行调试之前,我们需要使用 dumpchk.exe 检查转储文件是否损坏,该工具位于 Windows 安装 CD 中。转到命令行并输入:Dumpchk -a [dump file]。
- 如果内存转储文件通过检查,现在我们可以使用 windbg 或 kd 来调试内存转储。安装完成后,启动命令提示符,切换到安装了调试工具的路径。使用以下命令之一将转储文件加载到调试器中。Windbg.exe 是一个基于 Windows 的调试器,kd.exe 是一个命令行调试器。
windbg -y SymbolPath -i ImagePath -z DumpFilePath
kd -y SymbolPath -i ImagePath -z DumpFilePath
SymbolPath 是符号的路径,它是下载了符号文件的本地路径,或者是服务器路径(URL)。ImagePath 是 Windows 映像文件的路径,这些文件包含在 Windows 安装 CD-ROM 的 /I386 文件夹中。DumpPath 是您正在检查的转储文件的路径和文件名。
示例
kd -y SRV*f:\symbols*http://msdl.microsoft.com/download/symbols
-i e:\i386 -z f:\dumps\minidump.dmp
kd -y f:\symbols -i e:\i386 -z "f:\my debug files\minidump.dmp"
windbg -y f:\symbols -i e:\i386 -z "f:\dumps\minidump.dmp"
这两个调试器都接受用于从转储文件中收集信息的命令,例如:
The !analyze -show command displays the Stop error code
(also known as the bug check code) and its parameters.
The !analyze -v command displays verbose output.
The !drivers command displays a list of the drivers that
were loaded on the computer when the problem occurred.