map 文件的使用

                 map 文件的使用

             Horin|贺勤
        Email: [email protected]
        Blog: http://blog.csdn.net/horin153/


----- 前言 -----

    在程序发布后,最怕的事情是什么?不是效率低,不是界面不好,而是 crash。当用户把一张程序 crash 后的 Windows 截图发给你时,此时最大的愿望肯定是希望通过这张截图,获取更多的关于 crash 的信息。下面就探讨如何通过 crash 地址找出源代码的相关信息。

    探讨之前进行一些简单的界定:
适用:Windows 下 Visual C++ 编程。
条件:Project 设置正确,生成 map 文件,修正 Dll 加载地址的冲突。
功能:主要是查找内存访问违规导致的 crash,不能查找一个无效的指令地址,如访问 delete 后的指针。


----- 生成 map 文件 -----

    好的开始,是成功的一半!请按部就班进行以下操作:

1, 下载最新的 dbghelp.dll 符号引擎

    把该 Dll 放到要使用该 Dll 的程序相同的目录下。

2, 把 pdb 文件和 build 的二进制文件放在相同目录下。

    该步骤对生成 map 文件是不需要的,主要是方便用微软的调试工具 + dump 文件来查找 crash 信息。具体请查阅参考资料 [1]。

3, 用调试符来连编所有的构件

    调试符是调试器显示程序的源代码信息、变量名称以及数据类型信息的数据。

    在 VC++ 2003 中设置 (*** 代表 Project 名字):
    Project -> *** Properties -> *** Property Pages =>

a) -> c/c++ -> General -> Debug Information Format -> Program Database (/Zi)

    这将开启发布构件的调试符。把调试符加入二进制代码中时,二进制代码的大小会有少量增加。
    实测表明 .exe 文件大小无变化,但生成的 .pdb 和 .map 文件会增大不少。这会把程序代码相关的信息写入 .map 文件,否则 .map 文件中只写入 lib 文件的代码信息。

b) -> Linker -> Debugging -> Generate Debug Info (/DEBUG); Generate Map File (/MAP); Map Exports; Map Lines

   /MAP:生成 map 文件
   /MAPINFO:EXPORTS:将导出信息包含到 map 文件中 (增大 .pdb 文件)
   /MAPINFO:LINES:将代码行信息包含到 map 文件中

   -> Linker -> Command Line -> Additional Options 输入:/OPT:REF

   /OPT:REF 开关使链接器只调入程序直接调用的函数;否则,发布的应用程序会包括从未调用过的函数,使应用程序要大得多。

4, 修改部分代码的编译条件

    把有问题文件以 3 级警告编译,其他的用 4 级警告编译:
#pragma warning (push, 3)
#include "IDoNotCompileAtWarning4.h"
#pragma warning (pop)

    在特定代码区域关闭个别警告:
// Turning off 4201
#pragma warning (disable : 4201)
//... codes ...
// Turn warning back on.
#pragma warning (default:4201)

5, Dll 的加载地址

    根据向导创建项目的默认值,VC++ Dlls 加载在 0x10000000,可以改变 Dll 的基本地址(即 rebase)。

当在调试器的 Output 输出:
LDR: Dll xxx base 10000000 relocated due to collision with yyy
这说明:xxx 和 yyy 两个 Dll 有冲突。

此时需要重定义基址:
c) -> Linker -> Advanced -> Base Address; Fixed Base Address

    推荐按照字母顺序重定义 Dll 的基址:
Dll 首字母: Base Addr
A_C: 0X60000000
G_I: 0X62000000
J_L: 0X63000000
M_O: 0X64000000
P_R: 0X65000000
S_U: 0X66000000
V_X: 0X67000000
Y_Z: 0X68000000

    操作系统 Dll 在 0x70000000 - 0x78000000 这段地址上进行加载。


----- map文件的内容 -----

    通常,map 文件是一个几 MB 或更大的文本文件,包括以下几部分:
注:本文附带的是一个节选的 demo,为简单说明用。

a) 首部信息,包括:模块名称、显示 link.exe 链接模块的时间标记、首选的加载地址(可简记为 PLA )。
 demo
 Timestamp is 4326e927 (Tue Sep 13 22:58:47 2005)
 Preferred load address is 00400000

b) 扇区信息,说明链接器从各个 obj 文件和 lib 文件引入哪些扇区。
 Start         Length     Name                   Class
 0001:00000000 001840feH .text                   CODE
 0002:00000000 00000ba4H .idata$5                DATA

c) 公用函数信息包括函数名称,Rva+Base 表示函数的启动地址,与 Address 存在如下换算关系:
  Rva+Base = (Preferred load address) + Address + 0x1000

  Address         Publics by Value              Rva+Base     Lib:Object
 0000:00000000       ___ImageBase               00400000    
 0001:00000000       ?Allocate@CCRTAllocator@ATL@@SAPAXI@Z 00401000 f i AboutDlg.obj

    静态声明的函数信息:
 entry point at        0001:00135a59
 Static symbols
 0001:0000a933       $L111251                   0040b933 f i DownloadDlg.obj

d) 代码行信息
  Line numbers for ./Release/frozen.obj(f:/btdown/frozen.c) segment .text
   304 0001:000647e0   303 0001:000647e0   305 0001:000647e6   306 0001:000647f0
    其中 304 0001:000647e0,304 表示代码行号;0001:000647e0 表示从代码行所在的代码段开始部分到该代码行所在位置的偏移量。

