用Debug函數實作API函數的跟蹤
如果我們能自己編寫一個類似調試器的功能,這個調試器需要實作我們對于跟蹤監視工具的要求,即自動記錄輸入輸出參數,自動讓目标程序繼續運作。下面我們就來介紹在不知道函數原型的情況下也可以簡單輸出監視結果的方案——用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; 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函數監視的任務。
對目标程序設定斷點:
我們的目标是監視API函數的輸入輸出,那麼,首先應該知道DLL子產品中提供了哪些API函以及這些API的入口位址。在前面将過,廣義的API還包括未導出的内部函數。如果你有DLL子產品的調試版本和調試連接配接檔案(pdb檔案),也可以根據調試資訊得到内部函數的資訊。
· 得到函數名及函數入口位址
通過程式得到函數的入口位址有很多種方法。對于用VC編譯出來的DLL,如果是Debug版本,可以通過ImageHlp庫函數得到調試資訊,分析出函數的入口位址。如果沒有Debug版本,也可以通過分析導出函數表得到函數的入口位址。
1.用Imagehlp庫函數得到Debug版本的函數名和函數入口位址。
可以利用Imagehlp庫函數分析Debug資訊,關聯的函數為SymInitialize、SymEnumerateSymbols和UnDecorateSymbolName。詳細可以參考MSDN中關于這些函數的說明和用法。不過,用Imagehlp隻能分析出用VC編譯的程式,對C++Builder編譯的程式不能用這種方法分析。
2.DLL的導出表得到函數導出函數名和函數的入口位址。
在大多數情況下,我們還是希望監視的是Release版本的輸入輸出參數,畢竟Debug版本不是我們最終提供給使用者的産品。Debug和Release的編譯條件不同導緻産生的結果不同,在很多BBS中都讨論過。是以,我認為跟蹤監視Release版本更加有實用價值。
通過分析DLL導出表得到導出函數名在MSDN上就有源代碼。關于導出表的說明大家可以參考關于PE結構的文章。
3.通過OLE函數取得COM接口
你也可以通過OLE函數分析DLL提供的接口函數。接口函數不是通過DLL導出表導出的。你可以通過LoadTypeLib函數來分析COM接口,得到COM記錄接口的入口位址,這樣,你就可以監視COM接口的調用了。這是API HOOK沒法實作的。在這裡我不打算分析分析COM接口的方式了。在MSDN上通過搜尋LoadTypeLib sample關鍵詞你就可以找到相關的源代碼進行修改實作你的目标。
這裡是通過計算機自動分析目标子產品得到DLL導出函數的方案,作為我們監視的目的而言,這些工作隻是為了得到一系列的函數名和函數位址而已。函數名隻是一個讓我們容易識别函數的名稱而已,該函數入口位址才是我們真正關心的目标。換句話說,如果你能夠確定某一個位址一定是一個函數(包括内部函數)的入口位址,你就完全可以給這個函數定義自己的名稱,将它加入你的函數管理表中,同樣可以實作監視該函數的輸入輸出參數的功能。這也是實作Exe内部函數的監視功能的原因。如果你有Exe編譯時生成的Map檔案(你可以在編譯時選擇生成Map檔案),你就可以通過分析Map檔案,得到内部函數的入口位址,将内部函數加入到你的函數管理表中。(一個函數的名稱對于監視功能來講究竟是FunA還是FunB并沒有什麼意義,但名稱是FunA還是FunB的名稱對于監視者分析監視結果是有意義的,你完全可以将MessageBox的函數在輸出監視結果是以FunA的名稱輸出,是以在監視一些内部無名稱的函數時,你完全可以定義你自己的名字)。
· 在函數入口位址處設定斷點
設定斷點非常簡單,隻要将0xCC(int 3)寫入指定的位址就可以了。這樣程式運作到指定位址時,将産生調試中斷資訊通知調試程式。修改指定程序的記憶體資料可以通過WriteProcessMemory函數來完成。由于一般情況下作為程式代碼段都被保護起來了,是以還有一個函數也會用到。VirtualProtectEx。在實際情況下,當調試斷點發生時,調試程式還應該将原來的代碼寫回被調試程式。
unsigned char SetBreakPoint(DWORD pAdd, unsigned char code) unsigned char b; BOOL rc; DWORD dwRead, dwOldFlg; // 0x80000000以上的位址為系統共有區域,不可以修改 if( pAdd >= 0x80000000 || pAdd == 0) return code; // 取得原來的代碼 rc = ReadProcessMemory(_ghDebug, pAdd, &b, sizeof(BYTE), &dwRead); // 原來的代碼和準備修改的代碼相同,沒有必要再修改 if(rc == 0 || b == code) return code; // 修改頁碼保護屬性 VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), PAGE_READWRITE, &dwOldFlg); // 修改目标代碼 WriteProcessMemory(_ghDebug, pAdd, &code, sizeof(unsigned char), &dwRead); // 恢複頁碼保護屬性 VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), dwOldFlg, &dwOldFlg); return b; |
在設定斷點時你必須将原來的代碼儲存起來,這樣在恢複斷點時就可以将代碼還原了。一般用法為:設定斷點m_code = SetBreakPoint( pFunAdd, 0xCC); 恢複斷點:SetBreakPoint( pFunAdd, m_code); 記住,每個函數入口位址的代碼都可能不同,你應該為每個斷點位址儲存一個原來的代碼,在恢複時就不會發生錯誤了。
好了,現在目标程式中已經設定好了斷點,當目标程式調用設定了斷點的函數時,将産生一個調試中斷資訊通知調試程式。我們就要在調試程式中編寫我們的調試中斷程式了。
編寫調試中斷處理程式
被調試程式産生中斷時,将産生一個EXCEPTION_DEBUG_EVENT資訊通知調試程式進行處理。同時将填充EXCEPTION_DEBUG_INFO結構。
typedef struct _EXCEPTION_DEBUG_INFO { EXCEPTION_RECORD ExceptionRecord; DWORD dwFirstChance; } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO; typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD, *PEXCEPTION_RECORD; |
在該結構中,我們比較感興趣的是産生中斷的位址ExceptionAddress和産生中斷的資訊代碼ExceptionCode。在資訊代碼中與我們任務相關的資訊代碼為:
EXCEPTION_BREAKPOINT:斷點中斷資訊代碼 EXCEPTION_SINGLE_STEP:單步中斷資訊代碼 |
斷點中斷是由于我們在前面設定斷點0xCC代碼運作時産生的。由于産生中斷後,我們必須将原來的代碼寫回被調試程式中繼續運作。但是,代碼一旦被寫回目标程式,這樣,當目标程式再次調用該函數時将不會産生中斷,我們就隻能實作一次監視了。是以,我們必須在将原代碼寫回被調試程式後,應該讓被調試程式已單步的方式運作,再次産生一個單步中斷的調試資訊。在單步中斷進行中,我們再次将0xCC代碼寫入函數的入口位址,這樣就可以保證再次調用時産生中斷。
首先,在進行中斷處理前我們必須作些準備工作,管理起線程ID和線程句柄。為了管理單步中斷處理,我們還必須維護一個基于線程的單步位址的管理,這樣就可以允許被調試程式擁有多線程的功能。--我們不能保證單步運作時不被該程序的其他線程所打斷。
// 我們利用一個map進行管理線程ID和線程句柄之間的關系 // 同時也用一個map管理函數位址和斷點的關系 typedef map<DWORD, HANDLE, less<DWORD> > THREAD_MAP; typedef map<DWORD, void*, less<DWORD> > THREAD_SINGLESTEP_MAP; THREAD_MAP _gthreads; FUN_BREAK_MAP _gFunBreaks; // 并且假設設定斷點時采用了如下方案進行原來代碼的管理 BYTE code = SetBreakPoint(pFunAdd, 0xCC); if(code != 0xCC) _gFunBreaks[pFunAdd] = code; … // 調試處理程式 BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent) BOOL rc = TRUE; switch(pEvent->dwDebugEventCode) case CREATE_PROCESS_DEBUG_EVENT: // 記錄線程ID和線程句柄的關系 _gthreads[pEvent->dwThreadId] = pEvent->u.CreateProcessInfo.hThread; case CREATE_THREAD_DEBUG_EVENT: _gthreads [pEvent->dwThreadId] = pEvent->u.CreateThread.hThread; case EXIT_THREAD_DEBUG_EVENT: // 線程退出時清除線程ID _gthreads.erase (pEvent->dwThreadId); case EXCEPTION_DEBUG_EVENT: // 中斷處理程式 rc = OnDebugException(pEvent); return rc; |
下面進行中斷處理程式。同樣,我們隻考慮我們關心的中斷資訊代碼。在發生中斷時,我們通過GetThreadContext(&context)得到中斷線程的上下文資訊。此時,context.esp就是函數的傳回位址,context.esp+4位置的值就是函數的第一個參數,context.esp+8就是第二個參數,依次類推可以得到你想要的任何參數。需要注意的是因為參數是在被調試程序中的内容,是以你必須通過ReadProcessMemory函數才能得到:
DWORD buf[4]; // 取4個參數 ReadProcessMemory(_ghDebug, (void*)(context.esp + 4), &buf, sizeof(buf), &dwRead); |
那麼buf[0]就是第一個參數,buf[1]就是第二個參數。。。注意,在FunA(int a, char* p, OPENFILENAME* pof)函數調用時,buf[0] = a, buf[1] = p這裡buf[1]是p的指針而不是p的内容,如果你希望通路p的内容,必須同樣通過ReadProcessMemory函數再次取得p的内容。對于結構體指針也必須如此:
// 取得p的内容: char pBuf[256]; ReadProcessMemory(_ghDebug, (void*)(buf[1]), &pBuf, sizeof(pBuf), &dwRead); //取得pof的内容: OPENFILENAME of ReadProcessMemory(_ghDebug, (void*)(buf[2]), &of, sizeof(of), &dwRead); |
如果結構體中還有指針,要取得該指針的内容,也必須和取得p的内容一樣的方式讀取被調試程式的記憶體。總的來說,你必須意識到監視目标程式的所有内容都是對目标程序的記憶體讀取操作,這些指針都是目标程序的記憶體位址,而不是調試程序的位址。
很明顯,當被調試程序在函數入口産生中斷調試資訊時,調試程式隻能得到函數的輸入參數,而不能得到我們希望的輸出參數及傳回值!為了實作我們的目标,我們必須在函數調用結束時,再次産生中斷,取得函數的輸出參數和傳回值。在處理函數入口中斷時,就必須設定好函數的傳回位址的斷點。這樣,在函數傳回時,就可以得到函數的輸出參數和傳回值了。關于這裡的實作說明請參考附錄的源代碼。
你完全可以參照附錄的源代碼寫出你自己的簡單的調試監視程式。當然,有幾個問題因為比較複雜,我沒有在這裡進行說明。一個就是函數傳回斷點的處理,比如TRY、CATCH的處理,就必須重新設計好RETURN_FUN_STACK的結構,考慮一些除錯處理還是可以解決這個問題的。另外一個問題就是函數的入口斷點和傳回斷點沒有任何關系。這個問題更好解決,隻需重新設計RETURN_FUN,FUN_BREAK_MAP等結構體就可以将它們關聯起來。由于我在這裡隻要是分析如何實作中斷調試處理的過程,這些完善程式的工作就由讀者自行跟蹤改造了。
關于Win9X系統
細心的讀者在上面可以發現一個問題,那就是在SetBreakPoint函數中有一個限制,就是函數的入口位址不能大于0x80000000。确實如此,我們知道0x80000000以上的空間是系統共有的空間,我們一般不能修改這些空間的程式,否則将影響系統的工作。在NT環境下,所有的DLL都被加載在0x80000000下,修改0x80000000以下空間的代碼不會對其它程序産生影響。是以在NT下可以用上面的方案監視所有的DLL函數。然而,在Win9X下,kernel32.dll,user32.dll,gdi32.dll等系統DLL都被加載到0x80000000以上的空間,修改這些空間的代碼将破壞系統工作。那麼,在9X下就不能監視這些DLL子產品的函數嗎?
的确,在Win9X平台下不能利用在函數入口處設定斷點的方法實作監視。我們必須采用另外的方法實作該功能。在前面讨論中知道,通過API HOOK修改子產品導入表的方法可以實作将API的入口修改為自己監視程式的入口,也可以實作監視功能。如果采用API HOOK的方法有限制,即必須知道函數原型,對每一個函數都必須編寫相應的監視代碼,靈活性受到限制。而我們的目标是不管有多少個DLL,不管DLL有多少個導出函數,在不修改我們的程式前提下都可以實作我們的監視功能。是以,API HOOK是不可以完成我們的目标,但我們可以利用修改導入表的方案實作目标。首先,修改導入表,将函數的調用位址指向我們的監視代碼,在監視代碼中,我們無需對函數程式設計,隻是簡單調用jmp XXXX就可以了。然後,設定斷點時,不是設定在函數的入口點,而是設定在我們的監視代碼上。這樣,當我們的子產品調用系統API函數時,就可以實作監視功能了。修改原理如圖:
如圖所示,假設我們的監視代碼在目标程序的的0x20000000空間,我們在分析DLL導出表的同時,将導出表函數的位址經過計算,在監視代碼中設定為jmp xxxx的代碼。這樣我們在修改EXE子產品的導入表時寫入的位址為監視代碼的位址。當目标程式調用MessageBox函數是,程式将首先跳轉到監視代碼中執行jmp指令到user32.dll的MessageBox入口位址中。經過這樣處理後,我們希望監視MessageBox函數的調用時,隻需在監視代碼的0x20000000處設定斷點,就達到了監視的目的。限于篇幅原因,這裡不再讨論。
擴充應用
你可以很輕松的在此基礎上進行擴充你的監視跟蹤功能。隻需要修改一下記錄輸入輸出函數結果的程式,就得到一個新的功能:
1.在記錄輸入輸出參數的地方加入取得目前時刻的功能,就實作了監視函數調用性能的功能。(相當于Numega的TrueTime功能)由于采用了Debug技術,得到的時間将包括調試函數導緻産生程序的切換時間。等到的時間隻是一個參考價值,但對分析性能而言一般足夠。
2.在記錄輸入輸出參數的地方加入函數調用的計數器,就實作了Numega的TrueCoverage功能。
3.監視malloc, free, realloc函數的輸入輸出值,并進行統計,就實作了簡單的記憶體洩漏檢查功能。關鍵的是你可以通過Map檔案得到Release版本的malloc等函數的位址,實作對Release版的跟蹤。
4.在記錄輸入參數進行中加入StackWalk函數可以實作call stack功能,分析是由哪個函數調用了自己。在jmp方案中也可以實作這個功能,但是你必須確定StackWalk關聯的函數沒有調用被你監視的函數。在Hook API(IAT)的方案中到是不用保證,但得出的調用清單中有可能包含你的監視代碼。
有一點需要注意的是,我們的目标是監視程式的運作路徑,并不是改變參數和修改結果,是以,在jmp和Hook Api(IAT)中可以實作的修改參數和運作路徑的做法在這裡不能實作。
其他:
本文附錄的代碼TestDebug.zip就是實作了一個簡單的調試螢幕,自動輸出監視函數的4個輸入參數的位址内容和函數調用傳回值。該代碼隻是表明通過監視函數可以實作對API的跟蹤,是以沒有實作9X下對系統DLL的監視。
DebugApi.zip是一個利用這個方案編寫的應用程式DebugApiSpy.exe,它實作了這個方案中的最基本的跟蹤監視函數的輸入輸出參數功能,也實作了9X下對系統DLL的監視支援。該程式支援Win9X/NT/W2K/XP上的運用。