調試器

爲一個軟件逆向工程的研究者,必然會用到調試工具,如果能夠掌握調試器工作的原理必然對軟件調試會有更深入的瞭解。
 
本文根據筆者自己實現一個完整的調試器的過程,結合OD在調試功能上的實現方法,詳細講述一個調試器其主要功能的實現步驟,及實現過程中需要注意的細節,力求在原理、方法上能讓閱讀者有所收穫。由於本人能力有限,文中必然有錯漏之處,懇請讀者不吝賜教。
 
第一章 調試器框架流程、用戶輸入處理及三種斷點介紹
windows三環下的調試器是利用異常處理來實現的。其主要流程可簡要概述如下:創建或附加一個調試進程,然後進入等待調試事件的循環,整個調試工作大部分都在處理異常調試事件中完成。除了進程創建時需要在入口點設斷點(以便程序斷在入口點)、進程結束時進行一些必要的釋放資源的操作。
創建調試進程和普通的創建進程一樣都是使用CreateProcess API。只是創建調試進程時需要設置CreateProcess的第6個參數dwCreationFlags爲 DEBUG_PROCESS,這些細節都可以在MSDN中查到。
 
等待調試事件的循環可以直接在MSDN中找到VC的源碼,在MSDN搜索API:WaitForDebugEvent,然後看Debugging Overview,中的Using Debugging Support。即可找到VC代碼如下:
for(;;) 

WaitForDebugEvent(&DebugEv, INFINITE); 
switch (DebugEv.dwDebugEventCode) 

case EXCEPTION_DEBUG_EVENT: 
switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode) 

case EXCEPTION_ACCESS_VIOLATION: 
case EXCEPTION_BREAKPOINT: 
case EXCEPTION_DATATYPE_MISALIGNMENT: 
case EXCEPTION_SINGLE_STEP: 
case DBG_CONTROL_C: 

case CREATE_THREAD_DEBUG_EVENT: 
case CREATE_PROCESS_DEBUG_EVENT: 
case EXIT_THREAD_DEBUG_EVENT: 
case EXIT_PROCESS_DEBUG_EVENT: 
case LOAD_DLL_DEBUG_EVENT: 
case UNLOAD_DLL_DEBUG_EVENT: 
case OUTPUT_DEBUG_STRING_EVENT: 

ContinueDebugEvent(DebugEv.dwProcessId, 
DebugEv.dwThreadId, dwContinueStatus); 
}
 
爲節省篇幅,我將代碼中的註釋已經刪除,詳細的註釋說明請參閱MSDN。
 
通過這個循環我們可以看到WaitForDebugEvent這個API可以獲得的調試事件類型包括了“異常事件”,“創建線程事件”,“創建進程事件”,“退出線程事件”,“退出進程事件”,“加載DLL事件”,“卸載DLL事件”,“調試輸出字符串事件”。
在創建調試進程成功之後,會捕獲“創建進程事件”,在創建進程事件中我們可以獲得程序的入口地址(OEP),我們可以在OEP處設置一個一次性的軟件斷點(INT3),使得我們的被調試程序有機會斷在OEP處等待用戶輸入。
 
用戶的輸入包括查看反彙編代碼,查看數據,查看寄存器內容,下各種斷點,單步步入,單步步過,運行(到某地址)等各種命令。斷點一般包括軟件斷點(INT3斷點),硬件斷點和內存斷點。
 
用戶輸入的命令可分爲兩種類型:
1. 控制程序流程的命令。如單步步入(T)、步過(P),運行(G)等;
2. 斷點相關操作,如設置(bp)、查看(bl)、取消(bc)各種斷點;查看反彙編代碼(U)、數據(D)、寄存器(R)等。
可以設計一個等待用戶輸入的函數,當用戶輸入的是T、P、G時,函數返回值爲TRUE,表示程序需要往下運行(單步或運行)。爲其他輸入時,函數返回值爲FALSE,表示其他的操作或錯誤操作。
 
三種斷點分別對應的異常類型及處理:
 
1. 軟件斷點(INT3):對應的異常是 EXCEPTION_BREAKPOINT,處理方式爲恢復代碼爲原來字節,並將EIP減一,並設置單步以便進入單步後重新設置這個一般斷點。
 
2. 硬件斷點:對應的異常是 EXCEPTION_SINGLE_STEP,處理方式要看是否爲硬件執行斷點,如果是硬件執行斷點,則先取消斷點,設置單步,進入單步後重新設置這個硬件執行斷點。
 
3.內存斷點:將要設置內存斷點的分頁屬性設置爲 PAGE_NOACCESS,則會產生EXCEPTION_ACCESS_VIOLATION 異常。進入異常後先取消斷點,設置單步,進入單步後再重新設置這個內存斷點。
 
第一章先介紹到這裏,下一章將介紹軟件斷點(INT3斷點)。查看反彙編代碼,查看數據和查看寄存器值比較簡單我就不多說了,如果想要了解也可以從我以後發的源碼中閱讀到。
 
 
本系列文章參考書目、資料如下:
1.《加密與解密3》 編著:段鋼
2.《調試寄存器(DRx)理論與實踐》 作者:Hume/冷雨飄心
3.《數據結構》 作者:嚴蔚敏
 
附件爲《調試器使用手冊》和 調試器可執行文件。
上傳的附件 調試器使用手冊.rar
SunDbg.rar

  • 標 題:調試器實現(第二章)INT3斷點
  • 作 者:超然
  • 時 間:2010-04-29 23:19:17

一個調試器的實現
第二章 INT3斷點
 
     INT3斷點,簡單地說就是將你要斷下的指令地址處的第一個字節設置爲0xCC,軟件執行到0xCC(對應彙編指令INT3)時,會觸發異常代碼爲EXCEPTION_BREAKPOINT的異常。這樣我們的調試程序就能夠接收到這個異常,然後進行相應的處理。
 
INT3斷點的管理:

在我的程序中,INT3斷點的信息結構體如下:
struct stuPointInfo
{
PointType ptType; //斷點類型
int nPtNum; //斷點序號
LPVOID lpPointAddr; //斷點地址
BOOL isOnlyOne; //是否一次性斷點(針對INT3斷點)
char chOldByte; //原先的字節(針對INT3斷點)
};
而每一個INT3斷點信息結構體指針又保存到一個鏈表中。
 