e) 模块的导出函数
 Exports
 ordinal name
 1 ?MapDllFunction@@YAHXZ (int __cdecl MapDllFunction(void))


----- map 文件的使用 -----

    当有了合格的 map 文件,就可以进行使用了。

a) 寻找包含崩溃地址的文件

    if 首选的加载地址 <= 崩溃地址 <= 公用函数扇区的最后一个地址
    then 查看的是正确的 map 文件

b) 浏览公用函数的 Rva+Base 栏,直到找到第一个比崩溃地址大的函数的地址。map 文件中先前的那个入口就是产生 crash 的函数。
    所有以问号 '?' 打头的函数名称都是 C++ 修饰的名称,如要转换该名称,需要将它作为一个命令行参数传送到 Platform SDK 程序 UNDNAME.EXE 中。

c) 计算偏移量

    偏移量 = (crash address) - (preferred load address) - 0x1000
    因为 crash 地址是从代码扇区开始的偏移量,但代码扇区并不是二进制代码的开头部分;二进制代码的开头部分是 PE (Portable Executable) 标题,该标题的长度为 0x1000 字节;所以计算地址要减去 0x1000。

d) 查找代码行号

    查找 map 文件的代码行信息,直到找到不大于但却最接近这个偏移量的数为止。


----- 使用实例 -----

a) 报错信息:"0x0044a093" 指令引用的 "0x4e49b391" 内存。该内存不能为 "Read"。

b) 查找 map 文件:
 0001:00048b20       ?OnEraseBkgnd@CDemoView@@QAEHPAVCDC@@@Z 00449b20 f   DemoView.obj
 0001:00049300       ?OnLButtonDown@CDemoView@@QAEXIVCPoint@@@Z 0044a300 f   DemoView.obj
    因为 00449b20 < 0044a093 < 0044a300,所以 crash 的函数是:?OnEraseBkgnd@CDemoView@@QAEHPAVCDC@@@Z

c) 偏移量 = 崩溃地址 - 首选的加载地址 - 0x1000
          = 0x0044a093 - 0x00400000 - 0x1000 = 0x49093
    或者如下计算:
    偏移量 = 崩溃地址 - 崩溃函数起始地址 + 函数相对偏移
           = 0x0044a093 - 0x00449b20 + 0x00048b20 = 0x49093

d) 查找代码行:

 -- 方法1: 使用 map 文件进行查找
Line numbers for ./Release/DemoView.obj(f:/btdown/DemoView.cpp) segment .text
 536 0001:0004907d 537 0001:000490f2
    因为 4907d < 49093 < 490f2,所以 crash 代码行是:536 行。


 -- 方法2: 使用 cod 文件

    -- 生成源文件的 ASM 文件

    方法1: Project Property Pages => c/c++ => Output Files => Assembler Output 中直接选择。
    方法2: 在 Project Property Pages => c/c++ => Command Line => Additional Options 中键入:
    /FAs,编译器就会为每个源文件生成一个 ASM 文件。
    /FAcs,生成源文件代码和ASM代码对应的 cod 文件。
    以上选项可以为整个 Project 设置,也可以为某个源文件设置。

    -- 计算语句偏移量
函数内偏移量 = 崩溃地址 - 崩溃函数的起始地址
             = 0x0044a093 - 0x00449b20 = 0x573
崩溃语句在 cod 文件中的相对偏移 = 崩溃函数在 cod 文件中相对偏移 + 函数内偏移量
           = 0x0000 + 0x573

    -- 查找代码行
    因为崩溃语句在 cod 文件中的相对偏移为 0x573,由 cod 文件知为 536 行代码。

    -- 相关 cod 文件片段
?OnEraseBkgnd@CDemoView@@QAEHPAVCDC@@@Z PROC NEAR    ; CDemoView::OnEraseBkgnd, COMDAT
; _this$ = ecx
; 412  : {
  00000    6a ff         push     -1
; 语句汇编后的偏移地址 00000,二进制码 6a ff,汇编代码 push -1
; 其余省略......

; crash 语句
; 536  : CString str1 = player->GetNickname();
  0055d    8b 54 24 10     mov     edx, DWORD PTR _player$155238[esp+168]
  00561    8d 0c 3b     lea     ecx, DWORD PTR [ebx+edi]
  00564    89 7c 24 54     mov     DWORD PTR _rtName$[esp+168], edi
  00568    89 44 24 58     mov     DWORD PTR _rtName$[esp+172], eax
  0056c    89 4c 24 5c     mov     DWORD PTR _rtName$[esp+176], ecx
  00570    8b 72 10     mov     esi, DWORD PTR [edx+16]
  00573    8b 4e f0     mov     ecx, DWORD PTR [esi-16]
; 其余省略......


----- map 文件无能为力的地方 -----

1 指针指向一非法地址,然后对指针的内容进行了读或写的操作。
{
    int *pp;
    *pp = 222;
}
release 版本下出错:
" "0x6bc8b527" 指令引用的 "0x00000516" 内存。该内存不能为 "read"。"

2 函数或子程序中局部变量数组越界赋值,造成函数或子程序返回地址被覆盖,从而造成函数或子程序返回时崩溃。


----- 参考资料 -----

1 John Robbins. 应用程序调试技术. 清华大学出版社.
2 对“仅通过崩溃地址找出源代码的出错行”一文的补充与改进. http://www.vckbase.com/document/viewdoc/?id=1473
 

你可能感兴趣的:(C,&,C++)