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