INT3斷點的設置:

     設置INT3斷點比較簡單,只需要根據用戶輸入的斷點地址和斷點類型(是否一次性斷點),將被調試進程中對應地址處的字節替換爲0xCC,同時將原來的字節保存到INT3斷點信息結構體中,並將該結構體的指針加入到斷點鏈表中。
 
     一款好的軟件,無論大小,必然在程序的邏輯上要求嚴謹無誤。調試器的設計也不例外,如果在被調試的某地址處已經存在一個同樣的斷點了,那麼用戶還要往這個地址上設置相同的斷點,則必然會因爲重複設置斷點導致錯誤。例如這裏的INT3斷點,如果不對用戶輸入的地址進行是否重複的檢查,而讓用戶在同一個地址先後下了兩次INT3斷點,則後一次INT3斷點會誤以爲這裏本來的字節就是0xCC。所以在設置INT3斷點之前應該先看該地址是否已經下過INT3斷點,如果該地址已經存在一個INT3斷點,且是非一次性的,則不能再在此地址下INT3斷點,如果該地址有一個INT3一次性斷點,而用戶要繼續下一個INT3非一次性斷點,則將原來存在的INT3斷點的屬性從一次性改爲非一次性斷點。
 
以下是設置INT3斷點的一些關鍵代碼:
//在斷點列表中查找是否已經存在此處的一般斷點
stuPointInfo tempPointInfo;
stuPointInfo* pResultPointInfo = NULL;
memset(&tempPointInfo, 0, sizeof(stuPointInfo));
tempPointInfo.lpPointAddr = lpAddr;
tempPointInfo.ptType = ORD_POINT;
//判斷所下的INT3斷點是否是一次性斷點
if (stricmp(pCmd->chParam2, "once") == 0)
{
tempPointInfo.isOnlyOne = TRUE;

else
{
tempPointInfo.isOnlyOne = FALSE;
}
//如果查找到在要設置INT3斷點的地址處已經存在INT3斷點
if (FindPointInList(tempPointInfo, &pResultPointInfo, FALSE))
{
if (tempPointInfo.isOnlyOne == FALSE)//要設置的是非一次性斷點
{
if (pResultPointInfo->isOnlyOne == FALSE)//查找到的是非一次性斷點
{
printf("This Ordinary BreakPoint is already exist!\r\n");

else//查找到的是一次性斷點
{
//將查找到的斷點屬性改爲非一次性斷點
pResultPointInfo->isOnlyOne = FALSE;
}
}
return FALSE;

 
//要設置INT3斷點的位置不存在INT3斷點(也就是說,該地址可以設置INT3斷點)
char chOld;
char chCC = 0xcc;
DWORD dwOldProtect;
//先讀出原先的字節
bRet = ReadProcessMemory(m_hProcess, lpAddr, &chOld, 1, NULL);
if (bRet == FALSE)
{
printf("ReadProcessMemory error! may be is not a valid memory address!\r\n");
return FALSE;
}
//將要設置INT3斷點的地址處的字節改爲0xCC
bRet = WriteProcessMemory(m_hProcess, lpAddr, &chCC, 1, NULL);
if (bRet == FALSE)
{
printf("WriteProcessMemory error!\r\n");
return FALSE;
}
//將該INT3斷點信息結構體添加到斷點鏈表中
stuPointInfo* NewPointInfo = new stuPointInfo;
memset(NewPointInfo, 0, sizeof(stuPointInfo));
NewPointInfo->nPtNum = m_nOrdPtFlag;
m_nOrdPtFlag++;
NewPointInfo->ptType = ORD_POINT;
NewPointInfo->lpPointAddr = lpAddr;
NewPointInfo->u.chOldByte = chOld;
NewPointInfo->isOnlyOne = tempPointInfo.isOnlyOne;
g_ptList.push_back(NewPointInfo);
 
INT3斷點被斷下的處理:
     INT3斷點被斷下後,首先從斷點鏈表中找到對應的斷點信息結構體。如果沒有找到,則說明該INT3斷點不是用戶下的斷點,調試器不做處理,交給系統去處理(其他類型的斷點觸發異常也需要做同樣的處理)。如果找到對應的斷點,根據斷點信息將斷下地址處的字節還原爲原來的字節,並將被調試進程的EIP減一,因爲INT3異常被斷下後,被調試進程的EIP已經指向了INT3指令後的下一條指令,所以爲了讓被調試進程執行本來需要執行的指令,應該讓其EIP減1。
 
如以下代碼:
地址 機器碼 彙編代碼
01001959 55 push ebp
0100195A 33ED xor ebp,ebp
0100195C 3BCD cmp ecx,ebp
0100195E 896C24 04 mov dword ptr ss:[esp+4],ebp
 
當用戶在0100195A地址處設置INT3斷點後,0100195A處的字節將改變爲0xCC(原先是0x33)。
 
此時對應的代碼如下:
地址 機器碼 彙編代碼
01001959 55 push ebp
0100195A CC int3
0100195B ED in eax,dx
0100195C 3BCD cmp ecx,ebp
0100195E 896C24 04 mov dword ptr ss:[esp+4],ebp
 
     當被調試程序執行到0100195A地址處,觸發異常,進入異常處理程序後,獲取被調試線程的環境(GetThreadContext),可看出此時EIP指向了0100195B,也就是INT3指令之後,所以我們除了要恢復0xCC爲原來的字節之外,還要將被調試線程的EIP減一,讓EIP指向0100195A。否則CPU就會執行0100195B處的指令(0100195B ED in eax,dx),顯然這是錯誤的。
 
     如果查找到的斷點信息顯示該INT3斷點是一個非一次性斷點,那麼需要設置單步,然後在進入單步後將這一個斷點重新設置上(硬件執行斷點和內存斷點如果是非一次性的也需要做相同的處理)。因爲INT3斷點同時只會斷下一個,所以可以用一個臨時變量保存要重新設置的INT3斷點的地址,然後用一個BOOL變量表示當前是否有需要重新設置的INT3斷點。
 
關於INT3斷點的一些細節:

1. 創建調試進程後,爲了能夠讓被調試程序斷在OEP(程序入口點),我們可以在被調試程序的OEP處下一個一次性INT3斷點。
 
2. 在創建調試進程的過程中(程序還沒有執行到OEP處),會觸發一個ntdll.dll中的INT3,遇到這個斷點直接跳出不處理。這個斷點在使用微軟自己的調試工具WinDbg時會被斷下,可以猜測,微軟設置這個斷點是爲了能夠在程序到達OEP之前就被斷下,方便用戶做一些處理(如設置各種斷點)。
 
3. 因爲INT3斷點修改了被調試程序的代碼內容,所以在進行反彙編和顯示被調試進程內存數據的時候,需要檢查碰到的0xCC字節是否是用戶所下的INT3斷點,如果是需要替換爲原來的字節,再做相應的反彙編和顯示數據工作。這一點olldbg做的很不錯,而有一些國產的調試器好像沒有注意到這些小的細節。
 
本系列文章參考書目、資料如下:
1.《加密與解密3》 編著:段鋼
2.《調試寄存器(DRx)理論與實踐》 作者:Hume/冷雨飄心
3.《數據結構》 作者:嚴蔚敏

  • 標 題:調試器實現(第三章)硬件斷點
  • 作 者:超然
  • 時 間:2010-05-01 22:08:46

第三章 硬件斷點
 
一 硬件斷點介紹
 
     硬件斷點,顧名思義是由硬件提供給我們的調試寄存器組,我們可以對這些硬件寄存器設置相應的值,然後讓硬件幫我們斷在需要下斷點的地址。
 
     硬件斷點是CPU提供的功能,所以要怎麼做就得聽CPU的硬件寄存器的了。先來看看關於硬件寄存器的說明。Intel 80386以上的CPU提供了調試寄存器以用於軟件調試。386和486擁有6個(另外兩個保留)調試寄存器:Dr0,Dr1,Dr2,Dr3,Dr6和Dr7。這些寄存器均是32位的,如下圖所示(該圖來源於看雪文章《調試寄存器(DRx)理論與實踐》(http://www.pediy.com/bbshtml/BBS6/pediy6751.htm),在此對文章作者Hume/冷雨飄心表示感謝):
 
|---------------|----------------|
Dr0| 用於一般斷點的線性地址 
|---------------|----------------|
Dr1| 用於一般斷點的線性地址 
|---------------|----------------|
Dr2| 用於一般斷點的線性地址 
|---------------|----------------|
Dr3| 用於一般斷點的線性地址 
|---------------|----------------|
Dr4| 保留 
|---------------|----------------|
Dr5| 保留 
|---------------|----------------|
Dr6| |BBB BBB B |
| |TSD 3 2 1 0 |
|---------------|----------------|
Dr7|RWE LEN ... RWE LEN | G GLGLGLGLGL |
| 3 3 ... 0 0 | D E E 3 3 2 21 100 |
|---------------|----------------|
31 15 0
 
     Dr0~3用於設置硬件斷點,由於只有4個斷點寄存器,所以最多隻能設置4個硬件調試斷點,產生的異常是STATUS_SINGLE_STEP(單步異常) 。Dr7是一些控制位,用於控制斷點的方式,Dr6用於顯示是哪個硬件調試寄存器引起的斷點,如果是Dr0~3或單步(EFLAGS的TF)的話,則相對應的位會置一。 
即如果是Dr0引起的斷點,則Dr6的第0位被置1,如果是Dr1引起的斷點,則Dr6的第1位被置1,依此類推。因爲硬件斷點同時只會觸發一個,所以Dr6的低4位最多隻有一個位被置1,所以在進入單步後,我們可以通過檢測Dr6的低4位是否有值爲1的位,來判斷當前進入單步的原因是否是因爲硬件斷點被斷下。如果是因爲硬件斷點被斷下,也可以通過判斷Dr6的低4位中哪一位是1,來進一步判斷是被Dr0到dr3中的哪一個斷點所斷下。
 
調試控制寄存器Dr7比較重要,其32位結構如下:

1. 位0 L0和位1 G0:用於控制Dr0是全局斷點還是局部斷點,如果G0置位則是全局斷點,L0置位則是局部斷點。G1L1~G3L3用於控制D1~Dr3,其功能同上。
 
2. LE和GE:P6 family和之後的IA32處理器都不支持這兩位。當設置時,使得處理器會檢測觸發數據斷點的精確的指令。當其中一個被設置的時候,處理器會放慢執行速度,這樣當命令執行的時候可以通知這些數據斷點。建議在設置數據斷點時需要設置其中一個。切換任務時LE會被清除而GE不會被清除。爲了兼容性,Intel建議使用精確斷點時把LE和GE都設置爲1。 
 
3. LEN0到LEN3:指定調試地址寄存器DR0到DR3對應斷點所下斷的長度。如果R/Wx位爲0(表示執行斷點),則LENx位也必須爲0(表示1字節),否則會產生不確定的行爲。LEN0到LEN3其可能的取值如下:
(1)00 1字節
(2)01 2字節
(3)10 保留
(4)11 4字節
 
4. R/W0到R/W3:指定各個斷點的觸發條件。它們對應於DR0到DR3中的地址以及DR6中的4個斷點條件標誌。可能的取值如下:
(1) 00 只執行
(2) 01 寫入數據斷點
(3) 10 I/O端口斷點(只用於pentium+,需設置CR4的DE位,DE是CR4的第3位 )
(4) 11 讀或寫數據斷點
 
5. GD位:用於保護DRx,如果GD位爲1,則對Drx的任何訪問都會導致進入1號調試陷阱(int 1)。即IDT的對應入口,這樣可以保證調試器在必要的時候完全控制Drx。 
 
二 設置硬件斷點
 
     通過上面的介紹,我們知道設置一個硬件斷點一般只需要以下幾個步驟。
(1) 在Dr0到Dr3中找一個可用的寄存器,將其值填寫爲要斷下的地址。
(2) 設置Dr7對應的GX或LX位爲1。(例如斷點設置在Dr0上則設置Dr7的G0或L0位爲1)。
(3) 設置Dr7對應的斷點類型位(R/W0到R/W3其中之一)爲執行、寫入或訪問類型。
(4) 設置Dr7對應的斷點長度位(LEN0到LEN3其中之一)爲1、2或4字節。
 
 
三 處理硬件斷點
 
     在硬件斷點的介紹中已經說過,硬件斷點被斷下後將觸發單步異常,因此在進入單步異常後,我們檢測Dr6的低4位是否有值爲1的位,就可以判斷是否是因爲硬件斷點被斷下,以及進一步判斷是被Dr0到Dr3中的哪一個斷點所斷下。
硬件斷點有三種類型,硬件執行斷點、硬件訪問斷點和硬件寫入斷點。硬件斷點被斷下後,所斷下的位置(也就是程序的EIP值)會因爲斷點的類型不同而有差異。對於硬件執行斷點,會斷在所下斷點地址指令處,也就是EIP的值和斷點設置的值一樣,下斷點的指令還沒有被執行。而對於硬件訪問、寫入斷點,會斷在所下斷點地址指令的下一條指令處,也就是EIP的值已經是斷點指令後的下一條指令的地址了,下斷點地址處的指令已經被執行了。
 
     究其原因是因爲硬件執行斷點只需要查看EIP的值就可以判斷是否命中硬件執行斷點了,所以在指令執行前就可以斷下,而硬件訪問、寫入斷點是需要在CPU拿到完整指令代碼並譯碼完畢之後才能判斷是否命中了硬件訪問、寫入斷點的。此時EIP已經指向了下一條指令,又因爲intel的cpu指令是變長指令集,所以不易倒推實際觸發硬件訪問、寫入斷點的指令地址,所以intel對硬件訪問、寫入斷點的處理是停在觸發異常指令後的下一條指令處(這一段是我本人的理解,如有不對之處,請讀者多多指教)。
由於不同類型的硬件斷點觸發異常的情況不同,所以要區別對待。對於硬件執行斷點,觸發異常斷點被斷下後,要先暫時取消硬件執行斷點,然後設置單步,到下一次的單步中進行硬件斷點的恢復工作。對於硬件訪問、寫入斷點則不需要做多餘的處理,斷下後顯示一下斷點信息,並等待用戶操作就可以了。
 
     因爲硬件斷點的設計比較死板,照着intel手冊的說明一步步來就可以了。對Dr7的操作也就是一些位操作。我的代碼裏面是一個大大的SWITCH裏面套了4個小SWITCH來做的,顯得拖堂冗長、很不好看,所以就不放上來了。
 
硬件斷點設計需要注意的幾點如下:
 
1. 設置硬件斷點的時候也要檢查是否重複設置了。
 
2. 硬件執行斷點被斷下後,此時需要暫時取消掉該硬件執行斷點(否則程序一直被斷在這裏,跑不下去)。並設置單步,在下一次單步中重設該硬件執行斷點。
 
3. 如果硬件執行斷點被斷下之後,此時用戶執行了取消該斷點的操作,則不需要在下一次的單步中恢復該斷點的設置了(這一點同樣適用於INT3斷點和內存斷點)。
 
 
本系列文章參考書目、資料如下:
1.《加密與解密3》 編著:段鋼
2.《調試寄存器(DRx)理論與實踐》 作者:Hume/冷雨飄心
3.《數據結構》 作者:嚴蔚敏
上傳的附件 1.JPG

  • 標 題:第四章 多內存斷點
  • 作 者:超然
  • 時 間:2010-05-03 21:27:47

第四章 多內存斷點
 
    內存斷點通過修改內存分頁的屬性,使被調試程序觸發內存訪問、寫入異常而斷下。
 
多內存斷點的數據關係:
 
    因爲我設計的是多內存斷點,即在同一個內存分頁上可以下多個內存斷點,同一個斷點也可以跨分頁下在幾個內存分頁上。所以從數據關係上來說斷點和內存分頁是多對多的關係。因此需要設計三個表:“內存斷點信息表”,“內存分頁屬性表”,以及中間表“內存斷點-分頁對照表”。在用戶下內存斷點的時候,首先將斷點所跨越的內存分頁屬性加入到“內存分頁屬性表”中。然後在中間表“內存斷點-分頁對照表”中添加內存斷點對應的分頁信息,一個內存斷點對應了幾個分頁就會添加幾條信息。內存斷點的信息保存在“斷點信息表”中。
三個表的屬性字段如下:

 
 
內存斷點的設置:
 
    內存斷點的信息中需要用戶輸入確定的有:下斷點首地址、斷點的長度和斷點的類型(訪問還是寫入)。根據用戶輸入的信息可以組成一個臨時的內存斷點結構體,然後到內存斷點鏈表中查找是否已經存在同屬性的內存斷點,如果已經存在則不需要再設置,否則可以設置這個內存斷點。
 
    設置內存斷點,首先根據斷點的首地址和長度可以確定斷點所跨越的內存分頁,用VirtualQueryEx API獲取內存分頁的屬性,然後將內存分頁的屬性信息添加到“內存分頁表”中(需要注意的是,如果“內存分頁表”中已經存在同一內存分頁的屬性記錄了,則不需要再添加重複的記錄),同時將斷點對應分頁的信息添加到“內存斷點-分頁對照表”中,並設置斷點所跨過的每一個內存分頁的屬性爲不可訪問(PAGE_NOACCESS)。
 
    這一點和OllyDbg的做法不大一樣,OllyDbg設置內存訪問斷點是將斷點所跨分頁設置爲PAGE_NOACCESS屬性,而設置內存寫入斷點是將斷點所跨分頁屬性設置爲PAGE_EXECUTE_READ,而我的做法是不管哪種斷點都將斷點所跨內存頁的屬性設置爲PAGE_NOACCESS,這樣做的問題是會產生多餘的異常,好處是設置斷點,恢復斷點時省去類型的判斷。而且出於另外一個考慮,OllyDbg是隻能設置一個內存斷點的,所以它這樣設置合情合理,而我設計的是多內存長度任意的斷點。假設出現了用戶先在某個分頁上下了一個內存寫入斷點,之後用戶又在同一個分頁上下了內存訪問斷點,那麼如果按照OllyDbg的方式,先將內存頁的屬性設置爲PAGE_EXECUTE_READ,然後處理後一個內存斷點時,將內存頁的屬性設置爲PAGE_NOACCESS。而如果相反,出現了用戶先在某個分頁上下了一個內存訪問斷點,之後用戶又在同一個分頁上下了內存寫入斷點,內存頁的屬性首先被改爲PAGE_NOACCESS,但不能根據第二個斷點將內存頁的屬性改爲PAGE_EXECUTE_READ,否則前一個內存訪問斷點就失效了。與其因設置不同的屬性產生這麼多種麻煩的情況,不如犧牲一點效率(多了一些異常的情況),對內存訪問和寫入斷點都將斷點所跨過的分頁屬性設置爲PAGE_NOACCESS,再通過斷點被斷下後,異常記錄結構體EXCEPTION_RECORD中的訪問標誌和斷點信息中的類型標誌來判斷是否命中了用戶所下的內存斷點。
 
    處理完內存頁的屬性,將內存頁原先屬性信息、斷點-分頁對照信息加入對應鏈表之後,最後需要將斷點信息添加到斷點鏈表中。
 
關鍵代碼如下:
 
代碼:
//根據用戶輸入創建一個臨時內存斷點
代碼:
 stuPointInfo tempPointInfo;
 stuPointInfo* pResultPointInfo = NULL;
 memset(&tempPointInfo, 0, sizeof(stuPointInfo));
 tempPointInfo.lpPointAddr = lpAddr;
 tempPointInfo.ptType = MEM_POINT;
 tempPointInfo.isOnlyOne = FALSE;
 
 if (stricmp("access", pCmd->chParam2) == 0)
 {
     tempPointInfo.ptAccess = ACCESS;
 } 
 else if (stricmp("write", pCmd->chParam2) == 0)
 {
     tempPointInfo.ptAccess = WRITE;
 }
 else
 {
     printf("Void access!\r\n");
     return FALSE;
 }
 
 int nLen = (int)HexStringToHex(pCmd->chParam3, TRUE);
 
 if (nLen == 0 )
 {
     printf("Point length can not set Zero!\r\n");
     return FALSE;
 }
 
 tempPointInfo.dwPointLen = nLen;
 tempPointInfo.nPtNum = m_nOrdPtFlag;
 m_nOrdPtFlag++;
 
 //查找該內存斷點在斷點鏈表中是否已經存在
 if (FindPointInList(tempPointInfo, &pResultPointInfo, FALSE))
 {
     if (pResultPointInfo->dwPointLen >= nLen)//存在同樣類型且長度大於要設置斷點的斷點
     {
         printf("The Memory breakpoint is already exist!\r\n");
         return FALSE;
     } 
     else//查找到的斷點長度小於要設置的斷點長度,則刪除掉找到的斷點,重新設置
         //此時只需要刪除斷點-分頁表項  斷點表項
     {
         DeletePointInList(pResultPointInfo->nPtNum, FALSE);
     }
 }
 
 // 根據 tempPointInfo 設置內存斷點
 // 添加斷點鏈表項,添加內存斷點-分頁表中記錄,添加分頁信息表記錄
 // 首先根據 tempPointInfo 中的地址和長度獲得所跨越的全部分頁
 
 LPVOID lpAddress = (LPVOID)((int)tempPointInfo.lpPointAddr & 0xfffff000);
 DWORD OutAddr = (DWORD)tempPointInfo.lpPointAddr + 
         tempPointInfo.dwPointLen;
 
 MEMORY_BASIC_INFORMATION mbi = {0};
 
 while ( TRUE )
 {
     if ( sizeof(mbi) != VirtualQueryEx(m_hProcess, lpAddress, &mbi, sizeof(mbi)) )
     {
         break;
     }
 
     if ((DWORD)mbi.BaseAddress >= OutAddr)
     {
         break;            
     }
 
     if ( mbi.State == MEM_COMMIT )
     {
         //將內存分頁信息添加到分頁表中
         AddRecordInPageList(mbi.BaseAddress, 
                             mbi.RegionSize, 
                             mbi.AllocationProtect);
         //將斷點-分頁信息添加到斷點-分頁表中
         DWORD dwPageAddr = (DWORD)mbi.BaseAddress;
         while (dwPageAddr < OutAddr)
         {
             stuPointPage *pPointPage = new stuPointPage;
             pPointPage->dwPageAddr = dwPageAddr;
             pPointPage->nPtNum = tempPointInfo.nPtNum;
             g_PointPageList.push_back(pPointPage);
             //設置該內存頁爲不可訪問
             DWORD dwTempProtect;
             VirtualProtectEx(m_hProcess, (LPVOID)dwPageAddr,
                 1, PAGE_NOACCESS, &dwTempProtect);
 
             dwPageAddr += 0x1000;
         }
 
     }
     lpAddress = (LPVOID)((DWORD)mbi.BaseAddress + mbi.RegionSize);
     if ((DWORD)lpAddress >= OutAddr)
     {
         break;
     }
 }
 
 //斷點添加到斷點信息表中
 stuPointInfo *pPoint = new stuPointInfo;
 memcpy(pPoint, &tempPointInfo, sizeof(stuPointInfo));
 g_ptList.push_back(pPoint);
printf("***Set Memory breakpoint success!***\r\n");
 
內存斷點精確命中的判斷思路:
 
    根據產生訪問異常時,異常的類型是訪問還是寫入,以及異常訪問的地址這兩個信息到“斷點-分頁對照表”中去查找。如果沒有找到,則說明此異常不是用戶調試所下的內存斷點,調試器不予處理。
 
    如果找到,再根據斷點序號,到“斷點信息表”中查看斷點的詳細信息。看斷點是否準確命中(下斷的內存區域,斷點的類型:如果是讀異常則只命中訪問類型斷點;如果是寫異常,則訪問類型、寫入類型斷點都算命中)。
 
    如果遍歷完“斷點-分頁對照表”,異常訪問地址只是在“斷點-分頁對照表”中找到,但沒有精確命中內存斷點,則暫時恢復內存頁的原屬性,並設置單步,進入單步後再恢復該內存頁爲不可訪問。
 
    如果在“斷點-分頁表”中找到,且精確命中某個斷點,則先暫時恢復頁屬性,設置單步,並等待用戶輸入。程序運行進入單步後,再設置內存頁屬性爲不可訪問。
 
內存斷點的處理:
 
    當被調試程序觸發訪問異常時,異常事件被調試器接收到,分析此時的異常結構體如下:
 
struct _EXCEPTION_RECORD { 
DWORD ExceptionCode
DWORD ExceptionFlags
struct _EXCEPTION_RECORD *ExceptionRecord
PVOID ExceptionAddress
DWORD NumberParameters
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; 
}
 
    我們考察其中最後一個成員ExceptionInformation數組的數據,MSDN上的說明如下:
The first element of the array contains a read-write flag that indicates the type of operation that caused the access violation. If this value is zero, the thread attempted to read the inaccessible data. If this value is 1, the thread attempted to write to an inaccessible address. 
The second array element specifies the virtual address of the inaccessible data.
 
    即:數組的第一個元素ExceptionInformation[0]包含了表示引發訪問違規操作類型的讀寫標誌。如果該標誌爲0,表示線程試圖讀一個不可訪問地址處的數據;如果該標誌是1,表示線程試圖寫數據到一個不可訪問的地址。數組的第二個元素ExceptionInformation[1]指定了不可訪問的地址。
 
    根據這兩個信息,我們就可以利用上面提到的內存斷點精確命中的判斷思路,來判斷是否命中用戶所下的內存斷點,以及做出對應的處理。
 
整個模塊如下:
代碼:
//處理訪問異常部分
代碼:
BOOL CDoException::DoAccessException()
{
BOOL bRet;
DWORD dwAccessAddr; //讀寫地址
DWORD dwAccessFlag; //讀寫標誌
BOOL isExceptionFromMemPoint = FALSE; //異常是否由內存斷點設置引起,默認爲否
stuPointInfo* pPointInfo = NULL; //命中的斷點
BOOL isHitMemPoint = FALSE; //是否精確命中斷點
 
dwAccessFlag = m_DbgInfo.ExceptionRecord.ExceptionInformation[0];
dwAccessAddr = m_DbgInfo.ExceptionRecord.ExceptionInformation[1];
//根據 訪問地址 到“斷點-分頁表”中去查找
//同一個內存分頁可能有多個斷點
//如果沒有在“斷點-分頁表”中查找到,則說明這個異常不是斷點引起的
list<stuPointPage*>::iterator it = g_PointPageList.begin();
int nSize = g_PointPageList.size();
 
//遍歷鏈表中每個節點,將每個匹配的“斷點-分頁記錄”都添加到g_ResetMemBp鏈表(需要重設的斷點的內存分頁信息鏈表)中
for ( int i = 0; i < nSize; i++ )
{
stuPointPage* pPointPage = *it;
//如果在“斷點-分頁表”中查找到
//再根據斷點表中信息判斷是否符合用戶所下斷點信息
if (pPointPage->dwPageAddr == (dwAccessAddr & 0xfffff000))
{
stuResetMemBp *p = new stuResetMemBp;
p->dwAddr = pPointPage->dwPageAddr;
p->nID = pPointPage->nPtNum; 
g_ResetMemBp.push_back(p);
 
//暫時恢復內存頁原來的屬性
BOOL bDoOnce = FALSE;
if (!bDoOnce)
{
//這些操作只需要執行一次
bDoOnce = TRUE;
isExceptionFromMemPoint = TRUE;
//暫時恢復內存頁原來屬性的函數
TempResumePageProp(pPointPage->dwPageAddr);
//設置單步,在單步中將斷點設回
UpdateContextFromThread();
m_Context.EFlags |= TF;
UpdateContextToThread();
}
 
//先找到斷點序號對應的斷點
list<stuPointInfo*>::iterator it2 = g_ptList.begin();
for ( int j = 0; j < g_ptList.size(); j++ )
{
pPointInfo = *it2;
if (pPointInfo->nPtNum == pPointPage->nPtNum)
{
break;
}
it2++;
}
 
//再判斷是否符合用戶所下斷點信息(斷點類型和斷點範圍是否均相符)
if (isHitMemPoint == FALSE)
{
if (dwAccessAddr >= (DWORD)pPointInfo->lpPointAddr && 
dwAccessAddr < (DWORD)pPointInfo->lpPointAddr +
pPointInfo->dwPointLen)
{
if ( pPointInfo->ptAccess == ACCESS || 
(pPointInfo->ptAccess == WRITE && dwAccessFlag == 1) )
{
isHitMemPoint = TRUE;
}
}
}
}
it++;
}
 
//如果異常不是由內存斷點設置引起,則調試器不處理
if (isExceptionFromMemPoint == FALSE)
{
return FALSE;
}
 
//如果命中內存斷點,則暫停,顯示相關信息並等待用戶輸入
if (isHitMemPoint)
{
ShowBreakPointInfo(pPointInfo);
//顯示反彙編代碼
m_lpDisAsmAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
ShowAsmCode();
//顯示寄存器值
ShowRegValue(NULL);
 
//等待用戶輸入
bRet = FALSE;
while (bRet == FALSE)
{
bRet = WaitForUserInput();
}
}
return TRUE;
}
 
內存斷點需要注意的細節:
 
1. 由於內存斷點將頁面屬性改爲不可訪問了,所有很多命令(如反彙編、查看數據)都需要進行修改。
 
2. 內存斷點可能出現多個內存斷點下在同一個分頁的情況。所以在刪除一個內存斷點時,如果該斷點對應的某個(或某幾個)分頁也有其他的斷點,則不能將該內存分頁設置回原屬性。
 
本系列文章參考書目、資料如下:
 
1.《加密與解密3》 編著:段鋼
2.《調試寄存器(DRx)理論與實踐》 作者:Hume/冷雨飄心
3.《數據結構》 作者:嚴蔚敏

  • 標 題:答覆
  • 作 者:超然
  • 時 間:2010-05-05 19:51:44

第五章 單步異常的處理
 
    因爲在調試器的設計與實現中,很多關鍵性的操作都是在單步異常處理中完成的,故本章重點論述在單步異常中的處理。首先我們來看看會有哪些情況導致調試器進入單步異常。
 
進入單步異常的原因:
 

1. 用戶輸入了單步進入的命令,調試器需要設置單步,讓被調試程序單步執行。

2. 用戶所下的INT3斷點被斷下後,調試器會暫時恢復INT3斷點處的字節爲原有的字節,並讓被調試線程的EIP減一,爲了重新設置這個INT3斷點,調試器自己設置了單步。

3. 用戶所下的硬件斷點被斷下時,會觸發單步異常。

4. 用戶所下的硬件執行斷點被斷下後,調試器會暫時取消掉該硬件執行斷點,以便被調試進程順利跑下去,爲了重新設置該硬件執行斷點,調試器自己設置了單步。

5. 用戶所下的內存斷點被斷下後,調試器會暫時恢復產生訪問異常的內存分頁爲原來的屬性,以便被調試進程順利跑下去,爲了重新設置該內存分頁的屬性(以便內存斷點繼續起作用),調試器自己設置了單步。
 
單步異常的處理:
 

    從以上所述各點來看,進入單步的原因有三種,一是用戶需要單步步入運行程序;二是調試器需要重新設置之前被臨時取消的斷點而設置了單步;三是硬件斷點被斷下時觸發的單步。當然也有可能幾種原因同時存在。所以我們需要幾個BOOL變量來表明是否有需要重設的斷點。INT3斷點對應一個BOOL變量,硬件執行斷點對應一個BOOL變量,是否是用戶輸入的單步步入命令對應一個BOOL變量。另外,進入單步後還需要檢查線程環境中的Dr6的低4位是否有值爲1的位,如果有那麼進入單步的原因之一是因爲觸發了硬件斷點,此時需要進一步判斷該硬件斷點是否是硬件執行斷點,如果是硬件執行斷點需要做相應的處理(具體處理方法見《調試器實現(第三章)硬件斷點》)。
 
多斷點重合的情況:
 

    當用戶對代碼段的同一個地址(指令首字節)即設置了硬件執行斷點,又設置了INT3斷點,同時還設置了內存訪問斷點,此時會先觸發硬件執行斷點,然後會觸發內存訪問斷點,最後會觸發INT3斷點。如果用戶不想在同一個地址被多個斷點斷下多次,可以在相應的異常中做判斷,先臨時取消掉同一地址處的其他類型的斷點,然後設置一個單步,進入單步後再把前面取消的斷點再重新設置上。
 

處理單步異常的模塊代碼:
 
代碼:
//處理單步異常
BOOL CDoException::DoStepException()
{
   BOOL                    bRet;
   DWORD                  dwDr6 = 0;                //硬件調試寄存器Dr6的值
   DWORD                 dwDr6Low = 0;    //硬件調試寄存器Dr64位的值
   stuPointInfo               tempPointInfo;
   stuPointInfo*              pResultPointInfo = NULL;
   char                     CodeBuf[24] = {0};
 
   UpdateContextFromThread();
 
   //是否需要重設INT3斷點
   if (m_isNeedResetPoint == TRUE)
   {
       m_isNeedResetPoint = FALSE;
       char chCC = (char)0xcc;
       bRet = WriteProcessMemory(m_hProcess, m_pFindPoint->lpPointAddr, 
                                 &chCC, 1, NULL);
       if (bRet == FALSE)
       {
           printf("WriteProcessMemory error!\r\n");
           return FALSE;
       }
   }
 
   //是否需要重設硬件斷點
   if (m_isNeedResetHardPoint == TRUE)
   {
       m_Context.Dr7 |= (int)pow(4, m_nNeedResetHardPoint);
       UpdateContextToThread();
       m_isNeedResetHardPoint = FALSE;
   }
 
   dwDr6 = m_Context.Dr6;
   dwDr6Low = dwDr6 & 0xf; //取低4
 
   //如果是由硬件斷點觸發的單步,需要用戶輸入才能繼續
   //另外,如果是硬件執行斷點,則需要先暫時取消斷點,設置單步,下次再恢復斷點
   if (dwDr6Low != 0)
   {
       ShowHardwareBreakpoint(dwDr6Low);
       m_nNeedResetHardPoint = log(dwDr6Low)/log(2)+0.5;//0.5是爲了四捨五入
       //判斷由 dwDr6Low 指定的DRX寄存器,是否是執行斷點
       if((m_Context.Dr7 << (14 - (m_nNeedResetHardPoint * 2))) >> 30 == 0)
       {
           switch (m_nNeedResetHardPoint)
           {
           case 0:
               m_Context.Dr7 &= 0xfffffffe;
               break;
           case 1:
               m_Context.Dr7 &= 0xfffffffb;
               break;
           case 2:
               m_Context.Dr7 &= 0xffffffef;
               break;
           case 3:
               m_Context.Dr7 &= 0xffffffbf;
               break;
           default:
               printf("Error!\r\n");
           }
           m_Context.EFlags |= TF;
           UpdateContextToThread();
           m_isNeedResetHardPoint = TRUE;
       }
 
       m_isUserInputStep = TRUE; //這個設置只是爲了能夠等待用戶輸入
   }
 
   if (m_isUserInputStep == FALSE)
   {
       //重設內存斷點
       ResetMemBp();
       return TRUE;
   }
 
   //以下代碼在用戶輸入爲 "T" 命令、或硬件斷點觸發時執行
   //如果此處有INT3斷點,則需要先暫時刪除INT3斷點
   //這樣做是爲了在用戶輸入“T”命令、或硬件斷點觸發時忽略掉INT3斷點
   //以免在一個地方停下兩次
   memset(&tempPointInfo, 0, sizeof(stuPointInfo));
   tempPointInfo.lpPointAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
   tempPointInfo.ptType = ORD_POINT;
 
   if (FindPointInList(tempPointInfo, &pResultPointInfo, TRUE))
   {
       //非一次性斷點,才需要重設斷點
       if (pResultPointInfo->isOnlyOne == FALSE)
       {
           m_Context.EFlags |= TF;
           UpdateContextToThread();
           m_isNeedResetPoint = TRUE;
       }
       else//一次性斷點,從鏈表裏面刪除
       {
           delete[] m_pFindPoint;
           g_ptList.erase(m_itFind);
       }
       WriteProcessMemory(m_hProcess, m_pFindPoint->lpPointAddr, 
           &(m_pFindPoint->u.chOldByte), 1, NULL);
       if (bRet == FALSE)
       {
           printf("WriteProcessMemory error!\r\n");
           return FALSE;
       }
}
 
   m_lpDisAsmAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
   m_isUserInputStep = FALSE;
 
   //更新m_Context爲現在的環境值
   UpdateContextFromThread();
 
   //顯示彙編代碼和寄存器信息
   ShowAsmCode();
   ShowRegValue(NULL);
 
   //重設內存斷點
   ResetMemBp();
 
//等待用戶輸入
   bRet = FALSE;
   while (bRet == FALSE)
   {
       bRet = WaitForUserInput();
   }
   return TRUE;
}
 
本系列文章參考書目、資料如下:

1.《加密與解密3》 編著:段鋼
2.《調試寄存器(DRx)理論與實踐》 作者:Hume/冷雨飄心
3.《數據結構》 作者:嚴蔚敏

  • 標 題:調試器實現(第六章) 功能擴展
  • 作 者:超然
  • 時 間:2010-05-07 21:15:09

調試器實現(第六章) 功能擴展
 
     前面幾章基本上已經將調試器的基本功能及其實現過程講述的差不多了。本章作爲一個結束,將補充一些前面沒有提到的細節性問題,並就調試器的功能擴展做一些探討。
 
單步步過的實現:
 
     單步步過對於非CALL的指令,其實和單步步入一樣,遇到CALL指令的時候我的處理方式是在CALL之後的指令首地址設置一個一次性的INT3斷點,這一點和OllyDbg略有差異。OllyDbg的做法是看當前的4個硬件調試寄存器中是否有空閒可用的,如果有就設置一個一次性的硬件執行斷點,斷點地址爲CALL指令後的下一條指令首地址,如果沒有可用的硬件調試寄存器,才使用下一次性INT3斷點的方式。因爲下硬件斷點比INT3斷點效率高,所以OllyDbg優先使用硬件斷點。
 
顯示系統API、DLL導出函數的實現:
 
     大家在使用OD的時候,其API提示功能用的都很爽吧。顯示DLL導出函數的方法,可以是先遍歷所有DLL的導出函數,將函數名稱和函數地址放入一個鏈表中,反彙編過程中遇到地址值或寄存器值直接查鏈表匹配API;或者反過來,反彙編過程中遇到地址值或寄存器值到對應DLL的導出表中去查是否有匹配的函數地址。由於代碼比較長,且都是不停地查導出表的過程,我就不貼完整的代碼了。我把我寫的含有寄存器的表達式轉化爲對應數值的函數貼出來,讓大家幫我看看是否還有更好的處理方式,我總覺得我的處理方法比較冗長,應該還有更好的處理方法。
 
// 有寄存器參與的CALL指令,將寄存器表達式轉化爲數值
// 參數 pAddr 可能爲以下情況的字符串:
// eax
// eax+3
// eax*4
// eax*4+ebx
// eax*8+1000
// eax+ebx+3000
// ebx+eax*2+F10000
int CDoException::ExpressionToInt(char *pAddr)
{
char chNewBuf[30] = {0};
int nRetValue = 0;
 
//先找有沒有 * 號
BOOL isFindMultiplicationSign = FALSE; //是否找到乘號
BOOL isFindPlusSign = FALSE; //是否找到加號
int nLen = strlen(pAddr);
 
int nMultiplicationPos; //找到的乘號位置下標
for ( nMultiplicationPos = 0; nMultiplicationPos < nLen; nMultiplicationPos++)
{
if (pAddr[nMultiplicationPos] == '*')
{
isFindMultiplicationSign = TRUE;
break;
}
}
 
if (isFindMultiplicationSign == TRUE)
{
//從乘號向前找,直到遇到加號或找到頭
int nTemp = nMultiplicationPos;
while (nTemp > 0 && pAddr[nTemp] != '+')
{
nTemp--;
}
//獲得乘法的操作數1,必定是一個寄存器
char chOpNum1[5] = {0};
if (nTemp != 0)
{
memcpy(chOpNum1, &pAddr[nTemp+1], nMultiplicationPos - nTemp -1);

else
{
memcpy(chOpNum1, &pAddr[0], nMultiplicationPos);
}
int nOpNum1 = RegStringToInt(chOpNum1);
 
//從乘號向後找
//獲得乘法的操作數2,必定是比例因子2,4,8
if (pAddr[nMultiplicationPos+1] == '2')
{
nRetValue += nOpNum1*2;

else if(pAddr[nMultiplicationPos+1] == '4')
{
nRetValue += nOpNum1*4;

else if(pAddr[nMultiplicationPos+1] == '8')
{
nRetValue += nOpNum1*8;
}
else
{
printf("invalid scale!\r\n");
return 0;
}
 
//對 pAddr 字符串進行重組
if (nTemp != 0)
{
memcpy(&pAddr[nTemp], &pAddr[nMultiplicationPos+2], 20);

else
{
memcpy(&pAddr[0], &pAddr[nMultiplicationPos+2], 20);
}
nLen = strlen(pAddr);
}
 
//乘法處理完後,表達式中將只有“+”號,或沒有符號,或是空字符串
if (nLen == 0)
{
return nRetValue;
}
 
//找加號
int nPlusPos; //從前往後找到的加號位置下標
for ( nPlusPos = 0; nPlusPos < nLen; nPlusPos++)
{
if (pAddr[nPlusPos] == '+')
{
isFindPlusSign = TRUE;
break;
}
}
 
if (isFindPlusSign == TRUE)
{
//加法之前必定是一個寄存器
char chPlusOpNum1[5] = {0};
memcpy(chPlusOpNum1, &pAddr[0], 3);
int nPlusOp1 = RegStringToInt(chPlusOpNum1);
 
//加法之後可能是一個寄存器或立即數,判斷一下是否是Eax等寄存器
if (pAddr[nPlusPos+3] == 'x' || pAddr[nPlusPos+3] == 'X' ||
pAddr[nPlusPos+3] == 'i' || pAddr[nPlusPos+3] == 'I' ||
pAddr[nPlusPos+3] == 'p' || pAddr[nPlusPos+3] == 'P')
{
//是寄存器
char chPlusOpNum2[5] = {0};
memcpy(chPlusOpNum2, &pAddr[nPlusPos+1], 3);
int nPlusOp2 = RegStringToInt(chPlusOpNum2);
nRetValue += nPlusOp1 + nPlusOp2;
//對 pAddr 字符串進行重組
if (nLen == 7)
{
return nRetValue;

else
{
memcpy(&pAddr[0], &pAddr[8], 20);
nLen = strlen(pAddr);
}

else
{
//是立即數,說明是最後一個操作數
int nPlusOp2 = (int)HexStringToHex(&pAddr[nPlusPos+1], FALSE);
nRetValue += nPlusOp1 + nPlusOp2;
return nRetValue;
}
}
 
int nLast = (int)HexStringToHex(pAddr, FALSE);
if (nLast == 0)
{
nLast = RegStringToInt(pAddr);
}
nRetValue += nLast;
 
return nRetValue;
}
 
腳本功能:
 
     腳本功能其主要目的是能夠將用戶的操作命令保存成文本,同時也可以從文本中逐行導入命令並執行命令。避免用戶的重複操作。其實現也比較簡單,就是將用戶輸入的所有合法命令添加到一個鏈表中,在用戶調試完一個程序後可以將命令鏈表中的命令導出到文本文件中。導入功能與之相反,當使用導入功能的時候,從腳本文件中逐行讀取命令文本,通過查全局的“命令-函數對照表”,調用相應的函數。“命令-函數對照表”爲如下所示的結構體:
//全局命令-函數對照表
stuCmdNode g_aryCmd[] = {
ADD_COMMAND("T", CDoException::StepInto)
ADD_COMMAND("P", CDoException::StepOver)
ADD_COMMAND("G", CDoException::Run)
ADD_COMMAND("U", CDoException::ShowMulAsmCode)
ADD_COMMAND("D", CDoException::ShowData)
ADD_COMMAND("R", CDoException::ShowRegValue)
ADD_COMMAND("BP", CDoException::SetOrdPoint)
ADD_COMMAND("BPL", CDoException::ListOrdPoint)
ADD_COMMAND("BPC", CDoException::ClearOrdPoint)
ADD_COMMAND("BH", CDoException::SetHardPoint)
ADD_COMMAND("BHL", CDoException::ListHardPoint)
ADD_COMMAND("BHC", CDoException::ClearHardPoint)
ADD_COMMAND("BM", CDoException::SetMemPoint)
ADD_COMMAND("BML", CDoException::ListMemPoint)
ADD_COMMAND("BMC", CDoException::ClearMemPoint)
ADD_COMMAND("LS", CDoException::LoadScript)
ADD_COMMAND("ES", CDoException::ExportScript)
ADD_COMMAND("SR", CDoException::StepRecord)
ADD_COMMAND("H", CDoException::ShowHelp)
{"", NULL} //最後一個空項
};
 
其中的ADD_COMMAND爲一個宏定義:
 
#define ADD_COMMAND(str, memberFxn) {str, memberFxn},
 
簡單地說,也就是一個字符串對應一個函數指針,通過命令字符串查對應的函數指針,調用函數。
 
單步記錄指令功能:
 
     單步記錄指令就是讓程序以單步(步入或步過)的方式運行,將指令地址EIP、對應的二進制指令和一些其他的信息放到一個平衡二叉樹(以下簡稱AVL樹)上,單步運行的過程中,不斷地比較當前指令的EIP和二進制指令是否已經存在於AVL樹上,如果不存在則在AVL樹上添加這個新的指令結點。這裏之所有要用AVL樹是出於對檢查重複時效率的要求。當然AVL樹記錄指令非常佔用堆空間,如果堆空間消耗嚴重,可以將AVL樹上的一部分內容放到文件中去。記錄指令的意義在於讓程序走不同的流程,然後可以比較不同流程的差異。另外也可以使用記錄指令的方式跳過無意義的跳轉,只讓程序記錄有意義的指令。
 
     記錄指令的過程中,我的做法是遇到CALL一個DLL的導出函數時,就採用單步步過的方式,否則就採用單步步入的方式。實際運用中,對於控制檯程序記錄很有效,但是對於基於消息的窗口程序,由於窗口回調函數是系統API在調用的,所以需要先在回調函數中設置斷點,然後再記錄指令才能記錄到消息函數的代碼。

發佈了33 篇原創文章 · 獲贊 10 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章