2012-02-24 233 views
5

我正在研究一個我想用於在Windows Vista/7的計算機上記錄當前調用堆棧的類。 (非常類似於「漫步電話」http://www.codeproject.com/Articles/11132/Walking-the-callstack)。C++堆棧跟蹤問題

首先,我使用RtlCaptureContext獲取當前上下文記錄,然後使用StackWalk64獲取各個堆棧幀。現在,我意識到STACKFRAME64.AddrPC中的程序計數器實際上會隨着關閉程序並重新啓動它而更改爲特定的代碼行。出於某種原因,我認爲只要我不更改源代碼並重新編譯它,特定代碼行的PC地址就會保持不變。

我需要PC地址來使用SymFromAddr和SymGetLineFromAddr64來獲取有關被調用函數,代碼文件和行號的信息。不幸的是,只要程序調試數據庫(PDB-File)一直在運行,但我不能將其提供給客戶端。

我的計劃是記錄調用堆棧的PC地址(無論何時需要),然後將其從客戶端發送給我。所以我可以使用我的PDB文件找出哪些函數被調用,但這當然只適用於PC地址是唯一標識符的情況。由於每次啓動程序時都會發生變化,因此我無法使用該方法。

你知道更好的方法來讀取調用堆棧或解決更改程序計數器的問題嗎?

我認爲一個可能的解決方案可能是始終獲取已知位置的PC地址,並將其作爲參考來確定不同PC地址之間的偏移量。這似乎工作,但我不知道這是否是一種有效的方法,並將始終工作。

非常感謝您的幫助!我將在codeproject.com上發佈最終的(封裝)解決方案,如果你願意我會說你幫助了我。

+1

查看我的實施:http://www.dima.to/blog/?p=13 – Alexandru 2014-06-02 13:34:48

+0

根據您在博客中的意見,您的實施需要pdb-s。 – 2016-02-23 16:49:27

回答

4

使用信息表格CONTEXT您可以在PE圖像中找到功能部分和偏移量。例如,您可以使用此信息從鏈接器生成的.map文件獲取函數名稱。

  1. 獲取CONTEXT結構。您對程序櫃檯成員感興趣。由於CONTEXT是平臺相關的,所以你必須自己弄明白。你在初始化的時候就已經做好了,例如對於x64 Windows,STACKFRAME64.AddrPC.Offset = CONTEXT.Rip。現在我們開始棧走,並使用STACKFRAME64.AddrPC.Offset,填寫StaclkWalk64作爲我們的出發點。

  2. 您需要使用分配基地址RVA = STACKFRAME64.AddrPC.Offset - AllocationBase將其轉換爲相對虛擬地址(RVA)。您可以使用VirtualQuery獲得AllocationBase

  3. 一旦你有了這個,你需要找到該RVA下降到哪個部分,並從中減去部分開始地址以獲得SectionOffset:SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase。爲了做到這一點,你需要訪問PE圖像頭結構(IMAGE_DOS_HEADER,IMAGE_NT_HEADER,IMAGE_SECTION_HEADER)來獲取PE中的段數和它們的開始/結束地址。這非常簡單。

就是這樣。現在,您在PE圖像中具有節號和偏移量。函數偏移量是在.map文件中小於SectionOffset的最高偏移量。

如果您願意,我可以稍後發佈代碼。

編輯:代碼打印function address(我們假設64位通用CPU):

#include <iostream> 
#include <windows.h> 
#include <dbghelp.h> 

void GenerateReport(void) 
{ 
    ::CONTEXT lContext; 
    ::ZeroMemory(&lContext, sizeof(::CONTEXT)); 
    ::RtlCaptureContext(&lContext); 

    ::STACKFRAME64 lFrameStack; 
    ::ZeroMemory(&lFrameStack, sizeof(::STACKFRAME64)); 
    lFrameStack.AddrPC.Offset = lContext.Rip; 
    lFrameStack.AddrFrame.Offset = lContext.Rbp; 
    lFrameStack.AddrStack.Offset = lContext.Rsp; 
    lFrameStack.AddrPC.Mode = lFrameStack.AddrFrame.Mode = lFrameStack.AddrStack.Mode = AddrModeFlat; 

    ::DWORD lTypeMachine = IMAGE_FILE_MACHINE_AMD64; 

    for(auto i = ::DWORD(); i < 32; i++) 
    { 
    if(!::StackWalk64(lTypeMachine, ::GetCurrentProcess(), ::GetCurrentThread(), &lFrameStack, lTypeMachine == IMAGE_FILE_MACHINE_I386 ? 0 : &lContext, 
      nullptr, &::SymFunctionTableAccess64, &::SymGetModuleBase64, nullptr)) 
    { 
     break; 
    } 
    if(lFrameStack.AddrPC.Offset != 0) 
    { 
     ::MEMORY_BASIC_INFORMATION lInfoMemory; 
     ::VirtualQuery((::PVOID)lFrameStack.AddrPC.Offset, &lInfoMemory, sizeof(lInfoMemory)); 
     ::DWORD64 lBaseAllocation = reinterpret_cast<::DWORD64>(lInfoMemory.AllocationBase); 

     ::TCHAR lNameModule[ 1024 ]; 
     ::GetModuleFileName(reinterpret_cast<::HMODULE>(lBaseAllocation), lNameModule, 1024); 

     PIMAGE_DOS_HEADER lHeaderDOS = reinterpret_cast<PIMAGE_DOS_HEADER>(lBaseAllocation); 
     PIMAGE_NT_HEADERS lHeaderNT = reinterpret_cast<PIMAGE_NT_HEADERS>(lBaseAllocation + lHeaderDOS->e_lfanew); 
     PIMAGE_SECTION_HEADER lHeaderSection = IMAGE_FIRST_SECTION(lHeaderNT); 
     ::DWORD64 lRVA = lFrameStack.AddrPC.Offset - lBaseAllocation; 
     ::DWORD64 lNumberSection = ::DWORD64(); 
     ::DWORD64 lOffsetSection = ::DWORD64(); 

     for(auto lCnt = ::DWORD64(); lCnt < lHeaderNT->FileHeader.NumberOfSections; lCnt++, lHeaderSection++) 
     { 
     ::DWORD64 lSectionBase = lHeaderSection->VirtualAddress; 
     ::DWORD64 lSectionEnd = lSectionBase + max(lHeaderSection->SizeOfRawData, lHeaderSection->Misc.VirtualSize); 
     if((lRVA >= lSectionBase) && (lRVA <= lSectionEnd)) 
     { 
      lNumberSection = lCnt + 1; 
      lOffsetSection = lRVA - lSectionBase; 
      break; 
     } 
     }  
     std::cout << lNameModule << " : 000" << lNumberSection << " : " << reinterpret_cast< void * >(lOffsetSection) << std::endl; 
    } 
    else 
    { 
     break; 
    } 
    } 
} 

void Run(void); 
void Run(void) 
{ 
GenerateReport(); 
std::cout << "------------------" << std::endl; 
} 

int main(void) 
{ 
    ::SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); 
    ::SymInitialize(::GetCurrentProcess(), 0, 1); 

    try 
    { 
    Run(); 
    } 
    catch(...) 
    { 
    } 
    ::SymCleanup(::GetCurrentProcess()); 

    return (0); 
} 

