这是从“VC编程经验总结7”中转出来的
借花献佛——如何通过崩溃地址找到出错的代码行
作为程序员,我们平时最担心见到的事情是什么?是内存泄漏?是界面不好看?……错啦!我相信我的看法是不会有人反对的--那就是,程序发生了崩溃!
“该 程序执行了非法操作,即将关闭。请与你的软件供应商联系。”,呵呵,这句
M$Content$nbsp;的“名言”,恐怕就是程序员最担心见到的东西了。有的时候,自己的程序在自己的机器上运行得好好的,但是到了别人的机器上
就崩溃了;有时自己在编写和测试的过程中就莫名其妙地遇到了非法操作,但是却无法确定到底是源代码中的哪行引起的……是不是很痛苦呢?不要紧,本文可以帮
助你走出这种困境,甚至你从此之后可以自豪地要求用户把崩溃地址告诉你,然后你就可以精确地定位到源代码中出错的那行了。(很神奇吧?呵呵。)
首先我必须强调的是,本方法可以在目前市面上任意一款编译器上面使用。但是我只熟悉 M$Content$nbsp;的 VC 和 MASM
,因此后面的部分只介绍如何在这两个编译器中实现,请读者自行融会贯通,掌握在别的编译器上使用的方法。
Well,废话说完了,让我们开始! :)
首先必须生成程序的 MAP 文件。什么是 MAP 文件?简单地讲, MAP
文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,它可以在任何地方、任何时候使用,不需要有额外的程序进行支持。而且,这是唯一能找出程序崩溃的地方的救星。
好 吧,既然 MAP 文件如此神奇,那么我们应该如何生成它呢?在 VC 中,我们可以按下 Alt+F7 ,打开“Project
Settings”选项页,选择 C/C++ 选项卡,并在最下面的 Project Options 里面输入:/Zd ,然后要选择
Link 选项卡,在最下面的 Project Options 里面输入: /mapinfo:lines 和
/map:PROJECT_NAME.map 。最后按下 F7 来编译生成 EXE 可执行文件和 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
file://****************************************************************
02 file://程序名称:演示如何通过崩溃地址找出源代码的出错行
03 file://作者:罗聪
04 file://日期:2003-2-7
05
file://出处:http://www.luocong.com(老罗的缤纷天地)
06 file://本程序会产生“除0错误”,以至于会弹出“非法操作”对话框。
07 file://“除0错误”只会在 Debug
版本下产生,本程序为了演示而尽量简化。
08 file://注意事项:如欲转载,请保持本程序的完整,并注明:
09
file://转载自“老罗的缤纷天地”(http://www.luocong.com)
10
file://****************************************************************
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
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$Content$nbsp;的产品如何如何差,但其实
M$Content$nbsp;还是有意无意间提供了很多有价值的信息给我们的,只是我们往往不懂得怎么利用而已……相信这样一来,你就可以更为从容地面
对“非法操作”提示了。你甚至可以要求用户提供崩溃的地址,然后就可以坐在家中舒舒服服地找到出错的那行,并进行修正。
一般程序崩溃可以通过debug,找到程序在那一行代码崩溃了,最近编一个多线程的程序,都不知道在那发生错误,多线程并发,又不好单行调试,终于 找到一个比较好的方法来找原因,通过生成map文件,由于2005取消map文件生成行号信息(vc6.0下是可以生成行号信息的,不知道 microsoft怎么想的,在2005上取消了),只能定位在那个函数发生崩溃。这里可以通过生成cod文件,即机器码这一文件,具体定位在那一行崩 溃。
(2).cod文件:property->Configuration Properties->C/C++->output Files中Assembler OutPut中选择Assembly,Maching Code and Source(/FAcs),生成机器,源代码。
简单例子:
(1) #include "stdafx.h"
void errorFun(int * p)
{
*p=1;
}
int _tmain(int argc, _TCHAR* argv[])
{
int * p=NULL;
errorFun(p);
return 0;
}
在errorFun中函数中,*p=1这一行出错,由于p没有申请空间,运行时出错,弹出
Unhandled exception at 0x004113b1 in testError.exe: 0xC0000005: Access violation writing location 0x00000000.
在0x004113b1程序发生崩溃。
(2)debug文件下打开map文件,定位崩溃函数.
map文件开头是一些链接信息,然后我们要找函数和实始地址信息。地址是函始的开始地址
Address
0000:00000000
0000:00000000
0000:00000000
0001:00000000
0001:00010000
0002:00000390
0002:000003d0
0002:00000430
0002:00000470
0002:00000490
0002:000004c0
0002:00000540
....
程序崩溃地址0x004113b1,我们找到第一个比这个地址大的004113d0,前一个是00411390,地址是函数的开始地址,所以发生的崩溃的的函数是errorFun,这个函数的初始地址00411390.
(3)找出具体崩溃行号.
由(2)可知,发生错误函数是errorFun,在testError.obj,打开testError.cod文件,找到errorFun函数生成的机器码.
?errorFun@@YAXPAH@Z
; 7
00000 55
00001 8b ec
00003 81 ec c0 00 00
00
00009 53
0000a 56
0000b 57
0000c 8d bd 40 ff ff
ff
00012 b9 30 00 00 00 mov ecx,
48
00017 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH
0001c f3 ab
; 8
0001e 8b 45 08 mov eax, DWORD PTR _p$[ebp]
00021 c7 00 01 00 00
00
; 9
00027 5f
00028 5e
00029 5b
0002a 8b e5
0002c 5d
0002d c3
(说明: 7,8,9是表示在源代码的行号。
00000 55
通过(2)我们计算相对偏移地址,即崩溃地址-函数起始地址,0x004113b1-0x00411390=0x21(16进制的计数)
找到0x21这一行对应的机器码是 00021 c7 00 01 00 00,向上看它是由第8行*p=1;生成的汇编码,由此可见是这一行程序发生崩溃。
结束语:当然这只是一个简单的例子,实际上一运行便知道是这一行出错,但是对于一个比较大的工程,特别是在多线程并发情况下,要找出那一行出错比较困难,便可以使用map和cod文件找到程序崩溃原因。