zoukankan      html  css  js  c++  java
  • 如何通过崩溃地址找到出错的代码行(转)

     http://www.cnblogs.com/Thinknet/archive/2008/04/07/1141149.html

    这是从“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@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 .DebugCrashDemo.obj(d:msdevmyprojectscrashdemocrashdemo.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文件,即机器码这一文件,具体定位在那一行崩 溃。

       首先配置vc2005生成map文件和cod文件:(1).map文件:property->Configuration Properties->Linker->Debugging 中的Generate Map File选择Yes(/MAP);

    (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         Publics by Value              Rva+Base       Lib:Object

    0000:00000000       ___safe_se_handler_count   00000000     <absolute>
    0000:00000000       ___safe_se_handler_table   00000000     <absolute>
    0000:00000000       ___ImageBase               00400000     <linker-defined>
    0001:00000000       __enc$textbss$begin        00401000     <linker-defined>
    0001:00010000       __enc$textbss$end          00411000     <linker-defined>
    0002:00000390       ?errorFun@@YAXPAH@Z        00411390 f   testError.obj
    0002:000003d0       _wmain                     004113d0 f   testError.obj
    0002:00000430       __RTC_InitBase             00411430 f   MSVCRTD:init.obj
    0002:00000470       __RTC_Shutdown             00411470 f   MSVCRTD:init.obj
    0002:00000490       __RTC_CheckEsp             00411490 f   MSVCRTD:stack.obj
    0002:000004c0       @_RTC_CheckStackVars@8     004114c0 f   MSVCRTD:stack.obj
    0002:00000540       @_RTC_AllocaHelper@12      00411540 f   MSVCRTD:stack.obj

    ....

    程序崩溃地址0x004113b1,我们找到第一个比这个地址大的004113d0,前一个是00411390,地址是函数的开始地址,所以发生的崩溃的的函数是errorFun,这个函数的初始地址00411390.

    (3)找出具体崩溃行号.

    由(2)可知,发生错误函数是errorFun,在testError.obj,打开testError.cod文件,找到errorFun函数生成的机器码.

    ?errorFun@@YAXPAH@Z PROC    ; errorFun, COMDAT

    ; 7    : {

    00000 55   push ebp
    00001 8b ec   mov ebp, esp
    00003 81 ec c0 00 00
    00   sub esp, 192 ; 000000c0H
    00009 53   push ebx
    0000a 56   push esi
    0000b 57   push edi
    0000c 8d bd 40 ff ff
    ff   lea edi, DWORD PTR [ebp-192]
    00012 b9 30 00 00 00 mov ecx, 48   ; 00000030H
    00017 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH
    0001c f3 ab   rep stosd

    ; 8    : *p=1;

    0001e 8b 45 08 mov eax, DWORD PTR _p$[ebp]
    00021 c7 00 01 00 00
    00   mov DWORD PTR [eax], 1

    ; 9    : }

    00027 5f   pop edi
    00028 5e   pop esi
    00029 5b   pop ebx
    0002a 8b e5   mov esp, ebp
    0002c 5d   pop ebp
    0002d c3   ret 0
    (说明: 7,8,9是表示在源代码的行号。

    00000 55   push ebp,000000是相对偏移地地,55是机器码号,push ebp,000000是汇编码。)

    通过(2)我们计算相对偏移地址,即崩溃地址-函数起始地址,0x004113b1-0x00411390=0x21(16进制的计数)

    找到0x21这一行对应的机器码是 00021 c7 00 01 00 00,向上看它是由第8行*p=1;生成的汇编码,由此可见是这一行程序发生崩溃。

    结束语:当然这只是一个简单的例子,实际上一运行便知道是这一行出错,但是对于一个比较大的工程,特别是在多线程并发情况下,要找出那一行出错比较困难,便可以使用map和cod文件找到程序崩溃原因。


    本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/yjg8873042/archive/2009/11/19/4834803.aspx

  • 相关阅读:
    TextBox 只有下划线
    can't find web control library(web控件库)
    DropDownListSalesAC”有一个无效 SelectedValue,因为它不在项目列表中。
    IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型
    如何打印1px表格
    CSS控制打印 分页
    Virtual Server could not open its emulated Ethernet switch driver. To fix this problem, reenable the Virtual Server Emulated Et
    Xml中SelectSingleNode方法中的xpath用法
    热带水果莫入冰箱?水果存放冰箱大法
    探索Asp.net的Postback机制
  • 原文地址:https://www.cnblogs.com/zhangzhifeng/p/6039836.html
Copyright © 2011-2022 走看看