調試寄存器 原理與使用:DR0-DR7
下面介紹的知識性信息來自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,這個異常不是我的,找別人處理去
-
}