注意,我們調用堆棧(內而外)GenerateReport()->Run()->main()。 程序輸出(在我的機器上,路徑是絕對的):

D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000002F8D 
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 00000000000031EB 
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000003253 
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000007947 
C:\Windows\system32\kernel32.dll : 0001 : 000000000001552D 
C:\Windows\SYSTEM32\ntdll.dll : 0001 : 000000000002B521 
------------------ 

現在,在地址方面調用堆棧(內而外)00002F8D->000031EB->00003253->00007947->0001552D->0002B521。 比較前三偏移.map文件內容:

... 

0001:00002f40  [email protected]@YAXXZ  0000000140003f40 f FMain.obj 
0001:000031e0  [email protected]@YAXXZ    00000001400041e0 f FMain.obj 
0001:00003220  main      0000000140004220 f FMain.obj 

... 

其中00002f40最接近較小的偏移00002F8D等。最後三個地址指的是調用main_tmainCRTstartup等),CRT/OS的功能 - 我們應該忽略他們...

所以,我們可以看到,我們能夠恢復堆棧跟蹤與.map文件的幫助。爲了生成拋出異常的堆棧跟蹤,你所要做的就是將GenerateReport()代碼放入異常構造函數(實際上,這個GenerateReport()取自我自定義的異常類構造函數代碼(它的某些部分))。

+0

哇,這聽起來非常強大,我很想看看你會如何實現它!我設法創建.map文件。如果您想幫助我,請告訴我。 – user667967 2012-02-26 11:06:33

+0

@ user667967已發佈代碼。 – lapk 2012-02-28 01:13:07

+1

非常感謝您的幫助!我只是測試你的代碼,它的工作原理! – user667967 2012-03-01 04:30:55

1

我建議在看你的Visual Studio項目的設置:連接器 - >高級 - >對所有程序隨機基地址和相關DLL (你可以重建),然後再試一次。這是唯一想到的一件事。

希望有所幫助。

+1

避開ASLR是不明智的。 – 2012-02-24 03:58:45

+0

感謝您的快速和骯髒的解決方案!我會在短期內使用它。 – user667967 2012-02-26 11:07:50

2

您需要發送程序的運行內存映射,它告訴您從客戶端加載到您的基地址庫/程序。

然後你可以用基地址計算符號。

3

堆棧本身是不夠的,您需要加載模塊映射,以便您可以將任何地址(隨機,真)與模塊關聯,並找到PDB符號。但是,你真的重新發明輪子,因爲有至少兩個很好的支持外的現成解決方案來解決這個問題:

  • Windows的特定DbgHlp轉儲API:MiniDumpWriteDump。你的應用程序不應該直接調用它,而是應該附帶一個很小的.exe文件,它只需要轉儲一個進程(作爲參數給出的進程ID)和你的應用程序,當遇到錯誤情況時,應該啓動它。然後等待其完成。原因是「傾卸」過程會在轉儲過程中凍結轉儲過程,因此轉儲過程不能是轉儲過程中的同一過程。該方案適用於所有實施WER的應用程序。更不用說,結果轉儲是一個真正的.mdmp,你可以在WinDbg中加載(或者在VisualStudio中,如果你喜歡的話)。

  • 跨平臺開源解決方案:Breakpad。由Chrome,Firefox,Picassa和其他衆所周知的應用程序使用。

所以,主要是,不要重新發明輪子。作爲附帶說明,還有一些服務可以爲錯誤報告增值,例如彙總,通知,跟蹤和自動客戶端響應(如上述WER提供的WER(您的代碼必須經過數字簽名以符合資格),airbreak.io,exceptioneer.combugcollect.com(這是一個真正由你創造的)和其他,但afaik。只有WER與本機Windows應用程序一起使用。

+0

+1我喜歡這樣詳盡的答案。 – 2012-02-24 04:01:37