天天看點

用Debug函數實作API函數的跟蹤(1)

如果我們能自己編寫一個類似調試器的功能,這個調試器需要實作我們對于跟蹤監視工具的要求,即自動記錄輸入輸出參數,自動讓目标程序繼續運作。下面我們就來介紹在不知道函數原型的情況下也可以簡單輸出監視結果的方案——用Debug函數實作API函數的監視。

用Debug函數實作API函數的監視

大家知道,VC可以用來調試程式,除了調試Debug程式,當然也可以調試Release程式(調試Release程式時為彙編代碼)。如果知道函數的入口位址,隻需在函數入口上設定斷點,當程式調用了設定斷點的函數時,VC就會暫停目标程式的運作,你就可以得到目标程式記憶體的所有你希望得到的東西了。一般來說,隻要你有足夠的耐心和毅力,以及一些彙編知識,對于監視API函數的輸入輸出參數還是可以完成的。

不過,由于VC的調試器會在每次斷點時暫停目标程式的運作,對目标程式的過多的暫停對于監視任務而言實在不能忍受。是以,不會有太多的人真的會用VC的調試器作為一個良好的API函數螢幕的。

如果VC調試器能夠在你設定好斷點後,在運作時自動輸出斷點時的堆棧值(也就是函數的輸入參數),在函數運作結束時也自動輸出堆棧值(也就是函數的輸出參數)和CPU寄存器的值(就是函數傳回值),并且不會暫停目标程式。所有一切都是自動的無需我們幹預。你會用它來作為螢幕嗎?我會的。

我不知道如何讓VC這樣作(或許VC真的可以這樣,但我不知道。有人知道的話請通知我一聲,謝謝),但我知道顯然VC也是通過調用Windows API函數完成調試器的任務,而且,這些函數顯然可以實作我的要求。我需要作的事情就是自己利用這些API函數,寫一個簡單的調試器,在目标程式斷點發生時自動輸出監視結果并且自動恢複目标程式的運作。

顯然,用VC調試器作為螢幕的話無需知道目标函數的原型就可以得到簡單的輸入輸出參數和函數運作結果,而且,由于監視代碼沒有注入目标程式中,就不會出現監視目标函數和監視代碼的沖突。VC調試器顯然可以跟蹤遞歸函數,也可以跟蹤DLL子產品調用DLL本身的函數,以及EXE内部調用自身的函數。隻要你知道目标函數的入口位址,就可以跟蹤了(監視Exe自身的函數可以通過生成Exe子產品時選擇輸出Map檔案,就可以參考Map檔案得到Exe内部函數的位址)。沒有聽說VC不能調試多線程的,最多是說調試多線程比較麻煩----證明多線程是可以調試的。顯然,VC也可以調試DllMain中的代碼。這些,已經可以證明通過調試函數可以實作我們的目标了。

如何編寫實作我們目标的程式?需要哪些調試函數?

首先,讓目标程式進入被調試狀态:

對于一個已經啟動的程序而言,利用DebugActiveProcess函數就可以捕獲目标程序,将目标程序進入被調試狀态。

BOOL DebugActiveProcess(DWORD dwProcessId);      

參數dwProcessId是目标程序的程序ID。如何通過ToolHelp系列函數或Psapi庫函數獲得一個運作程式的程序ID在很多文章中介紹過,這裡就不再重複。對于伺服器程式而言,由于沒有權限無法捕獲目标程序,可以通過提升監視程式的權限得到調試權限進行捕獲目标程序(使用者必須擁有調試權限)。

對于啟動一個新的程式而言,通過CreateProcess函數,設定必要的參數就可以将目标程式進入被調試狀态。

BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine, 
LPSECURITY_ATTRIBUTES lpProcessAttributes,  LPSECURITY_ATTRIBUTES 
lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID 
lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, 
LPPROCESS_INFORMATION lpProcessInformation );      

該函數的具體說明請參考MSDN,在這裡我僅介紹我們感興趣的參數。這裡和一般的用法不同,作為被調試程式dwCreationFlags必須設定為DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。這樣啟動的目标程式就會進入被調試狀态。這裡說明一下DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。DEBUG_ONLY_THIS_PROCESS就是隻調試目标程序,而DEBUG_PROCESS參數則不僅調試目标程序,而且調試由目标程序啟動的所有子程序。比如:在A.exe中啟動B.exe,如果用DEBUG_ONLY_THIS_PROCESS啟動,監視程序隻調試A.exe不會調試B.exe,如果是DEBUG_PROCESS就會調試A.exe和B.exe。為簡單起見,本文隻讨論啟動參數為DEBUG_ONLY_THIS_PROCESS的情況。

