当我们开发的Windows程序崩溃时,我们常会将Catch到的Dump文件进行分析。
以往当我们拿到一个Dump时,通常是这样做的:
1.确定Dump发生时的版本号;
2.找到备份的对应版本的PDB文件;
3.必要时再找对应版本的源文件;
然而,当发布出去的版本较多时,上述过程是很麻烦的;
建议一个符号服务器可以为我们省去上述麻烦。
----以下介绍带源文件索引的符号服务器的创建过程--------:
以下是创建符号服务器的过程,使用符号服务器不需要这些工作;
以下描述仅适用于源代码服务器为SubVersion的情形,其他源代码服务器的情况,请参考相应资料。
1. 准备工作
安装Windows Debugging Tools
下载地址:http://www.microsoft.com/whdc/devtools/downloads.mspx
安装Perl
下载地址:http://www.activestate.com/activeperl
安装SubVersion
下载地址:http://www.sliksvn.com/en/download
安装SubVersion要注意几点:
Ø 它可以与TortoiseSVN共存;
Ø 一定不能安装带本地化语言的版本,必须是英文版;如果安装上述推荐地址的SilkSVN,注意不要选择本地语言选项。为了确定是否安装了英文版,可以在命令行中敲svn help,看输出的是否全部为英文提示;这点尤为重要,而且容易被忽视;
将上述软件的安装路径加入到Path环境变量中;注意对于Debugging Tools for Windows,不仅需要把Debugging Tools for Windows目录加入到Path环境变量,还需要将其下面的srcsrv目录也加进去,因为后续执行的svnindex命令等实际上是在srcsrv目录中。
2. 为符号文件添加源文件索引
当编译完成后,将符号文件添加到符号服务器之前,需要对符号文件进行处理,在符号文件上加入我们的源文件服务器地址信息;如果没有这一步,那么源文件只能人工自己去找了。
执行如下命令行:
Svnindex /debug /symbols="E:\MySource\Symbols" /source=" E:\MySource\"
上述的"E:\MySource\Symbols"为符号文件所在路径;" E:\MySource\"为源文件所在路径
注意上述两个路径一定必须是全路径,不能是相对路径;这点与若干文献描述不一致,
但是我的实践证明,如果是相对路径,这个命令是不能执行成功的。
上述命令的执行结果如下图:
上述命令实际上是往符号文件中加入了svn命令,用于下载相应的源文件。
要看究竟发生了什么,可以采用srctool命令查看某PDB文件中的信息,如:
Srctool e:\MySource\Symbols\OneFile.pdb
执行结果类似下图:
看到了吧,文件中含svn.exe cat命令,从源文件服务器中将相应revision的文件下载下来。
3. 将符号文件添加到符号服务器
接下来就是把符号文件加入到符号服务器了。很简单,就是一条命令:
symstore.exe add /r /f "E:\AliWWAutoPackage\Trunk\SourceCode\setup\release*.pdb" /s "E:\SymbolServer" /t TaoyouSetup /v "1.0.0.1" /compress /d "E:\SymbolServer\SymStore_TaoyouSetup.log" /o
上述命令中,/f后的路径是符号文件路径;/s后的路径是符号服务器所在路径;其他命令参数请参考Symstore.exe的命令说明吧。
然后把符号服务器路径设置为文件共享,或者通过HTTP服务器暴露出去,那就是名副其实的"符号服务器"了。
以下介绍如何使用符号服务器:
先介绍windbg中的用法。
1. 设置符号服务器地址;
如我的设置是这样的:*srv*D:\SymbolCache*http://msdl.microsoft.com/download/symbols;\\YourServer\SymbolServer\
后面那部分就是我们的符号服务器地址;
2. 设置源文件服务器地址:
同样,可以通过Windbg的菜单File->Source File Path,设置如下:
*srv*D:\SourceCache*http://svn.yourserver.com/
上述的D:\SourceCache是本地缓存路径,注意自己先把该文件夹建好了;
3. 在windbg中打开某dump文件。
如敲入!analyze -v命令,此时就能够显示相应的源文件信息了,如:
牛B了吧,如果没有源文件索引,上述信息是不会显示出来的。
不过第一次使用时,windbg会弹出如下安装提示:
选择最后一项后,今后就不会出现了。
Visual Studio中的用法更为简单:
在菜单的工具->选项->调试->常规选项中,将如下两个选项勾上:
启用源服务器支持
将源服务器诊断信息打印到输出窗口
如下图:
如此设置后,就可以在VS中打开相应版本的源文件了。
参考文献:
http://www.result42.com/howto/symbol-storage-subversion-indexed-symbols
http://www.codeproject.com/KB/debug/sourceindexing.aspx?display=Printhttp://msdn.microsoft.com/en-us/magazine/cc163563.aspx
http://msdn.microsoft.com/en-us/magazine/cc163563.aspx
第一个文献看起来有理有据,描述了基本过程;不过在细节方面貌似还是有些问题的;
第三个文献很详细...
关于建设symbol store的建议
2006-12-21
一、symbol store的需求分析:
1、我们现在的调试环境严重依赖开发人员自己使用的开发环境,缺点在于其他人要进行调试要么搭建一个同样的环境,严重地占去大家不必要花费的工作时间,进一步带来的问题是各个版本的源代码流落各处,易出问题,要么就只有交给原开发人员,同样带了严重的交流时间开销,而且不仅原开发人员的工作会被打断,而且需求人还要在这个过程中等待。
2、即便是开发人员自己面对不同版本的调试,也是一个令人挠头的事情,即便是现在我们保存了map文件,但是调试的便利与效率,相信也是大家希望进一步提高的。
如何改变现有的调试难的问题,并且提高大家的调试效率,应该是一个需要考虑的问题。
二、搭建symbol store的调研:
使用过MS的调试符号服务器的同事,大概已经领略到调试符号服务器的高效率。经过调研,可以确认我们可以搭建自己的调试符号服务器,相关事项罗列如下:
1、如何设置你的VC Project
在VC的Project Settings对话框上,选择你要设置的Project(如果有多个Poject的话),选择你要设置的build选项(一般为Win32 Release),在Link页上,Category选择General,然后选择Generate debug info,或者Category选择Debug,然后选择Debug info,选择Microsoft format。
这样如果生成不了pdb,或者生成的pdb有错误,同样在VC的Project Settings对话框上,选择你要设置的Project(如果有多个Poject的话),选择你要设置的build选项(一般为Win32 Release),在C/C++页上,Category选择General,在Debug info中,选择C7 Compatible或Program Database
你原有的优化等选择都不必改变,这样设置会产生一个pdb文件,就是我们所需要的调试符号了,这样会导致你的exe或dll等生成模块会多出一定数目的字节,不必担心,这是链接器在你的模块里加入了对这个pdb的一个指引,并没有任何调试信息包含到你的模块。
2、如何变绝对路径为相对路径
前面讲到在你的模块里有一个对pdb的指引,有一点比较讨厌,就是这个指引使用了绝对路径——你build这个模块时的绝对路径,我们要做得像MS的一样,去除这个绝对路径,MS提供一个工具来做这件事,那就是Binplace,它甚至可以让你为使用你的接口的客户提供受限的符号文件,具体可参考MS的帮助文档。
binplace -a -x -s %stripped_path% -n %full_path% %bin_path% -r %RootDestinationPath% -:DEST %ClassPath%
使用时请替换%stripped_path %、%full_path %、%bin_path%、%RootDestinationPath%、%ClassPath%
3、如何添加调试符号到仓库
请首先安装windbg,然后使用如下脚本:
set path=C:\Program Files\Debugging Tools for Windows;%path%
symstore add /r /p /f %YOUR_PDB_PATH%\*.* /s %SYMBOL_STORE_PATH% /t "ivtdriver"
使用时请替换%YOUR_PDB_PATH %,与% SYMBOL_STORE_PATH %
4、如何引用仓库中的调试符号
设置环境变量:
_NT_SYMBOL_PATH=srv*DownstreamStore*SYMBOL_STORE_PATH*http://msdl.microsoft.com/download/symbols
使用时,请注意替换DownstreamStore,与SYMBOL_STORE_PATH
这样,在以后使用windbg调试程序或分析dmp文件时,就可以自动取得我们自己的调试符号了。
三、建立symbol store的步骤:
1、 确定symbol store的路径,申请服务器空间,用WEB是个好主意,用共享文件夹也不错
2、 修改现有VC Project的设置,生成pdb(请不要丢了map设置,没准儿还有用)
3、 应用binplace工具,让pdb文件中的绝对路径变相对路径
4、 应用symstore工具,将pdb文件添加到symbol store
四、应用symbol store的步骤:
1、设置环境变量_NT_SYMBOL_PATH,引用我们自己的symbol store
五、总结:
结合我前面提出的daily build script,大家现在可以在里面提供自动向symbol store添加pdb的脚本,这样一定会很爽,呵呵。
Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=1615448
好不容易复现的宕机bug,结果不产生dump文件,让俺情何以堪啊!坑爹嘛!
大部分情况还好,就是弹出这种“Runtime Error”框的时候,不产生dump文件,解决它!!!
VS2005中SetUnhandledExceptionFilter函数应用
很多软件通过设置自己的异常捕获函数,捕获未处理的异常,生成报告或者日志(例如生成mini-dump文件),达到Release版本下追踪Bug的目的。但是,到了VS2005(即VC8),Microsoft对CRT(C运行时库)的一些与安全相关的代码做了些改动,典型的,例如增加了对缓冲溢出的检查。新CRT版本在出现错误时强制把异常抛给默认的调试器(如果没有配置的话,默认是Dr.Watson),而不再通知应用程序设置的异常捕获函数,这种行为主要在以下三种情况出现。
(1) 调用abort函数,并且设置了_CALL_REPORTFAULT选项(这个选项在Release版本是默认设置的)。
(2) 启用了运行时安全检查选项,并且在软件运行时检查出安全性错误,例如出现缓存溢出。(安全检查选项 /GS 默认也是打开的)
(3) 遇到_invalid_parameter错误,而应用程序又没有主动调用
_set_invalid_parameter_handler设置错误捕获函数。
所以结论是,使用VS2005(VC8)编译的程序,许多错误都不能在SetUnhandledExceptionFilter捕获到。这是CRT相对于前面版本的一个比较大的改变,但是很遗憾,Microsoft却没有在相应的文档明确指出。
解决方法
之所以应用程序捕获不到那些异常,原因是因为新版本的CRT实现在异常处理中强制删除所有应用程序先前设置的捕获函数,如下所示:
/* Make sure any filter already in place is deleted. */
SetUnhandledExceptionFilter(NULL);
UnhandledExceptionFilter(&ExceptionPointers);
解决方法是拦截CRT调用SetUnhandledExceptionFilter函数,使之无效。在X86平台下,可以使用以下代码。
#ifndef _M_IX86
#error "The following code only works for x86!"
#endif
void DisableSetUnhandledExceptionFilter()
{
void *addr = (void*)GetProcAddress(LoadLibrary(_T("kernel32.dll")),
"SetUnhandledExceptionFilter");
if (addr)
{
unsigned char code[16];
int size = 0;
code[size++] = 0x33;
code[size++] = 0xC0;
code[size++] = 0xC2;
code[size++] = 0x04;
code[size++] = 0x00;
DWORD dwOldFlag, dwTempFlag;
VirtualProtect(addr, size, PAGE_READWRITE, &dwOldFlag);
WriteProcessMemory(GetCurrentProcess(), addr, code, size, NULL);
VirtualProtect(addr, size, dwOldFlag, &dwTempFlag);
}
}
在设置自己的异常处理函数后,调用DisableSetUnhandledExceptionFilter禁止CRT设置即可。
其它讨论
上面通过设置api hook,解决了在VS2005上的异常捕获问题,这种虽然不是那么“干净”的解决方案,确是目前唯一简单有效的方式。
虽然也可以通过_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT), signal(SIGABRT, ...), 和_set_invalid_parameter_handler(...) 解决(1)(3),但是对于(2),设置api hook是唯一的方式。
http://blog.youkuaiyun.com/WinGeek/article/details/3942995
[Windows编程] 如何捕捉程序异常/crash 并生成 dump 文件
前面介绍如如何用WinDBG 生成crash dump 《WinDBG 技巧:如何生成Dump 文件(.dump 命令) 》,但是用户机器上通常不安装WinDBG, 而且多数用户也不知道怎么使用WinDBG。 所以最好是自己程序里面能够捕捉exception/crash,并且生成crash dump,然后通过网络传回到自己服务器。
捕捉exception 可以用API 函数 SetUnhandledExceptionFilter 。生成crash dump 可以用DbgHelp.dll 里面的MiniDumpWriteDump 函数。
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( __in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
BOOL WINAPI MiniDumpWriteDump( __in HANDLE hProcess, __in DWORD ProcessId, __in HANDLE hFile, __in MINIDUMP_TYPE DumpType, __in PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, __in PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, __in PMINIDUMP_CALLBACK_INFORMATION CallbackParam );
代码示例:
1 #include <dbghelp.h> 2 #include <shellapi.h> 3 #include <shlobj.h> 4 5 6 // 自定义的exectpion filter 7 LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *pExceptionPointers) 8 { 9 10 SetErrorMode( SEM_NOGPFAULTERRORBOX ); 11 12 //收集信息 13 CStringW strBuild; 14 strBuild.Format(L"Build: %s %s", __DATE__, __TIME__); 15 CStringW strError; 16 HMODULE hModule; 17 WCHAR szModuleName[MAX_PATH] = L""; 18 GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)pExceptionPointers->ExceptionRecord->ExceptionAddress, &hModule); 19 GetModuleFileName(hModule, szModuleName, ARRAYSIZE(szModuleName)); 20 strError.AppenedFormat(L"%s %d , %d ,%d.", szModuleName,pExceptionPointers->ExceptionRecord->ExceptionCode, pExceptionPointers->ExceptionRecord->ExceptionFlags, pExceptionPointers->ExceptionRecord->ExceptionAddress); 21 22 //生成 mini crash dump 23 BOOL bMiniDumpSuccessful; 24 WCHAR szPath[MAX_PATH]; 25 WCHAR szFileName[MAX_PATH]; 26 WCHAR* szAppName = L"AppName"; 27 WCHAR* szVersion = L"v1.0"; 28 DWORD dwBufferSize = MAX_PATH; 29 HANDLE hDumpFile; 30 SYSTEMTIME stLocalTime; 31 MINIDUMP_EXCEPTION_INFORMATION ExpParam; 32 GetLocalTime( &stLocalTime ); 33 GetTempPath( dwBufferSize, szPath ); 34 StringCchPrintf( szFileName, MAX_PATH, L"%s%s", szPath, szAppName ); 35 CreateDirectory( szFileName, NULL ); 36 StringCchPrintf( szFileName, MAX_PATH, L"%s%s//%s-%04d%02d%02d-%02d%02d%02d-%ld-%ld.dmp", 37 szPath, szAppName, szVersion, 38 stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay, 39 stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond, 40 GetCurrentProcessId(), GetCurrentThreadId()); 41 hDumpFile = CreateFile(szFileName, GENERIC_READ|GENERIC_WRITE, 42 FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); 43 44 MINIDUMP_USER_STREAM UserStream[2]; 45 MINIDUMP_USER_STREAM_INFORMATION UserInfo; 46 UserInfo.UserStreamCount = 1; 47 UserInfo.UserStreamArray = UserStream; 48 UserStream[0].Type = CommentStreamW; 49 UserStream[0].BufferSize = strBuild.GetLength()*sizeof(WCHAR); 50 UserStream[0].Buffer = strBuild.GetBuffer(); 51 UserStream[1].Type = CommentStreamW; 52 UserStream[1].BufferSize = strError.GetLength()*sizeof(WCHAR); 53 UserStream[1].Buffer = strError.GetBuffer(); 54 55 ExpParam.ThreadId = GetCurrentThreadId(); 56 ExpParam.ExceptionPointers = pExceptionPointers; 57 ExpParam.ClientPointers = TRUE; 58 59 MINIDUMP_TYPE MiniDumpWithDataSegs = MiniDumpNormal 60 | MiniDumpWithHandleData 61 | MiniDumpWithUnloadedModules 62 | MiniDumpWithIndirectlyReferencedMemory 63 | MiniDumpScanMemory 64 | MiniDumpWithProcessThreadData 65 | MiniDumpWithThreadInfo; 66 bMiniDumpSuccessful = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), 67 hDumpFile, MiniDumpWithDataSegs, &ExpParam, NULL, NULL); 68 // 上传mini dump 到自己服务器(略) 69 ... 70 71 return EXCEPTION_CONTINUE_SEARCH; //或者 EXCEPTION_EXECUTE_HANDLER 关闭程序 72 } 73 74 int _tmain() 75 { 76 // 设置 execption filter 77 SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); 78 .... 79 return 0; 80 }
http://blog.youkuaiyun.com/mergerly/article/details/6662797
VC++ Runtime Error 异常捕获之不挂的程序
VC++ Runtime Error, 对不少朋友来说, 这是一个十分讨厌的错误提示, 您可能不知道如何着手调试: 产生这个错误的原因是什么? 确实只有知道了产生这个错误的直接原因, 才能去调试这个错误.
刚碰到这个错误的时候, 是发生在视频解码的时候, 由于解码一直在工作状态, 所以我也不知道如何去调试, 当出现这个错误之后, 我们大多数时候就忽略了, 想从其他地方解决, 提高稳定性, 甚至怀疑解码器的稳定性; 后来, 我接触解码库之后, 开始调试这样的错误, 刚开始这样的错误并不容易重现, 往往要几个小时, 当这个错误重现之后, 程序还是在运行的, 只是其中的某一个线程中断了执行, 其中的这个线程弹出了 "VC++ Runtime Error" 这样的对话框, 如果你点击它, 则整个应用程序会直接退出. 为了调试, 我就不能点击这个对话框, 而是使用VC2005附件到进程, 然后再直接中断进程, 这个时候, 会有一个线程中断点就在对话框的消息循环中, 仔细查看堆栈, 发现了一个函数: msvcrt.dll!_abort() , 到这里是时候查看MSDN了:
函数名: abort
功 能: 异常终止一个进程
用 法: void abort(void);
In a single or multithreaded Windows-based application, abort calls the Windows MessageBox function to create a message box to display the message with anOK button. When the user clicks OK, the program aborts immediately.
我们的程序就是基于WINDOS窗口的多线程应用程序, 调用了abort就会弹出对话框, 在release版本中, 就是一个确认对话框, 点击后程序就提示出错并退出.
在正常的程序里, 我们是不会调用abort的, 除非是遇到了严重的, 不能恢复的错误. 那么到底这个abort是怎么被调用的呢, 我们自己写的代码显然是没有这个函数, 再仔细查看堆栈, 发现是在一个C语言版本的开源库中. 我们的程序是需要7*24小时运行的, 出现了解码异常应该要被我们忽略, 而不是应用程序崩溃. 开源的跨平台解码库是C语言写的, 在出现了严重错误时, 就直接abort这也是可以理解的, 不过, 这样的程序在我们的代码中显然要避免. 大哥, 现在都是什么年代了, 很多程序都是需要一直跑的, 我只好改的库的源代码来重新编译程序才能解决这个问题了, 该怎么改了, 如果去分析解码的逻辑, 我们没有专业的人才. 我想就干脆从abort函数这里入手, 直接返回成功值, 但是这样对解码逻辑影响更大, 可能导致更大的错误, 我想到了操作系统的异常机制, 由于我们是在WINDOWS平台上工作, 所以可以利用WINDOWS结构化异常, 我们可取消abort调用, 在这里我们使用代码产生一个结构化异常(SEH), 结构化异常分为硬件异常和软件异常, CPU可以检查到内存非法访问和除零错误等异常, 那么我们就将abort替换成除零语句, 比如 int i = 10/0;
当程序执行到这里的时候, CPU会捕捉这个异常, 并提示用户, 我们可以在调用解码函数的地方, 增加SEH捕捉代码, 来捕捉这个错误, 那么程序就能忽略这个错误并继续执行了. 后来的事实也证明了这个错误的忽略对程序并没有什么明显的影响. 怎么写这个捕捉代码呢, 操作系统支持的SEH捕捉代码块为 __try - __finally 块和 __try - __except 块, 而__try - __finnaly块就可以实现我们的功能. 写到这里, 可能有朋友要说了, 我们平时见的最多的是try-catch语句, 那么我要解释一下了, try-catch 是C++异常的处理方式, 而__try-__finnaly是操作系统SEH异常处理方式. 在C++语言的try-catch并不能捕捉操作系统结构化异常(比如CPU异常, 内存访问冲突, 除零错误等). C++异常只能捕获软件异常, 通常是调用throw而产生的异常, 比如MFC异常中常见的CException.
SEH异常和C++异常有本质的区别, SEH是操作系统提供的异常处理技术, 在任何支持该操作系统的编程语言中, 都可以使用, 而C++异常处理只能在编写C++代码时使用。然而, 应当知道WINDOWS的VC++编译器是使用操作系统结构化异常来实现C++异常的. 也就是说, C++的try块在VC++下编译时, 会变成__try块, C++的catch块会变成SEH的 __except块: catch测试则变成SEH异常过滤器, catch中的代码则变为__except中的代码. 事实上, C++的throw块, 在编译的时候也会变成SEH的RaiseException函数调用, 由c++异常变为SEH异常.
__finnally的好处在于, 有时更详细的异常信息对我们没有更大帮助, 我们只需要捕获到异常并忽略它。上面提到C++异常在VC++里被转换成SEH异常, 那么在VC下使用try-catch是否能捕获硬件异常呢? 比如我们常见的 0x0000000C 不可读或写. VC++编译器已经提供了支持:
try {;} catch(...){;} 这样的语句就能够捕获所有异常:包括CPU异常, 以及C++异常; 不过需要注意的是, 在VC6.0中, 是默认支持的. 但是在VC2005中, 是默认不捕获CPU异常的. 区别在于一个C++编译选项/Eha , 只有这个选项打开才能用上面的try-catch()捕捉SEH异常.
程序偶尔会出现:
Microsoft Visual C++ Runtime Library Runtime Error!
Program: [APPPATH] Abnormal program termination
同时带有红色叉叉的对话框。
打开VC,附加对应的进程,在线程选项卡里查找类似MessageBox的字样,找到的那个就是弹出对话框的线程,右键,转到线程,查看调用堆栈,一般就能找到问题了
根据刚才的函数调用堆栈,可以发现,这个框框是由abort间接产生的,根据查看abort的源码,可以发现,产生结果是有选项_WRITE_ABORT_MSG和_CALL_REPORTFAULT这两个标志决定的,这两个标志一个是产生如上所述的对话框,一个是产生那个常见的错误报告对话框。在msdn里,和abort相关的还有个函数_set_abort_behavior,可通过此函数,改变abort的行为。
还有个常见的错误框与此类似,是pure virtual function 就是纯虚函数的调用,它的过程与abort相类似。
测试例子:
1 //SEH的异常处理过滤器 2 static LONG __stdcall MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExPtrs) 3 { 4 AfxMessageBox("glllll"); 5 return EXCEPTION_EXECUTE_HANDLER; 6 } 7 void SignalHandler(int signal) 8 { 9 AfxMessageBox("Application aborting...\n"); 10 } 11 HANDLE handle; 12 DWORD WINAPI ThreadProc(LPVOID lp) 13 { 14 // TODO: Add your control notification handler code here 15 //int a = 0; 16 //int b = 6 / a; 17 char* pBuffer = (char*) (int)(rand()); 18 char szBuff[10]; 19 //StrCpy(pBuffer, "Hello, Crash!"); 20 //MessageBox(pBuffer); 21 //strcpy(pBuffer, szBuff); 22 //CloseHandle((HANDLE)handle); 23 //abort(); 24 //throw bad_alloc("aaaaa"); 25 //terminate(); 26 RaiseException(0, 0, 0, NULL); 27 AfxMessageBox("good"); 28 return 0; 29 } 30 char ppp; 31 static char pppp; 32 const char p5 = 'a'; 33 const static char p6 = 'b'; 34 void CCrashTestDlg::OnBnClickedButton1() 35 { 36 _set_abort_behavior( 0, _WRITE_ABORT_MSG); 37 _set_abort_behavior(0, _CALL_REPORTFAULT); 38 SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); 39 typedef void (*SignalHandlerPointer)(int); 40 SignalHandlerPointer previousHandler; 41 //previousHandler = signal(SIGABRT, SignalHandler); 42 //MessageBox("ddd", NULL); 43 //abort(); 44 DWORD dwID = 0; 45 handle = CreateThread(NULL, 0, ThreadProc, handle, 0, &dwID); 46 CloseHandle(handle); 47 //WaitForSingleObject(handle, INFINITE); 48 49 }
文/玄魂
.PDB文件,全称为“程序数据库”文件。我们使用它(更确切的说是看到它被应用)大多数场景是调试应用程序。目前我们对.PDB文件的普遍认知是它存储了被编译文件的调试信息,作为符号文件存在。那么,它具体包含哪些内容呢?在调试过程中是怎样发挥作用的呢?我们有没有办法去操作这个文件呢?
1. PDB文件内容
.PDB文件的内部格式,微软并没有公开,现在仍然是一个秘密,但是它提供了相关的API用于调试器来从中获取信息。
一个非托管C++程序的PDB文件包含如下信息:
- l Public, private,和static函数地址
- l 全局变量的名称和地址
- l 参数和局部变量的名称及它们在栈中的偏移量
- l 类型定义,包括class, structure,和 data definitions
- l FPO(Frame Pointer Omission,帧指针省略)数据
- l 源文件名称和行号
说明:
从XP SP2起就不再启用FPO。
对于.NET PDB文件,只包含上面说到的两种信息:
- l 源文件名称和行号
- l 局部变量名称
.NET PDB文件包含如此少的信息,原因在于其他信息我们可以从元数据中获取,所以也就没有必要重复存储了。
2. PDB文件匹配
当你讲一个模块加载到当前进程的地址空间中,调试器根据两个信息去寻找找匹配的PDB文件。第一个信息很明了,根据模块的名称。比如你加载的模块为“Test.DLL”,那么调试器将会寻找Test.PDB文件。但是但是通过名称,是无法判断模块和PDB文件是否是完整匹配的,调试器通过第二个信息来判断——一个GUID值。这个GUID值同时存在于模块文件和PDB文件中,如果GUID值不匹配,那么我们是无法在源代码级别来调试程序的。
这个GUID值是编译器和链接器放到文件中的。目前我们没有办法来改变这个值,但是我们查看在编译生成的文件中的GUID值。这里我们使用的工具是DUMPBIN,使用它我们可以列举所有PE文件的信息。如果您安装了VS2008或者VS2010的话,可以直接从VS命令行启动该工具,如图1所示。
图1 启动DUMPBIN
DUMPBIN工具的命令行选项,您可以参阅相关的文档,这里我们使用它的/headers选项,看看会输出什么样的结果。
在使用dumpbin之前,我们首先创建一个控制台程序,内容如代码清单1所示。
代码清单1 示例程序
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace dumptest
{
class Program
{
static void Main(string[] args)
{
}
}
}
编译清单1的代码,obj目录中的文件如图2所示。
图2 obj目录中的文件
Bin目录下的文件内容如图3所示。
图3 Bin目录下的文件
准备工作做完之后,我们现在从命令行切换到bin目录下,执行“dumpbin /headers dumptest.exe”命令,结果如代码清单2所示。
代码清单2 dump查看headers结果
E:\test\c#\dumptest\dumptest\obj\x86\Debug>dumpbin /headers dumptest.exe
Microsoft (R) COFF/PE Dumper Version 10.00.30319.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file dumptest.exe
PE signature found
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (x86)
3 number of sections
4E92A178 time date stamp Mon Oct 10 15:40:40 2011
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
102 characteristics
Executable
32 bit word machine
OPTIONAL HEADER VALUES
10B magic # (PE32)
8.00 linker version
800 size of code
800 size of initialized data
0 size of uninitialized data
272E entry point (0040272E)
2000 base of code
4000 base of data
400000 image base (00400000 to 00407FFF)
2000 section alignment
200 file alignment
4.00 operating system version
0.00 image version
4.00 subsystem version
0 Win32 version
8000 size of image
200 size of headers
0 checksum
3 subsystem (Windows CUI)
8540 DLL characteristics
Dynamic base
NX compatible
No structured exception handler
Terminal Server Aware
100000 size of stack reserve
1000 size of stack commit
100000 size of heap reserve
1000 size of heap commit
0 loader flags
10 number of directories
0 [0] RVA [size] of Export Directory
26D4 [57] RVA [size] of Import Directory
4000 [588] RVA [size] of Resource Directory
0 [0] RVA [size] of Exception Directory
0 [0] RVA [size] of Certificates Directory
6000 [C] RVA [size] of Base Relocation Directory
2668 [1C] RVA [size] of Debug Directory
0 [0] RVA [size] of Architecture Directory
0 [0] RVA [size] of Global Pointer Directory
0 [0] RVA [size] of Thread Storage Directory
0 [0] RVA [size] of Load Configuration Directory
0 [0] RVA [size] of Bound Import Directory
2000 [8] RVA [size] of Import Address Table Directory
0 [0] RVA [size] of Delay Import Directory
2008 [48] RVA [size] of COM Descriptor Directory
0 [0] RVA [size] of Reserved Directory
SECTION HEADER #1
.text name
734 virtual size
2000 virtual address (00402000 to 00402733)
800 size of raw data
200 file pointer to raw data (00000200 to 000009FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
Debug Directories
Time Type Size RVA Pointer
-------- ------ -------- -------- --------
4E92A178 cv 50 00002684 884 Format: RSDS, {99F34C5E-5BC3-4043-AE11-D85F7990AF00}, 1, E:\test\c#\dumptest\dumptest\obj\x86\Debug\dumptest.pdb
SECTION HEADER #2
.rsrc name
588 virtual size
4000 virtual address (00404000 to 00404587)
600 size of raw data
A00 file pointer to raw data (00000A00 to 00000FFF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
SECTION HEADER #3
.reloc name
C virtual size
6000 virtual address (00406000 to 0040600B)
200 size of raw data
1000 file pointer to raw data (00001000 to 000011FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
Read Only
Summary
2000 .reloc
2000 .rsrc
2000 .text
现在我们将目光集中在代码清单2中斜体加粗的部分,这里我们可以找到调试路径的信息,一个GUID值(99F34C5E-5BC3-4043-AE11-D85F7990AF00)和一个路径(E:\test\c#\dumptest\dumptest\obj\x86\Debug\dumptest.pdb)。现在我们已经清楚了调试器如何判断PDB文件是否匹配,下面我们再来看调试器是如何寻找PDB文件的。
3. PDB文件寻路
如果我们观察VS启动调试加载模块和符号文件的过程,会发现它通常会从可执行文件或者DLL文件的相同目录中加载符号文件。这正是调试器寻找PDB文件的第一选择。
如果在模块文件的相同目录下找不到匹配的PDB文件,会发生什么呢?我们在前文知道编译器在PE文件中硬编码了一个路径(比如:E:\test\c#\dumptest\dumptest\obj\x86\Debug\dumptest.pdb),这个路径就是调试器的第二个选择。对于对外发布的应用,很可能这两个路径下都找不到PDB文件。此时调试器会在本地的符号服务器缓存路径下寻找PDB文件。如果本地的符号服务器缓存路径下仍然找不到,它会调试器本身配置的符号服务器中寻找符号文件。图4是VS2010配置符号服务器和本地符号缓存路径的界面。
图4 VS2010配置符号存储
4. PDB与GAC
上面讲到的调试器寻找PDB文件的方式在大多数情况下都工作的很好,当我们遇到必须要讲编制之后的文件安装的GAC中的时候,情况开始变得有意思起来。当我们在本地编译并调试程序集的时候,即使程序集被安装到GAC中,调试器仍然能在编译目录下找到PDB文件,但是如果我们已经将Private Build的应用部署到其他机器上的时候,此时还想在被部署的机器上调试安装到GAC上的程序集,将会是一件很麻烦的事情。我们有两种方案来解决这个问题。
注:
Private Build与Public Build的区别
private build, 用来表示在开发人员自己机器上生成的build;public build,表示在公用的build机器上生成的build。对于public build,需要symbol server存储所有的PDB,然后当用户报告错误的时候,debugger才可以自动地找到binay相应的PDB文件, visual studio 和 windbg都知道如何访问symbol server。在将PDB和binay存储到symbol server前,还需要对PDB运行进行source indexing, source indexing的作用是将PDB和source关联起来。
第一种方案是我们在GAC的目录中找到被安装的程序集,然后将PDB文件拷贝到该目录下。通常我们安装到GAC中的程序集会存在于类似这样的路径中:C:Windows\assembly\GAC_MSIL\Example\1.0.0.0__682bc775ff82796a,该示例目录中“Example”代表程序集的名称,“1.0.0.0”代表版本号,“682bc775ff82796a”代表程序集的Public Token。当你找到确切的目录,将PDB文件放到该目录下,调试器就可以加载符号文件了。
这里还有一种更好的方案,就是设置一个名为“DEVPATH”的系统环境变量。该环境变量设定一个磁盘目录作为其值,该目录将作为GAC的辅助目录存在,在GAC中查找程序集同样会搜索该目录。但是在这样的目录中程序集并不会执行版本检查,这是需要我们注意的地方。
使用DEVPATH,我们首先要选定一个目录,然后确保应用程序对它持有读、写权限。然后创建DEVPATH系统环境变量。当然这只是准备工作,我们还要告知.NET运行时,应用程序启用DEVPATH作为GAC的扩展目录。所以接下来我们在配置文件(APP.CONFIG,WEB.CONFIG,MACHINE.CONFIG)中添加如下配置:
<configuration>
<runtime>
<developmentMode developerInstallation="true"/>
</runtime>
</configuration>
在你打开了development模式后,如果DEVPATH没有定义或路径不存在的话会导致程序启动时异常"Invalid value for registry"。而且如果在machine.config中开启DEVPATH的使用会影响其他的所有的程序,所以要慎重使用machine.config。
5. PDB与源文件
现在我们再来讨论一个开发人员经常问到的问题:源文件的信息是如何在PDB文件中存储的?对于Public Build,PDB文件存储的是如何利用版本控制工具从代码缓存获取源码代码的命令。对于Private Build,很显然,符号文件存储的是源代码的完整路径。
理想状态下,对于Public Build而言源代码索引和符号被缓存到符号服务器的操作都会自动执行,我们无需考虑源代码和符号文件在哪存储的问题。事实上,很多开发团队并没有公用的符号服务器或者源代码索引服务。对于一个小型项目而言,每个开发人员都有足够的磁盘空间来存储源代码和符号文件,也许不会过多的考虑这个问题。但是也许会有这样的场景:将一个Private Build项目转移到另一台机器上有30M的源码需要放到C盘,但是此时C盘只有20M的剩余空间该怎么办?我们能修改PDB文件中源码的路径吗?
我们前面提到没有办法修改PDB文件,但是这里有一个取巧的方法可以尝试,所谓山不向我走来,我向山走去。这里有一个工具恰好可以派上用场,它就是subst.exe,它是Windows自带的命令行工具,可以从cmd窗口启动。
说明:
subst用于路径替换 ,将路径与驱动器号关联,就是把一个目录当作一个磁盘驱动器来看,不过不能格式化。运用一定技巧,subst命令还可以实现隐藏驱动器、特殊软件的安装、模拟光盘自动运行等功能。
用法格式
一、subst [盘符] [路径] 将指定的路径替代盘符,该路径将作为驱动器使用
二、subst /d 解除替代
三、不加任何参数键入 SUBST,可以显示当前虚拟驱动器的清单。
[例子]
C:\DOS>subst a: c:\temp 将c:\temp虚拟化成a盘
C:\>subst a: /d? 解除替代
想到解决上面问题的方法了吗?我们只需将本机源码的路径虚拟化成一个磁盘,例如M,之后无论你将代码部署到任何机器上,只需将被部署的路径虚拟成M就可以了,就不会出现符号文件和目标代码不匹配的情况了。
关于PDB文件的资料,目前在互联网上还是凤毛麟角,我这里只是抛砖引玉,欢迎大家共同交流。
作者:玄魂
出处:http://www.cnblogs.com/xuanhun/
原文链接:http://www.cnblogs.com/xuanhun/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如有商业用途请联系本人:xuanhun521@126.com
当应用程序被链接以后,代码被逐一地翻译为一个个的地址,优化以后的代码可能初看起来更是面目全非。每当我们使用vs或者windbg等微软的调试工具进行调试的时候,我们可以方便地使用变量名来查看内存、可以使用函数名称来下断点、甚至可以指定某个文件的某一行来下断点。这一切背后是什么在指导调试器工作呢?答案就是符号——pdb或者dbg文件(.NET自己有元数据,符号不需要元数据已有的信息)。
程序运行的时候,计算机只需要逐条执行指令即可。而与源代码对应的关系是完全不需要知道的。这就给调试带来了困难,所以无论什么编译都有自己的一套用于对应代码和可执行程序。各种编译器都有自己保存类似这种对应关系的办法,有的直接嵌入可执行文件,有的则是独立出来的。而微软的编译器则是独立产生了这种文件,它就被成为符号文件。
符号文件的历史有兴趣可以网上查查,dbg文件十分古老,微软在新的产品中也不再使用了。所以今天我们新产生的符号文件一般都是pdb文件。而pdb可以理解成提供给调试器用于对应可执行文件和源代码的东西,这个东西运行的时候是没有任何作用的,但是对于调试器和我们调试则有很大的帮助。
那么pdb文件里面到底存储了什么东西呢?根据微软官方的解释有:
1、 全局变量;
2、 局部变量;
3、 函数名及入口点;
4、 FPO记录;
5、 源代码行号。
如果使用vs2010随便写一个本机C语言,那么链接的时候编译器就会帮我们产生一个pdb文件。里面包含大量的符号,包括上面提及的内容。
每次调试程序或者查dmp的时候,我们都必须使用正确的符号。否则我们看到的栈等信息可能不准确。同时我们也无法建立应用程序和源代码之间的关系,没有符号你所面对只有地址。
符号同时又分为两种:public symbols and private symbols。至于他们的区别以后再具体介绍。
调试器是如何来判别EXE、DLL等是否和一个pdb文件匹配呢?每次我们链接EXE或者DLL或者SYS的时候,链接器都将产生一个唯一的GUID,然后将其写入到PDB和可执行文件。调试器加载的时候将检查两者的GUID,如果一致就表示他们匹配。
很多时候我们对PDB都不够重视。如果足够自信发布的东西一定不会产生bug,而且确实也没有产生bug,或者用户为0。那么PDB对我们确实没有多大作用,但是如果我们需要调试,我们需要查dmp文件,那么请妥善保管好自己的代码和pdb。每次重新编译,即使所有代码均没有变化,他们的GUID也不同(PDB还有age的概念,以后再解释)。
想想每个版本从测试到发布得编译多少次,每次都得辛苦去找PDB那么不是很痛苦啊。所幸我们有符号服务器这种东西。微软有自己HTTP符号服务器,我们自己也可以在20s内迅速搭建(以后会介绍如何搭自己的建符号服务器)。而且较新的vs或者windbg都能智能得对符号服务器进行搜索,避免了自己找符号的麻烦。
为了提供一个基本的感性认识,我们看看符号和DLL之间的关系:
0:012> !lmi ntdll
Loaded Module Info: [ntdll]
Module: ntdll
Base Address: 77040000
Image Name: ntdll.dll
Machine Type: 332 (I386)
Time Stamp: 4a5bdadb Tue Jul 14 09:09:47 2009
Size: 13c000
CheckSum: 14033f
Characteristics: 2102 perf
Debug Data Dirs: Type Size VA Pointer
CODEVIEW 22, d5308, d4708 RSDS - GUID: {F0164DA7-1FAF-4765-B8F3-DB4F2D7650EA}
Age: 2, Pdb: ntdll.pdb
CLSID 4, d5304, d4704 [Data not mapped]
Image Type: FILE - Image read successfully from debugger.
C:\Windows\SYSTEM32\ntdll.dll
Symbol Type: PDB - Symbols loaded successfully from symbol server.
c:\symcache\ntdll.pdb\F0164DA71FAF4765B8F3DB4F2D7650EA2\ntdll.pdb
Load Report: public symbols , not source indexed
c:\symcache\ntdll.pdb\F0164DA71FAF4765B8F3DB4F2D7650EA2\ntdll.pdb
从上面红色的地方我们可以看到ntdll里面的GUID、age等信息。同时我们从微软的符号服务器下载了对应的符号,然后保存到了本地的c:\symcache里。
当我们使用vs进行调试的时候,编译器总是能帮我们找到我们编译的应用程序或者DLL的符号,所以往往我们不会遇到和符号相关的太多麻烦。但是如果我们使用的是其他调试工具,或者查dmp的时候,符号的问题就来了。如果我们给调试器指定了正确符号文件,那么一切都很正常,否则我们将看到令人困惑的东西。
调试信息的历史回顾
连接二进制指令和源代码之间的纽带——符号是如何被编译器生成的呢?要具体了解这个内容我们需要先简单回顾一下微软调试信息格式的历史。
COFF:
最早的调试信息格式是COFF格式,这种格式内嵌到可执行文件中的,它能记录函数、变量、行号、FPO等信息。
CodeView:
随后就是比较熟悉的CodeView了。这种调试信息的格式提供了内嵌和分离两种形式,和PDB唯一的不同就是没有编辑并继续的功能。独立的CodeView调试信息存储在.dbg文件中。
PDB:
而微软最新的调试信息格式就是PDB(Program Data Base)了。这种调试信息和可执行文件是完全分离的。他记录了很多丰富的信息,同时还提供了调试并继续、增量链接的功能。不过这种调试信息的格式并没有官方的公开文档,要操作它一般只有通过debughelp或者DIA。PDB又分为两种格式,一种是vc6使用的PDB2.0,后来的版本则全是PDB7.0。PDB7.0是不能向下兼容的。
编译器产生符号的过程
我们看到调试信息是逐步发展的,最新的调试信息格式为PDB7.0。这是一种和可执行文件分离的格式。对于可执行文件,一般只有几百字节的额外负担。下面我们仅讨论PDB这种调试信息格式。
如果指定生成调试信息,编译器在每次编译完文件以后就会产生一个obj文件,然后同时产生它对应的调试信息。当我们进行连接的时候,编译器就会帮我们把所有obj统一编译为一个可执行文件,然后所有的调试信息统一生成一个PDB文件。
如果我们是生成静态库,那么编译器编译完各个源代码以后会统一产生lib文件,同时也将所有的调试信息生成到一个pdb中。如果我们在编译可执行文件的同时需要使用某一个静态库,那么编译器也需要使用到静态库的调试信息,最终可执行文件和调试信息都被单独地生成。
编译器选项
对于VS系列编译器,我们可以有一个总开关:/debug。如果没有这个链接选项,所有调试信息均不会被生成。/pdb可以指定符号文件的名称;/pdbstripped可以指定是否同时产生一个公共符号(public symbol)。
编译选项则有:/Z7 /Zi /ZI 3种。其中/Z7表示生成CodeView格式的调试信息;/Zi表示生成不支持编辑并继续的PDB调试信息;/ZI表示生成支持编辑并继续的PDB调试信息。
上面提到的选项均有项目属性的GUI设置与之对应:
静态库的符号问题
曾经遇到过一个问题,就是使用了vc6编译的静态库,然后在vs2008中进行链接。结果每次链接的时候都产生警告,提示没有找到静态库的符号,结果就像没有调试信息一样。这个问题研究很久无果。
后来自习研究了一下静态库的编译方式才解决了问题。上面已经提到,静态库的PDB是每个文件的调试信息的集合,而默认情况下静态库生成的PDB文件都是VCX0.PDB,例如vs2008就是VC90.PDB,VS2010就是VC100.PDB。生成静态库以后,最终的可执行程序进行链接时候,就会根据lib中各个obj记录的信息区找VCX0.PDB,而这个文件就是我们需要的。如果我们要链接很多个静态库,可能就需要在编译静态库的时候/FD给静态库的符号重命名了。
这一点在.NET中解决得很好,所有依赖的程序集符号都会被自动保存,并且程序集之间的符号不会合并为一份。
符号的生成非常简单,几个编译选项就搞定,默认情况下DEBUG模式都会产生编辑并继续的符号,而Release模式建议也使用/Zi来产生对应调试信息。