作为程序员,我们平时最担心见到的事情是什么?是内存泄漏?是界面不好看?……错啦!我相信我的看法是不会有人反对的——那就是,程序发生了崩溃!
“该程序执行了非法操作,即将关闭。请与你的软件供应商联系。”,呵呵,这句 M$ 的“名言”,恐怕就是程序员最担心见到的东西了。有的时候,自己的程序在自己的机器上运行得好好的,但是到了别人的机器上就崩溃了;有时自己在编写和测试的过程中就莫名其妙地遇到了非法操作,但是却无法确定到底是源代码中的哪行引起的……是不是很痛苦呢?不要紧,本文可以帮助你走出这种困境,甚至你从此之后可以自豪地要求用户把崩溃地址告诉你,然后你就可以精确地定位到源代码中出错的那行了。(很神奇吧?呵呵。)
首先我必须强调的是,本方法可以在目前市面上任意一款编译器上面使用。但是我只熟悉 M$ 的 VC 和 MASM ,因此后面的部分只介绍如何在这两个编译器中实现,请读者自行融会贯通,掌握在别的编译器上使用的方法。
Well,废话说完了,让我们开始! :)
首先必须生成程序的 MAP 文件。什么是 MAP 文件?简单地讲, MAP 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,它可以在任何地方、任何时候使用,不需要有额外的程序进行支持。而且,这是唯一能找出程序崩溃的地方的救星。
好吧,既然 MAP 文件如此神奇,那么我们应该如何生成它呢?在 VC 中,我们可以按下 Alt+F7 ,打开“Project Settings”选项页,选择 CC++ 选项卡,并在最下面的 Project Options 里面输入:Zd ,然后要选择 Link 选项卡,在最下面的 Project Options 里面输入: mapinfolines 和 mapPROJECT_NAME.map 。最后按下 F7 来编译生成 EXE 可执行文件和 MAP 文件。
在 MASM 中,我们要设置编译和连接参数,我通常是这样做的:
rc %1.rc
ml c coff Zd %1.asm
link subsystemwindows mapinfoexports mapinfolines map%1.map %1.obj %1.res
把它保存成 makem.bat ,就可以在命令行输入 makem filename 来编译生成 EXE 可执行文件和 MAP 文件了。
在此我先解释一下加入的参数的含义:
Zd 表示在编译的时候生成行信息
map[filename] 表示生成 MAP 文件的路径和文件名
mapinfolines 表示生成 MAP 文件时,加入行信息
mapinfoexports 表示生成 MAP 文件时,加入 exported functions (如果生成的是 DLL 文件,这个选项就要加上)
OK,通过上面的步骤,我们已经得到了 MAP 文件,那么我们该如何利用它呢?
让我们从简单的实例入手,请打开你的 VC ,新建这样一个文件:
01
02 程序名称:演示如何通过崩溃地址找出源代码的出错行
03 作者:罗聪
04 日期:2003-2-7
05 出处:httpwww.luocong.com(老罗的缤纷天地)
06 本程序会产生“除0错误”,以至于会弹出“非法操作”对话框。
07 “除0错误”只会在 Debug 版本下产生,本程序为了演示而尽量简化。
08 注意事项:如欲转载,请保持本程序的完整,并注明:
09 转载自“老罗的缤纷天地”(httpwww.luocong.com)
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 092302 2003)
Preferred load address is 00400000
Start Length Name Class
000100000000 0000de04H .text CODE
00010000de04 0001000cH .textbss CODE
000200000000 00001346H .rdata DATA
000200001346 00000000H .edata DATA
000300000000 00000104H .CRT$XCA DATA
000300000104 00000104H .CRT$XCZ DATA
000300000208 00000104H .CRT$XIA DATA
00030000030c 00000109H .CRT$XIC DATA
000300000418 00000104H .CRT$XIZ DATA
00030000051c 00000104H .CRT$XPA DATA
000300000620 00000104H .CRT$XPX DATA
000300000724 00000104H .CRT$XPZ DATA
000300000828 00000104H .CRT$XTA DATA
00030000092c 00000104H .CRT$XTZ DATA
000300000a30 00000b93H .data DATA
0003000015c4 00001974H .bss DATA
000400000000 00000014H .idata$2 DATA
000400000014 00000014H .idata$3 DATA
000400000028 00000110H .idata$4 DATA
000400000138 00000110H .idata$5 DATA
000400000248 000004afH .idata$6 DATA
Address Publics by Value Rva+Base LibObject
000100000020 Crash@@YAXXZ 00401020 f CrashDemo.obj
000100000070 _main 00401070 f CrashDemo.obj
000400000000 __IMPORT_DESCRIPTOR_KERNEL32 00424000 kernel32KERNEL32.dll
000400000014 __NULL_IMPORT_DESCRIPTOR 00424014 kernel32KERNEL32.dll
000400000138 __imp__GetCommandLineA@0 00424138 kernel32KERNEL32.dll
00040000013c __imp__GetVersion@0 0042413c kernel32KERNEL32.dll
000400000140 __imp__ExitProcess@4 00424140 kernel32KERNEL32.dll
000400000144 __imp__DebugBreak@0 00424144 kernel32KERNEL32.dll
000400000148 __imp__GetStdHandle@4 00424148 kernel32KERNEL32.dll
00040000014c __imp__WriteFile@20 0042414c kernel32KERNEL32.dll
000400000150 __imp__InterlockedDecrement@4 00424150 kernel32KERNEL32.dll
000400000154 __imp__OutputDebugStringA@4 00424154 kernel32KERNEL32.dll
000400000158 __imp__GetProcAddress@8 00424158 kernel32KERNEL32.dll
00040000015c __imp__LoadLibraryA@4 0042415c kernel32KERNEL32.dll
000400000160 __imp__InterlockedIncrement@4 00424160 kernel32KERNEL32.dll
000400000164 __imp__GetModuleFileNameA@12 00424164 kernel32KERNEL32.dll
000400000168 __imp__TerminateProcess@8 00424168 kernel32KERNEL32.dll
00040000016c __imp__GetCurrentProcess@0 0042416c kernel32KERNEL32.dll
000400000170 __imp__UnhandledExceptionFilter@4 00424170 kernel32KERNEL32.dll
000400000174 __imp__FreeEnvironmentStringsA@4 00424174 kernel32KERNEL32.dll
000400000178 __imp__FreeEnvironmentStringsW@4 00424178 kernel32KERNEL32.dll
00040000017c __imp__WideCharToMultiByte@32 0042417c kernel32KERNEL32.dll
000400000180 __imp__GetEnvironmentStrings@0 00424180 kernel32KERNEL32.dll
000400000184 __imp__GetEnvironmentStringsW@0 00424184 kernel32KERNEL32.dll
000400000188 __imp__SetHandleCount@4 00424188 kernel32KERNEL32.dll
00040000018c __imp__GetFileType@4 0042418c kernel32KERNEL32.dll
000400000190 __imp__GetStartupInfoA@4 00424190 kernel32KERNEL32.dll
000400000194 __imp__HeapDestroy@4 00424194 kernel32KERNEL32.dll
000400000198 __imp__HeapCreate@12 00424198 kernel32KERNEL32.dll
00040000019c __imp__HeapFree@12 0042419c kernel32KERNEL32.dll
0004000001a0 __imp__VirtualFree@12 004241a0 kernel32KERNEL32.dll
0004000001a4 __imp__RtlUnwind@16 004241a4 kernel32KERNEL32.dll
0004000001a8 __imp__GetLastError@0 004241a8 kernel32KERNEL32.dll
0004000001ac __imp__SetConsoleCtrlHandler@8 004241ac kernel32KERNEL32.dll
0004000001b0 __imp__IsBadWritePtr@8 004241b0 kernel32KERNEL32.dll
0004000001b4 __imp__IsBadReadPtr@8 004241b4 kernel32KERNEL32.dll
0004000001b8 __imp__HeapValidate@12 004241b8 kernel32KERNEL32.dll
0004000001bc __imp__GetCPInfo@8 004241bc kernel32KERNEL32.dll
0004000001c0 __imp__GetACP@0 004241c0 kernel32KERNEL32.dll
0004000001c4 __imp__GetOEMCP@0 004241c4 kernel32KERNEL32.dll
0004000001c8 __imp__HeapAlloc@12 004241c8 kernel32KERNEL32.dll
0004000001cc __imp__VirtualAlloc@16 004241cc kernel32KERNEL32.dll
0004000001d0 __imp__HeapReAlloc@16 004241d0 kernel32KERNEL32.dll
0004000001d4 __imp__MultiByteToWideChar@24 004241d4 kernel32KERNEL32.dll
0004000001d8 __imp__LCMapStringA@24 004241d8 kernel32KERNEL32.dll
0004000001dc __imp__LCMapStringW@24 004241dc kernel32KERNEL32.dll
0004000001e0 __imp__GetStringTypeA@20 004241e0 kernel32KERNEL32.dll
0004000001e4 __imp__GetStringTypeW@16 004241e4 kernel32KERNEL32.dll
0004000001e8 __imp__SetFilePointer@16 004241e8 kernel32KERNEL32.dll
0004000001ec __imp__SetStdHandle@8 004241ec kernel32KERNEL32.dll
0004000001f0 __imp__FlushFileBuffers@4 004241f0 kernel32KERNEL32.dll
0004000001f4 __imp__CloseHandle@4 004241f4 kernel32KERNEL32.dll
0004000001f8 177KERNEL32_NULL_THUNK_DATA 004241f8 kernel32KERNEL32.dll
entry point at 0001000000f0
Line numbers for .DebugCrashDemo.obj(dmsdevmyprojectscrashdemocrashdemo.cpp) segment .text
13 000100000020 14 000100000038 15 00010000003f 16 000100000046
17 000100000050 20 000100000070 21 000100000088 22 00010000008d
如果仔细浏览 Rva+Base 这栏,你会发现第一个比崩溃地址 0x0040104a 大的函数地址是 0x00401070 ,所以在 0x00401070 这个地址之前的那个入口就是产生崩溃的函数,也就是这行:
000100000020 Crash@@YAXXZ 00401020 f CrashDemo.obj
因此,发生崩溃的函数就是 Crash@@YAXXZ ,所有以问号开头的函数名称都是 C++ 修饰的名称。在我们的源程序中,也就是 Crash() 这个子函数。
OK,现在我们轻而易举地便知道了发生崩溃的函数名称,你是不是很兴奋呢?呵呵,先别忙,接下来,更厉害的招数要出场了。
请注意 MAP 文件的最后部分——代码行信息(Line numbers information),它是以这样的形式显示的:
13 000100000020
第一个数字代表在源代码中的代码行号,第二个数是该代码行在所属的代码段中的偏移量。
如果要查找代码行号,需要使用下面的公式做一些十六进制的减法运算:
崩溃行偏移 = 崩溃地址(Crash Address) - 基地址(ImageBase Address) - 0x1000
为什么要这样做呢?细心的朋友可能会留意到 Rva+Base 这栏了,我们得到的崩溃地址都是由 偏移地址(Rva)+ 基地址(Base) 得来的,所以在计算行号的时候要把基地址减去,一般情况下,基地址的值是 0x00400000 。另外,由于一般的 PE 文件的代码段都是从 0x1000 偏移开始的,所以也必须减去 0x1000 。
好了,明白了这点,我们就可以来进行小学减法计算了:
崩溃行偏移 = 0x0040104a - 0x00400000 - 0x1000 = 0x4a
如果浏览 MAP 文件的代码行信息,会看到不超过计算结果,但却最接近的数是 CrashDemo.cpp 文件中的:
16 000100000046
也就是在源代码中的第 16 行,让我们来看看源代码
16 i = j;
哈!!!果然就是第 16 行啊!
兴奋吗?我也一样! :)
方法已经介绍完了,从今以后,我们就可以精确地定位到源代码中的崩溃行,而且只要编译器可以生成 MAP 文件(包括 VC、MASM、VB、BCB、Delphi……),本方法都是适用的。我们时常抱怨 M$ 的产品如何如何差,但其实 M$ 还是有意无意间提供了很多有价值的信息给我们的,只是我们往往不懂得怎么利用而已……相信这样一来,你就可以更为从容地面对“非法操作”提示了。你甚至可以要求用户提供崩溃的地址,然后就可以坐在家中舒舒服服地找到出错的那行,并进行修正。