来源:http://blog.csdn.net/mydeardingxiaoli/article/details/20371585程序员
这是从“VC编程经验总结7”中转出来的借花献佛——如何经过崩溃地址找到出错的代码行做为程序员,咱们平时最担忧见到的事情是什么?是内存泄漏?是界面很差看?……错啦!我相信个人见解是不会有人反对的--那就是,程序发生了崩溃! “该程序执行了非法操做,即将关闭。请与你的软件供应商联系。”,呵呵,这句 M$ 的“名言”,恐怕就是程序员最担忧见到的东西了。有的时候,本身的程序在本身的机器上运行得好好的,可是到了别人的机器上就崩溃了;有时本身在编写和测试的过程当中就莫名其妙地遇到了非法操做,可是却没法肯定究竟是源代码中的哪行引发的……是否是很痛苦呢?没关系,本文能够帮助你走出这种困境,甚至你今后以后能够自豪地要求用户把崩溃地址告诉你,而后你就能够精确地定位到源代码中出错的那行了。(很神奇吧?呵呵。首先我必须强调的是,本方法能够在目前市面上任意一款编译器上面使用。可是我只熟悉 M$ 的 VC 和 MASM ,所以后面的部分只介绍如何在这两个编译器中实现,请读者自行融会贯通,掌握在别的编译器上使用的方法。Well,废话说完了,让咱们开始!:)编程
首先必须生成程序的 MAP 文件。什么是 MAP 文件?简单地讲, MAP 文件是程序的全局符号、源文件和代码行号信息的惟一的文本表示方法,它能够在任何地方、任什么时候候使用,不须要有额外的程序进行支持。并且,这是惟一能找出程序崩溃的地方的救星。windows
好吧,既然 MAP 文件如此神奇,那么咱们应该如何生成它呢?在 VC 中,咱们能够按下 Alt+F7 ,打开“Project Settings”选项页,选择 C/C++ 选项卡,并在最下面的 Project Options 里面输入:/Zd ,而后要选择 Link 选项卡,在最下面的 Project Options 里面输入: /mapinfo:lines 和 /map:PROJECT_NAME.map 。最后按下 F7 来编译生成 EXE 可执行文件和 MAP 文件。api
在 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 文件了。.net
在此我先解释一下加入的参数的含义:命令行
/Zd 表示在编译的时候生成行信息
/map[:filename] 表示生成 MAP 文件的路径和文件名
/mapinfo:lines 表示生成 MAP 文件时,加入行信息
/mapinfo:exports 表示生成 MAP 文件时,加入 exported functions (若是生成的是 DLL 文件,这个选项就要加上)orm
OK,经过上面的步骤,咱们已经获得了 MAP 文件,那么咱们该如何利用它呢?让咱们从简单的实例入手,请打开你的 VC ,新建这样一个文件: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 。blog
再看看它的 MAP 文件:(因为文件内容太长,中间没用的部分我进行了省略)CrashDemoTimestamp 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
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.dllentry point at 0001:000000f0
Line numbers for ./Debug/CrashDemo.obj(d:/msdev/myprojects/crashdemo/crashdemo.cpp) segment .text13 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$ 仍是有意无心间提供了不少有价值的信息给咱们的,只是咱们每每不懂得怎么利用而已……相信这样一来,你就能够更为从容地面对“非法操做”提示了。你甚至能够要求用户提供崩溃的地址,而后就能够坐在家中舒舒服服地找到出错的那行,并进行修正。