下面介紹的知識性信息來自intel IA-32手冊(可以在intel的開發手冊或者官方網站查到),提示和補充來自學習調試器實現時的總結。
希望能給你帶去有用的信息。
(DRx對應任意的一個調試寄存器。LENn對應任意一個長度。Ln對應任意一個局部置位)
DR0-DR7可以直接被讀寫操作(MOV 指令之類的,DRx可以是源操作數也可以是目的操作數)
但是,DRx的訪問是需要一定權限的。比如你用MOV操作的話,你需要在實地址模式,系統管理模式(smm)或者在保護模式(CPL設0).如果權限不夠,將會在訪問DRx的時候嘗產生#GP(general-protection)異常
現在來看看DRx可以干些什么?
1.設置發生斷點的地址(線性地址)
2.設置斷點的長度(1,2,4個字節,但是執行斷點只能是1)
3.設置在調試異常產生的地址執行的操作
4.設置斷點是否可用
5.在調試異常產生時,調試條件是否是可用
(以上直接翻譯自"Intel 64 and IA-32 Architectures Software Developer’s Manual" volume 3。
以下來自個人的總結。當然,也是參考intel官方資料得來的)
我們來看看調試寄存器的一些細節信息。
下圖很重要,后面的介紹都是針對這個圖說的。
(當然不是我畫的,是來自intel ia-32系統結構開發手冊18章2節)。
調試寄存器 DR0-DR3
這四個寄存器是用來設置 斷點地址的。斷點的比對在物理地址轉換前(異常產生時,還沒有將線性地址轉換成物理地址)。由於只有0-3四個保存地址的寄存器,所以,硬件斷點,在物理上最多只能有4個。
調試寄存器DR4-DR5
這兩個調試寄存器有CR4的DE標記控制。如果DE置位,那么對這兩個寄存器的訪問會導致#UD異常。如果DE置0,那么他們就被化名為DR6-DR7(你一定會問原來的DR6-DR7怎么辦?這個…… 我也不知道。如果你搞明白了,一定記得告訴我)
調試寄存器DR7(控制寄存器)
(先介紹DR7對DR6的理解有好處。)
DR7是調試控制寄存器。控制方式嘛!繼續看:
1. L0-L3(由第0,2,4,6位控制):對應DR0-DR3,設置斷點作用范圍,如果被置位,那么將只對當前任務有效。每次異常后,Lx都被清零。
2. G0-G3(由第1,3,5,7位控制):對應DR0-DR3,如果置位,那么所有的任務都有效。每次異常后不會被清零。以確保對所有任務有效。但是,不知道為什么,我在測試時:
設置Gn后,不能返回調試異常給調試器(如果你知道為什么,記得告訴我)
3. LE,GE(由第8,9位控制):這個在P6以下系列CPU上不被支持,在升級版的系列里面:如果被置位,那么cpu將會追蹤精確的數據斷點。LE是局部的,GE是全局的。(到底什么算精確的,我也不清楚,但是,我知道如果設置了這兩個,cpu的速度會降低。我在測試中,都沒有置位。)
4. GD(由第13位控制):如果置位,追蹤下一條指令是否會訪問調試寄存器。如果是,產生異常。在下面的DR6里面,你會知道他還和另外一個標志位有點關系。
5. R/W0-R/W3:(由第16,17,20,21,24,25,28,29位控制):這個東西的處理有兩種情況。
如果CR4的DE被置位,那么,他們按照下面的規則處理問題:
00:執行斷點
01:數據寫入斷點
10:I/0讀寫斷點
11:讀寫斷點,讀取指令不算
如果DE置0,那么問題會這樣處理:
00:執行斷點
01:數據寫入斷點
10:未定義
11:數據讀寫斷點,讀取指令不算
6. LEN0-LEN3:(由第18.19.22.23.26.27.30位控制):指定內存操作的大小。
00:1字節(執行斷點只能是1字節長)
01:2字節
10:未定義或者是8字節(和cpu的系列有關系)
11:4字節
調試寄存器DR6(調試狀態寄存器)
這個寄存器主要是在調試異常產生后,報告產生調試異常的相關信息
1. B0-B3(DR0-DR3):DRx指定的斷點在滿足DR7指定的條件下,產生異常。那么Bx就置位。但是,有時,即使Ln和Gn置0,也可能產生Bx被置位。這種現象可能這樣出現(提示:在p6系列處理器,REP MOVS在不斷循環中產生的調試異常需要執行完了才能准確返回給調試進程):DR0的L0,G0都置0(DR0就是一個不能產生異常的斷點了),然后在DR0指定的地址是一個REP指令的循環,這樣,DR0就可能在這個循環之后的REP指令產生的調試異常中將B0置位
2. BD:BD需要DR7的GD置位,才有效。BD是在下一條指令要訪問到某一個調試寄存器的時候,被置位的。
3. BS:單步執行模式時,被置位。單步執行是最高權限的調試異常。
4. BT:在任務切換的時候,被置位。但是必須在被切換去的任務的TSS段里面的T標記被置位的情況下才有效。在控制權被切換過去后,在執行指令前,返回調試異常。但是,需要注意,如果調試程序是一個任務,那么T標記的設置肯定就沖突了。然后,導致了死循環(BT的這些信息都是按照官方資料翻譯而來,由於沒有實際的操作,肯定會有理解上的出入。如果要深入的話,建議看官方資料)
有些調試異常會將B0-B3清零。但是其他的DR6的位是不能被產生異常的進程清零的。每次調試異常返回后,調試進程都會先將DR6清零,再按照情況設置。以免產生不必要的錯誤。
對齊問題和64位處理器
對齊問題:
這個問題是來源於LENn的設置,如果設置4字節,那么必須4字節對齊。例如:我們下4字節的斷點,那么DRx需要是A0000/A0004/A0008這樣的地址上。I/O地址是零擴展的(這個……也許意味着必須完全對齊)。因為,intel在比對地址時:用LENn的值去覆蓋DRx里面保存的地址的低位。你可以想到,不對齊會有什么后果了吧。注意:執行斷點只能是1字節。
再用圖片解釋下(當然,圖片來自intel官方資料):
在64位處理器下:
調試寄存器當然也是64位的。在操作過程中,寫入,前面32位被置零。讀取:只返回后32位。MOV DRx操作,前32位被忽略。
DR6-DR7的高32位被保留。置零。如果置位,會產生#GP異常。8字節的讀寫斷點完全被支持。
最后,還是給個圖片(64位處理器的布局):
最后需要提醒一個小問題:數據寫入斷點設置后。是在原數據被修改后,才產生調試異常。所以,返回異常時,原有數據已經被修改。如果想保留原有數據,需要自己提前保存對應地址的數據。
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
VC利用調試寄存器實現硬件斷點,處理斷點異常
/************************************************************************ SetHardWareBP: 設置線程硬件斷點 hThread: 線程句柄 dwAddr: 斷點地址 dwDrIndex: 硬件寄存器(0~3) nType: 斷點類型(0:執行,1:讀取,2:寫入) nLen: 讀寫斷點數據長度(1,2,4) /************************************************************************/ BOOL SetHardWareBP(HANDLE hThread,DWORD dwAddr,DWORD dwDrIndex=0,UINT nType=0,UINT nLen=1) { BOOL bResult=FALSE; CONTEXT context = {0}; context.ContextFlags=CONTEXT_DEBUG_REGISTERS; if(::GetThreadContext(hThread,&context)) { DWORD dwDrFlags=context.Dr7; //將斷點地址復制進入對應Dr寄存器(參考CONTEXT結構) memcpy(((BYTE *)&context)+4+dwDrIndex*4,&dwAddr,4); //決定使用哪個寄存器 dwDrFlags|=(DWORD)0x1<<(2*dwDrIndex); //見OD讀寫斷點時 這個置位了,執行沒有(置位也正常-_-) dwDrFlags|=0x100; //先將對應寄存器對應4個控制位清零(先或,再異或,還有其它好方法嗎) =.= 悲催的小學生 dwDrFlags|=(DWORD)0xF<<(16+4*dwDrIndex); dwDrFlags^=(DWORD)0xF<<(16+4*dwDrIndex); //設置斷點類型,執行:00 讀取:11 寫入:01 //(不知何故,測試時發現不論是11還是01,讀寫數據時均會斷下來) if (nType==1) dwDrFlags|=(DWORD)0x3<<(16+4*dwDrIndex); //讀取 else if(nType==2) dwDrFlags|=(DWORD)0x1<<(16+4*dwDrIndex); //寫入 //else if(nType==0) //dwDrFlags=dwDrFlags //執行 //設置讀寫斷點時數據長度 if (nType!=0) { if(nLen==2 && dwAddr%2==0) dwDrFlags|=(DWORD)0x1<<(18+4*dwDrIndex); //2字節 else if(nLen==4 && dwAddr%4==0) dwDrFlags|=(DWORD)0x3<<(18+4*dwDrIndex); //4字節 } context.Dr7=dwDrFlags; if (::SetThreadContext(hThread,&context)) bResult=TRUE; } return bResult; }
//異常處理 //直接從工程中拷出來的 typedef ULONG (WINAPI *pfnRtlDispatchException)(PEXCEPTION_RECORD pExcptRec,CONTEXT * pContext); static pfnRtlDispatchException m_fnRtlDispatchException=NULL; BOOL RtlDispatchException(PEXCEPTION_RECORD pExcptRec,CONTEXT * pContext); ULONG WINAPI CSysHook::_RtlDispatchException( PEXCEPTION_RECORD pExcptRec,CONTEXT * pContext ) { if(RtlDispatchException(pExcptRec,pContext)) return 1; return m_fnRtlDispatchException(pExcptRec,pContext); } //Hook程序異常處理,當程序發生異常時,由ring0轉回ring3時調用的第一個函數:KiUserExceptionDispatcher BOOL CSysHook::HookSystemSEH() { BOOL bResult=FALSE; BYTE *pAddr=(BYTE *)::GetProcAddress(::GetModuleHandleA("ntdll"),"KiUserExceptionDispatcher"); if (pAddr) { while (*pAddr!=0xE8)pAddr++; //XP~Win7正常,Win8尚無緣得見 m_fnRtlDispatchException=(pfnRtlDispatchException)((*(DWORD *)(pAddr+1))+5+(DWORD)pAddr); //得到原函數地址 DWORD dwNewAddr=(DWORD)_RtlDispatchException-(DWORD)pAddr-5; //計算新地址 CMemory::WriteMemory((DWORD)pAddr+1,(BYTE *)&dwNewAddr,4); //這個寫內存的自己改造吧 bResult=TRUE; } return bResult; } //異常處理函數 BOOL RtlDispatchException(PEXCEPTION_RECORD pExcptRec,CONTEXT * pContext) { 返回TRUE,這個異常我已經處理好了,繼續運行程序 返回FALSE,這個異常不是我的,找別人處理去 }
其實這個文章我也不知道源頭是哪,因為我看到它的時候,它也是轉的,但是他也沒寫源頭,據圖片推測,可能是CSDN哪的。