使用方法:

STARTUPINFO st = {0};
PROCESS_INFORMATION pro = {0};
st.cb = sizeof(st);
CreateProcess(NULL, pszCmd, NULL, NULL, FALSE,					
DEBUG_ONLY_THIS_PROCESS,
NULL, szPath, &st, &pro));
// 關閉句柄---這些句柄在調試程式中不再使用,是以可以關閉
CloseHandle(pro.hThread);
CloseHandle(pro.hProcess);      

其次,對進入被調試狀态的程式進行監視:

目标程序進入了被調試狀态,調試程式(這裡調試程式就是我們的監視程式,以後不再說明)就負責對被調試的程式進行調試操作的排程。調試程式通過WaitForDebugEvent函數獲得來自被調試程式的調試消息,調試程式根據得到的調試消息進行處理,被調試程序将暫停操作,直到調試程式通過ContinueDebugEvent函數通知被調試程式繼續運作。

BOOL WaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,  // debug event information
  DWORD dwMilliseconds         // time-out value
);      

在參數lpDebugEvent中可以獲得調試消息,需要注意的是該函數必須和讓目标程式進入調試狀态的線程是同一線程。也就是說和通過DebugActiveProcess或CreateProcess調用的線程是一個線程。另外,我又喜歡将dwMilliseconds設定為-1(無限等待)。是以我通常都會将CreateProcess和WaitForDebugEvent函數在一個新的線程中使用。

typedef struct _DEBUG_EVENT { 
  DWORD dwDebugEventCode; 
  DWORD dwProcessId; 
  DWORD dwThreadId; 
  union { 
        EXCEPTION_DEBUG_INFO Exception; 
    CREATE_THREAD_DEBUG_INFO CreateThread; 
    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; 
        EXIT_THREAD_DEBUG_INFO ExitThread; 
    EXIT_PROCESS_DEBUG_INFO ExitProcess; 
      LOAD_DLL_DEBUG_INFO LoadDll; 
      UNLOAD_DLL_DEBUG_INFO UnloadDll; 
      OUTPUT_DEBUG_STRING_INFO DebugString; 
      RIP_INFO RipInfo; 
   } u; 
} DEBUG_EVENT, *LPDEBUG_EVENT;      

在這個調試消息結構體中,dwDebugEventCode記錄了産生調試中斷的消息代碼。消息代碼的詳細說明可以參考MSDN。其中,我們感興趣的消息代碼為:

EXCEPTION_DEBUG_EVENT:産生調試例外
CRATE_THREAD_DEBUG_EVENT:新的線程産生
CREATE_PROCESS_DEBUG_EVENT:新的程序産生。注:在DEBUG_ONLY_THIS_PROCESS時隻有一次,
在DEBUG_PROCESS時如果該程式啟動了子程序就可能有多次。
EXIT_THREAD_DEBUG_EVENT:一個線程運作中止
EXIT_PROCESS_DEBUG_EVENT:一個程序中止。注:在DEBUG_ONLY_THIS_PROCESS時隻有一次,
在DEBUG_PROCESS可能有多次。
LOAD_DLL_DEBUG_EVENT:一個DLL子產品被載入。
UNLOAD_DLL_DEBUG_EVENT:一個DLL子產品被解除安裝。      

在得到目标程式的調試消息後,調試程式根據這些消息代碼進行不同的處理,最後通知被調試程式繼續運作。

BOOL ContinueDebugEvent(
  DWORD dwProcessId,       // process to continue
  DWORD dwThreadId,        // thread to continue
  DWORD dwContinueStatus   // continuation status
);      

該函數通知被調試程式繼續運作。

使用例:

DEBUG_EVENT dbe;
BOOL rc;
CreateProcess(NULL, pszCmd, NULL, NULL, FALSE,					
DEBUG_ONLY_THIS_PROCESS,
NULL, szPath, &st, &pro));
while(WaitForDebugEvent(&dbe, INFINITE))
{
// 如果是退出消息,調試監視結束
if(dbe. dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
break;
// 進入調試監視處理
rc = OnDebugEvent(&dbe);
if(rc)
ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId , DBG_CONTINUE );
else
ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId , 
DBG_ DBG_EXCEPTION_NOT_HANDLED);
}
// 調試消息處理程式
BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)
{
// 我們還沒有對目标程序進行操作,是以,先傳回TRUE。
return TRUE;
}      

上面這些程式就是一個最簡單的調試程式了。不過,它基本上沒有什麼用途。你還沒有在目标程序中設定斷點,你就不能完成對API函數監視的任務。