http://blog.csdn.net/Eric_Jo/archive/2009/04/14/4072699.aspx
作 为 程序 员 ,我 们 平 时 最担心 见 到的事情是什 么 ?是内存泄漏?是界面不好看? …… 错 啦!我相信我的看法是不会有人反 对 的 —— 那就是,程序 发 生了崩 溃 ! “ 该 程序 执 行了非法操作,即将 关闭 。 请 与你的 软 件供 应 商 联 系。 ” ,呵呵, 这 句 M$ 的 “ 名言 ” ,恐怕就是程序 员 最担心 见 到的 东 西了。有的 时 候,自己的程序在自己的机器上运行得好好的,但是到了 别 人的机器上就崩 溃 了;有 时 自己在 编 写和 测试 的 过 程中就莫名其妙地遇到了非法操作,但是却无法确定到底是源代 码 中的哪行引起的 …… 是不是很痛苦呢?不要 紧 ,本文可以帮助你走出 这种 困境,甚至你从此之后可以自豪地要求用 户 把崩 溃 地址告 诉 你,然后你就可以精确地定位到源代 码 中出 错 的那行了。(很神奇吧?呵呵。) 首先我必 须强调 的是,本方法可以在目前市面上任意一款 编译 器上面使用。但是我只熟悉 M$ 的 VC 和 MASM ,因此后面的部分只介 绍 如何在 这 两个 编译 器中 实现 , 请读 者自行融会 贯 通,掌握在 别 的 编译 器上使用的方法。
首先必 须 生成程序的 MAP 文件。什 么 是 MAP 文件? 简单 地 讲 , MAP 文件是程序的全局符号、源文件和代 码 行号信息的唯一的文本表示方法,它可以在任何地方、任何 时 候使用,不需要有 额 外的程序 进 行支持。而且, 这 是唯一能找出程序崩 溃 的地方的救星。 我 们应该 如何生成 MAP 文件呢?在 VC 中,我 们 可以按下 Alt+F7 ,打 开 “ Project Settings” 选项页 , 选择 C/C++ 选项 卡,并在最下面的 Project Options 里面 输 入: /Zd ,然后要 选择 Link 选项 卡, 选 中“ Generate mapfile” 复选 框,并在最下面的 Project Options 里面 输 入: /mapinfo:lines ,表示生成 MAP 文件 时 ,加入行信息。最后按下 F7 来 编译 生成 EXE 可 执 行文件和 MAP 文件,此 时 可以在工程的 Debug 目 录 下找到 刚刚 生成的 MAP 文件,文件名 为 “工程名 .map” 。 在 MASM 中,我 们 要 设 置 编译 和 连 接参数,我通常是 这样 做的: rc %1.rc ml /c /coff /Zd %1.asm link /subsystem:windows /mapinfo:exports /mapinfo:lines /map:%1.map %1.obj %1.res 把它保存成 makem.bat ,就可以在命令行 输 入 makem filename 来 编译 生成 EXE 可 执 行文件和 MAP 文件了。 在此我先解 释 一下加入的参数的含 义 : /Zd 表示在 编译 的 时 候生成行信息 /map[:filename] 表示生成 MAP 文件的路径和文件名 /mapinfo:lines 表示生成 MAP 文件 时 ,加入行信息 /mapinfo:exports 表示生成 MAP 文件 时 ,加入 exported functions (如果生成的是 DLL 文件, 这 个 选项 就要加上) OK ,通 过 上面的 步骤 ,我 们 已 经 得到了 MAP 文件,那 么 我 们该 如何利用它呢? 让 我 们 从 简单 的 实 例入手, 请 打 开 你的 VC ,新建 这样 一个文件: 01 //**************************************************************** 02 // 程序名称:演示如何通 过 崩 溃 地址找出源代 码 的出 错 行 03 // 作者:Eric 04 // 日期:2003-2-7 05 // 出 处 :Eric的空间
06 // 本程序会 产 生“ 除0 错误 ” ,以至于会 弹 出“ 非法操作” 对话 框。 07 //“ 除 0 错误 ” 只会在 Debug 版本下 产 生,本程序 为 了演示而尽量 简 化。 08 // 注意事 项 :如欲 转载 , 请 保持本程序的完整,并注明: 09 // 转载 自“亿人博览 ” 10 //**************************************************************** 11 12 void Crash(void) 13 { 14 int i = 1; 15 int j = 0; 16 i /= j; 17 } 18 19 void main(void) 20 { 21 Crash(); 22 } 很 显 然本程序有“ 除0 错误 ” ,在 Debug 方式下 编译 的 话 ,运行 时 肯定会 产 生“ 非法操作” 。 好, 让 我 们 运行它,果然, “ 非法操作 ” 对话 框出 现 了, 这时 我 们 点 击 “ 详细 信息 ” 按 钮 , 记录 下 产 生崩 溃 的地址 —— 在我的机器上是 0x0040104a 。 再看看它的 MAP 文件:(由于文件内容太 长 ,中 间 没用的部分我 进 行了省略) CrashDemo Timestamp is 3e430a76 (Fri Feb 07 09:23:02 2003) Preferred load address is 00400000 Start Length Name Class 0001:00000000 0000de04H .text CODE 0001:0000de04 0001000cH .textbss CODE 0002:00000000 00001346H .rdata DATA 0002:00001346 00000000H .edata DATA 0003:00000000 00000104H .CRT$XCA DATA 0003:00000104 00000104H .CRT$XCZ DATA 0003:00000208 00000104H .CRT$XIA DATA 0003:0000030c 00000109H .CRT$XIC DATA 0003:00000418 00000104H .CRT$XIZ DATA 0003:0000051c 00000104H .CRT$XPA DATA 0003:00000620 00000104H .CRT$XPX DATA 0003:00000724 00000104H .CRT$XPZ DATA 0003:00000828 00000104H .CRT$XTA DATA 0003:0000092c 00000104H .CRT$XTZ DATA 0003:00000a30 00000b93H .data DATA 0003:000015c4 00001974H .bss DATA 0004:00000000 00000014H .idata$2 DATA 0004:00000014 00000014H .idata$3 DATA 0004:00000028 00000110H .idata$4 DATA 0004:00000138 00000110H .idata$5 DATA 0004:00000248 000004afH .idata$6 DATA Address Publics by Value Rva+Base Lib:Object 0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj 0001:00000070 _main 00401070 f CrashDemo.obj 0004:00000000 __IMPORT_DESCRIPTOR_KERNEL32 00424000 kernel32:KERNEL32.dll 0004:00000014 __NULL_IMPORT_DESCRIPTOR 00424014 kernel32:KERNEL32.dll 0004:00000138 __imp__GetCommandLineA@0 00424138 kernel32:KERNEL32.dll 0004:0000013c __imp__GetVersion@0 0042413c kernel32:KERNEL32.dll 0004:00000140 __imp__ExitProcess@4 00424140 kernel32:KERNEL32.dll 0004:00000144 __imp__DebugBreak@0 00424144 kernel32:KERNEL32.dll 0004:00000148 __imp__GetStdHandle@4 00424148 kernel32:KERNEL32.dll 0004:0000014c __imp__WriteFile@20 0042414c kernel32:KERNEL32.dll 0004:00000150 __imp__InterlockedDecrement@4 00424150 kernel32:KERNEL32.dll 0004:00000154 __imp__OutputDebugStringA@4 00424154 kernel32:KERNEL32.dll 0004:00000158 __imp__GetProcAddress@8 00424158 kernel32:KERNEL32.dll 0004:0000015c __imp__LoadLibraryA@4 0042415c kernel32:KERNEL32.dll 0004:00000160 __imp__InterlockedIncrement@4 00424160 kernel32:KERNEL32.dll 0004:00000164 __imp__GetModuleFileNameA@12 00424164 kernel32:KERNEL32.dll 0004:00000168 __imp__TerminateProcess@8 00424168 kernel32:KERNEL32.dll 0004:0000016c __imp__GetCurrentProcess@0 0042416c kernel32:KERNEL32.dll 0004:00000170 __imp__UnhandledExceptionFilter@4 00424170 kernel32:KERNEL32.dll 0004:00000174 __imp__FreeEnvironmentStringsA@4 00424174 kernel32:KERNEL32.dll 0004:00000178 __imp__FreeEnvironmentStringsW@4 00424178 kernel32:KERNEL32.dll 0004:0000017c __imp__WideCharToMultiByte@32 0042417c kernel32:KERNEL32.dll 0004:00000180 __imp__GetEnvironmentStrings@0 00424180 kernel32:KERNEL32.dll 0004:00000184 __imp__GetEnvironmentStringsW@0 00424184 kernel32:KERNEL32.dll 0004:00000188 __imp__SetHandleCount@4 00424188 kernel32:KERNEL32.dll 0004:0000018c __imp__GetFileType@4 0042418c kernel32:KERNEL32.dll 0004:00000190 __imp__GetStartupInfoA@4 00424190 kernel32:KERNEL32.dll 0004:00000194 __imp__HeapDestroy@4 00424194 kernel32:KERNEL32.dll 0004:00000198 __imp__HeapCreate@12 00424198 kernel32:KERNEL32.dll 0004:0000019c __imp__HeapFree@12 0042419c kernel32:KERNEL32.dll 0004:000001a0 __imp__VirtualFree@12 004241a0 kernel32:KERNEL32.dll 0004:000001a4 __imp__RtlUnwind@16 004241a4 kernel32:KERNEL32.dll 0004:000001a8 __imp__GetLastError@0 004241a8 kernel32:KERNEL32.dll 0004:000001ac __imp__SetConsoleCtrlHandler@8 004241ac kernel32:KERNEL32.dll 0004:000001b0 __imp__IsBadWritePtr@8 004241b0 kernel32:KERNEL32.dll 0004:000001b4 __imp__IsBadReadPtr@8 004241b4 kernel32:KERNEL32.dll 0004:000001b8 __imp__HeapValidate@12 004241b8 kernel32:KERNEL32.dll 0004:000001bc __imp__GetCPInfo@8 004241bc kernel32:KERNEL32.dll 0004:000001c0 __imp__GetACP@0 004241c0 kernel32:KERNEL32.dll 0004:000001c4 __imp__GetOEMCP@0 004241c4 kernel32:KERNEL32.dll 0004:000001c8 __imp__HeapAlloc@12 004241c8 kernel32:KERNEL32.dll 0004:000001cc __imp__VirtualAlloc@16 004241cc kernel32:KERNEL32.dll 0004:000001d0 __imp__HeapReAlloc@16 004241d0 kernel32:KERNEL32.dll 0004:000001d4 __imp__MultiByteToWideChar@24 004241d4 kernel32:KERNEL32.dll 0004:000001d8 __imp__LCMapStringA@24 004241d8 kernel32:KERNEL32.dll 0004:000001dc __imp__LCMapStringW@24 004241dc kernel32:KERNEL32.dll 0004:000001e0 __imp__GetStringTypeA@20 004241e0 kernel32:KERNEL32.dll 0004:000001e4 __imp__GetStringTypeW@16 004241e4 kernel32:KERNEL32.dll 0004:000001e8 __imp__SetFilePointer@16 004241e8 kernel32:KERNEL32.dll 0004:000001ec __imp__SetStdHandle@8 004241ec kernel32:KERNEL32.dll 0004:000001f0 __imp__FlushFileBuffers@4 004241f0 kernel32:KERNEL32.dll 0004:000001f4 __imp__CloseHandle@4 004241f4 kernel32:KERNEL32.dll 0004:000001f8 /177KERNEL32_NULL_THUNK_DATA 004241f8 kernel32:KERNEL32.dll entry point at 0001:000000f0 Line numbers for ./Debug/CrashDemo.obj(d:/msdev/myprojects/crashdemo/crashdemo.cpp) segment .text 13 0001:00000020 14 0001:00000038 15 0001:0000003f 16 0001:00000046 17 0001:00000050 20 0001:00000070 21 0001:00000088 22 0001:0000008d 如果仔 细浏览 Rva+Base 这栏 ,你会 发现 第一个比崩 溃 地址 0x0040104a 大的函数地址是 0x00401070 ,所以在 0x00401070 这 个地址之前的那个入口就是 产 生崩 溃 的函数,也就是 这 行: 0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj 因此, 发 生崩 溃 的函数就是 ?Crash@@YAXXZ ,所有以 问 号 开头 的函数名称都是 C++ 修 饰 的名称。 在我 们 的源程序中,也就是 Crash() 这 个子函数。 OK , 现 在我 们轻 而易 举 地便知道了 发 生崩 溃 的函数名称,你是不是很 兴奋 呢?呵呵,先 别 忙,接下来,更 厉 害的招数要出 场 了。 请 注意 MAP 文件的最后部分 —— 代 码 行信息( Line numbers information ),它是以 这样 的形式 显 示的: 13 0001:00000020 第一个数字代表在源代 码 中的代 码 行号,第二个数是 该 代 码 行在所属的代 码 段中的偏移量。 如果要 查 找代 码 行号,需要使用下面的公式做一些十六 进 制的减法运算: 崩 溃 行偏移 = 崩 溃 地址( Crash Address ) - 基地址( ImageBase Address ) - 0x1000 为 什 么 要 这样 做呢? 细 心的朋友可能会留意到 Rva+Base 这栏 了,我 们 得到的崩 溃 地址都是由 偏移地址( Rva ) + 基地址( Base ) 得来的,所以在 计 算行号的 时 候要把基地址减去,一般情况下,基地址的 值 是 0x00400000 。另外,由于一般的 PE 文件的代 码 段都是从 0x1000 偏移 开 始的,所以也必 须 减去 0x1000 。 好了,明白了 这 点,我 们 就可以来 进 行小学减法 计 算了: 崩 溃 行偏移 = 0x0040104a - 0x00400000 - 0x1000 = 0x4a 如果 浏览 MAP 文件的代 码 行信息,会看到不超 过计 算 结 果,但却最接近的数是 CrashDemo.cpp 文件中的: 16 0001:00000046 也就是在源代 码 中的第 16 行, 让 我 们 来看看源代 码 : 16 i /= j; 哈!!!果然就是第 16 行啊! 兴奋吗 ?我也一 样 ! :) 方法已 经 介 绍 完了,从今以后,我 们 就可以精确地定位到源代 码 中的崩 溃 行,而且只要 编译 器可以生成 MAP 文件(包括 VC 、 MASM 、 VB 、 BCB 、 Delphi…… ),本方法都是适用的。我 们时 常抱怨 M$ 的 产 品如何如何差,但其 实 M$ 还 是有意无意 间 提供了很多有价 值 的信息 给 我 们 的,只是我 们 往往不懂得怎 么 利用而已 …… 相信 这样 一来,你就可以更 为 从容地面 对 “ 非法操作 ” 提示了。你甚至可以要求用 户 提供崩 溃 的地址,然后就可以坐在家中舒舒服服地找到出 错 的那行,并 进 行修正。 是不是很爽呢? :) 方法已经介绍完了,从今以后,我们就可以精确地定位到源代码中的崩溃行,而且只要编译器可以生成 MAP 文件,无论在 WIN 平台还是 UNIX 平台,本方法都是适用的。 本文我们只是列举了一个非常简单的 “ 除 0 异常 ” 例子,使用 MAP 文件的效力或许还不十分明显。但相信在我们的大型应用系统调试中,使用 MAP 文件的辅助方法来快速定位发生程序崩溃的函数以及代码行,将会为我们的程序调试工作节省大量时间和精力,提高我们的调试质量。我们甚至可以要求远地用户直接提供程序崩溃的地址,然后就可以在自己机器上利用 MAP 文件静态地找到出错的那行,并在程序中进行相应修正了。