關於程序崩潰時轉儲內存DMP,可以設置注冊表,使程序崩潰時自動轉儲內存DMP,見程序崩潰時利用注冊表自動轉儲內存DMP。本文要介紹的是使用SetUnhandledExceptionFilter函數在程序崩潰時取得程序內存DMP,並解決一些困擾人的問題。
從名字上就可以看出SetUnhandledExceptionFilter的作用就是設置未捕獲異常函數,程序崩潰就是因為有些異常我們沒有捕獲,而當這些異常我們沒捕獲時,系統就會調用SetUnhandledExceptionFilter設置的函數,在此函數中可以進行一些操作,比如彈出對話框、打印語句等。關於SetUnhandledExceptionFilter更詳細的信息,參見MSDN,這里不作詳細介紹。
見代碼:
- LONG WINAPI ExpFilter(struct _EXCEPTION_POINTERS *pExp)
- {
- cout << "Unhandled Exception!!!" << endl;
- return EXCEPTION_EXECUTE_HANDLER;
- }
- void StartUnhandledExceptionFilter()
- {
- ::SetUnhandledExceptionFilter(ExpFilter);
- }
- int main()
- {
- cout << "begin !" << endl;
- StartUnhandledExceptionFilter();
- int i = 0;
- i = i / i;
- cout << "end !" << endl;
- getch();
- return 0;
- }
LONG WINAPI ExpFilter(struct _EXCEPTION_POINTERS *pExp) { cout << "Unhandled Exception!!!" << endl; return EXCEPTION_EXECUTE_HANDLER; } void StartUnhandledExceptionFilter() { ::SetUnhandledExceptionFilter(ExpFilter); } int main() { cout << "begin !" << endl; StartUnhandledExceptionFilter(); int i = 0; i = i / i; cout << "end !" << endl; getch(); return 0; }
運行結果:
main函數的第6行“i = i / i;”語句,產生一個除數為0的異常,這個異常我們沒有捕獲(使用try、catch或__try、__except等),因此系統調用::SetUnhandledExceptionFilter設置的函數ExpFilter,此函數輸出一個語句,然后返回EXCEPTION_EXECUTE_HANDLER,表明異常處理完畢,程序可以退出。
有了上面的經驗,於是我們可以在ExpFilter函數中進行一些操作,保存程序的DMP,然后結合PDB,我們就可以分析程序崩潰的原因了。
- LONG WINAPI ExpFilter(struct _EXCEPTION_POINTERS *pExp)
- {
- char szExec[256];
- sprintf(szExec, "ntsd -c \".dump /f c:\\123.dmp;q\" -p %d",
- ::GetCurrentProcessId());
- WinExec(szExec, SW_SHOWNORMAL);
- Sleep(1000);
- return EXCEPTION_EXECUTE_HANDLER;
- }
LONG WINAPI ExpFilter(struct _EXCEPTION_POINTERS *pExp) { char szExec[256]; sprintf(szExec, "ntsd -c \".dump /f c:\\123.dmp;q\" -p %d", ::GetCurrentProcessId()); WinExec(szExec, SW_SHOWNORMAL); Sleep(1000); return EXCEPTION_EXECUTE_HANDLER; }
執行ntsd語句得到程序的DMP,保存在C盤根目錄123.dmp,注意WinExec執行了ntsd語句后,要Sleep一段時間,因為WinExec是異步的,執行ntsd時可能主程序已經退出了,導致ntsd找不到指定的程序,無法生成DMP。
以上是用ntsd得到程序的DMP,還可以利用Dbghelp.dll提供的MiniDumpWriteDump函數取得程序的DMP,代碼如下:
- LONG WINAPI ExpFilter(struct _EXCEPTION_POINTERS *pExp)
- {
- HANDLE hFile = ::CreateFile(
- "c:\\123.dmp",
- GENERIC_WRITE,
- 0,
- NULL,
- CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if(INVALID_HANDLE_VALUE != hFile)
- {
- MINIDUMP_EXCEPTION_INFORMATION einfo;
- einfo.ThreadId = ::GetCurrentThreadId();
- einfo.ExceptionPointers = pExp;
- einfo.ClientPointers = FALSE;
- ::MiniDumpWriteDump(
- ::GetCurrentProcess(),
- ::GetCurrentProcessId(),
- hFile,
- MiniDumpWithFullMemory,
- &einfo,
- NULL,
- NULL);
- ::CloseHandle(hFile);
- }
- return EXCEPTION_EXECUTE_HANDLER;
- }
LONG WINAPI ExpFilter(struct _EXCEPTION_POINTERS *pExp) { HANDLE hFile = ::CreateFile( "c:\\123.dmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(INVALID_HANDLE_VALUE != hFile) { MINIDUMP_EXCEPTION_INFORMATION einfo; einfo.ThreadId = ::GetCurrentThreadId(); einfo.ExceptionPointers = pExp; einfo.ClientPointers = FALSE; ::MiniDumpWriteDump( ::GetCurrentProcess(), ::GetCurrentProcessId(), hFile, MiniDumpWithFullMemory, &einfo, NULL, NULL); ::CloseHandle(hFile); } return EXCEPTION_EXECUTE_HANDLER; }
使用MiniDumpWriteDump需要加頭文件Dbghelp.h和鏈接文件Dbghelp.lib。MiniDumpWriteDump中第四個參數可以設置取得DMP的類型,例子中是取得所有內存DMP。
現在來試下用windbg打開DMP,看看程序的堆棧,看是否能找到導致程序崩潰的地方,以下例子使用執行ntsd語句版本的ExpFilter:
OK,成功打開DMP!仔細看看程序的堆棧,好像不對。main函數第6行產生異常,但堆棧中沒有,卻直接跳到了ExpFilter函數中執行ntsd的的地方。這是因為main函數並非程序最開始執行的函數,鏈接器在鏈接可執行文件時,選擇了正確的C/C++運行庫運行函數,在此運行庫函數中才調用的main函數,查看堆棧,可以知道,此運行庫函數為mainCRTStartup,此函數的相關代碼(VC++6.0下為crtexe.c)如下:
- __try {
- ...
- }
- __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )
- {
- _exit( GetExceptionCode() );
- }
__try { ... } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { _exit( GetExceptionCode() ); }
省略號的地方就是調用main的地方,可以看到,調用main的地方被__try、__except包起來了,產生異常時,首先會調用_XcptFilter,之后的第二步才到我們設置的函數,堆棧自然也就變了。
查找__try、__except的資料可知,__except后面跟一個表達式,表達式的值與SetUnhandledExceptionFilter設置的未捕獲異常函數返回值的意義一樣,如果設置為EXCEPTION_CONTINUE_SEARCH,表示異常沒有被識別到,異常繼續往上層拋,至到SetUnhandledExceptionFilter設置的未捕獲異常函數。有了這些資料,我們就可以解決不能正確顯示堆棧的問題了:
- int MyXcptFilter()
- {
- return EXCEPTION_CONTINUE_SEARCH;
- }
- void StartUnhandledExceptionFilter()
- {
- ::SetUnhandledExceptionFilter(ExpFilter);
- void *_XcptFilter = (void*)GetProcAddress(
- LoadLibrary("msvcrt.dll"), "_XcptFilter");
- DWORD dwOldProtect;
- VirtualProtect(_XcptFilter, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
- *(char*)_XcptFilter = 0xe9;
- *(unsigned int*)((char*)_XcptFilter + 1) =
- (unsigned int)MyXcptFilter - ((unsigned int)_XcptFilter + 5);
- VirtualProtect(_XcptFilter, 5, dwOldProtect, &dwOldProtect);
- }
int MyXcptFilter() { return EXCEPTION_CONTINUE_SEARCH; } void StartUnhandledExceptionFilter() { ::SetUnhandledExceptionFilter(ExpFilter); void *_XcptFilter = (void*)GetProcAddress( LoadLibrary("msvcrt.dll"), "_XcptFilter"); DWORD dwOldProtect; VirtualProtect(_XcptFilter, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); *(char*)_XcptFilter = 0xe9; *(unsigned int*)((char*)_XcptFilter + 1) = (unsigned int)MyXcptFilter - ((unsigned int)_XcptFilter + 5); VirtualProtect(_XcptFilter, 5, dwOldProtect, &dwOldProtect); }
修改msvcrt.dll中函數_XcpFilter,使調用_XcpFilter時直接跳轉到我們自己的函數MyXcptFilter去執行,此函數返回EXCEPTION_CONTINUE_SEARCH,因此__except后面表達式就為EXCEPTION_CONTINUE_SEARCH,表示此異常未能被識別,進而調用ExpFilter,堆棧也被保存下來了,結果如下:
這下就OK了,我們就可以知道哪里產生異常了,查看Locals,可以看到i的值為0。
注意:如果此時顯示的堆棧還不正確,可能是因為沒有加載kernel32.dll等文件的pdb,需要從微軟官網下載,將”srv*downstreamstore*http://msdl.microsoft.com/download/symbols“加入到Symbol File Path中,windbg即可自動從微軟官網下載相應版本的pdb文件。
我們還可以這樣處理:
- void StartUnhandledExceptionFilter()
- {
- ::SetUnhandledExceptionFilter(ExpFilter);
- void *_XcptFilter = (void*)GetProcAddress(
- LoadLibrary("msvcrt.dll"), "_XcptFilter");
- DWORD dwOldProtect;
- VirtualProtect(_XcptFilter, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
- VirtualProtect(_XcptFilter, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
- *(char*)_XcptFilter = 0x33;
- *((char*)_XcptFilter + 1) = 0xc0;
- *((char*)_XcptFilter + 2) = 0xc2;
- *((char*)_XcptFilter + 3) = 0x00;
- *((char*)_XcptFilter + 4) = 0x00;
- VirtualProtect(_XcptFilter, 5, dwOldProtect, &dwOldProtect);
- }
void StartUnhandledExceptionFilter() { ::SetUnhandledExceptionFilter(ExpFilter); void *_XcptFilter = (void*)GetProcAddress( LoadLibrary("msvcrt.dll"), "_XcptFilter"); DWORD dwOldProtect; VirtualProtect(_XcptFilter, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); VirtualProtect(_XcptFilter, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); *(char*)_XcptFilter = 0x33; *((char*)_XcptFilter + 1) = 0xc0; *((char*)_XcptFilter + 2) = 0xc2; *((char*)_XcptFilter + 3) = 0x00; *((char*)_XcptFilter + 4) = 0x00; VirtualProtect(_XcptFilter, 5, dwOldProtect, &dwOldProtect); }
同樣修改_XcptFilter,不過不是讓其跳轉,而是讓其直接返回EXCEPTION_CONTINUE_SEARCH的值0,也可以達到目的。
如果不處理_XcptFilter,非主線程產生異常后的堆棧也不一樣,比如用_beginthread創建線程的堆棧就不對,而用CreateThread創建線程的堆棧卻是對的,至於原因,有興趣的可以試試,在非主線程中產生異常,然后分析DMP,一看就明白了。
以上測試例子在VC++6.0環境中編譯,其他編譯器略有不同,具體環境具體分析。