[Win32]一个调试器的实现(四)读取寄存器和内存
[Win32]一個(gè)調(diào)試器的實(shí)現(xiàn)(四)讀取寄存器和內(nèi)存
作者:Zplutor?
出處:http://www.cnblogs.com/zplutor/?
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載。但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。
在前幾篇文章中,我實(shí)現(xiàn)的那個(gè)調(diào)試器只能被動(dòng)接收調(diào)試事件并輸出這些事件的信息。現(xiàn)在,我要將它修改成可以接收命令,并根據(jù)命令對(duì)被調(diào)試進(jìn)程進(jìn)行各種操作。首先從最基本的操作開(kāi)始。
?
獲取寄存器的值
每個(gè)線程都有一個(gè)上下文環(huán)境,它包含了有關(guān)線程的大部分信息,例如線程棧的地址,線程當(dāng)前正在執(zhí)行的指令地址等。上下文環(huán)境保存在寄存器中,系統(tǒng)進(jìn)行線程調(diào)度的時(shí)候會(huì)發(fā)生上下文切換,實(shí)際上就是將一個(gè)線程的上下文環(huán)境保存到內(nèi)存中,然后將另一個(gè)線程的上下文環(huán)境裝入寄存器。
?
獲取某個(gè)線程的上下文環(huán)境需要使用GetThreadContext函數(shù),該函數(shù)聲明如下:
1?BOOL?WINAPI?GetThreadContext(2?????HANDLE?hThread,
3?????LPCONTEXT?lpContext
4?);
第一個(gè)參數(shù)是線程的句柄,第二個(gè)參數(shù)是指向CONTEXT結(jié)構(gòu)的指針。要注意,調(diào)用該函數(shù)之前需要設(shè)置CONTEXT結(jié)構(gòu)的ContextFlags字段,指明你想要獲取哪部分寄存器的值。該字段的取值如下:
?
| CONTEXT_CONTROL | 獲取EBP,EIP,CS,EFLAGS,ESP和SS寄存器的值。 |
| CONTEXT_INTEGER | 獲取EAX,EBX,ECX,EDX,ESI和EDI寄存器的值。 |
| CONTEXT_SEGMENTS | 獲取DS,ES,FS和GS寄存器的值。 |
| CONTEXT_FLOATING_POINT | 獲取有關(guān)浮點(diǎn)數(shù)寄存器的值。 |
| CONTEXT_DEBUG_REGISTERS | 獲取DR0,DR1,DR2,DR3,DR6,DR7寄存器的值。 |
| CONTEXT_FULL | 等于CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS |
?
調(diào)用GetThreadContext函數(shù)之后,CONTEXT結(jié)構(gòu)相應(yīng)的字段就會(huì)被賦值,此時(shí)就可以輸出各個(gè)寄存器的值了。
?
對(duì)于其它寄存器來(lái)說(shuō),直接輸出它的值就可以了,但是EFLAGS寄存器的輸出比較麻煩,因?yàn)樗拿恳晃淮聿煌暮x,我們需要將這些含義也輸出來(lái)。一般情況下我們只需要了解以下標(biāo)志:
?
| 標(biāo)志 | 位 | 含義 |
| CF | 0 | 進(jìn)位標(biāo)志。無(wú)符號(hào)數(shù)發(fā)生溢出時(shí),該標(biāo)志為1,否則為0。 |
| PF | 2 | 奇偶標(biāo)志。運(yùn)算結(jié)果的最低字節(jié)中包含偶數(shù)個(gè)1時(shí),該標(biāo)志為1,否則為0。 |
| AF | 4 | 輔助進(jìn)位標(biāo)志。運(yùn)算結(jié)果的最低字節(jié)的第三位向高位進(jìn)位時(shí),該標(biāo)志為1,否則為0。 |
| ZF | 6 | 0標(biāo)志。運(yùn)算結(jié)果未0時(shí),該標(biāo)志為1,否則為0。 |
| SF | 7 | 符號(hào)標(biāo)志。運(yùn)算結(jié)果未負(fù)數(shù)時(shí),該標(biāo)志為1,否則為0。 |
| DF | 10 | 方向標(biāo)志。該標(biāo)志為1時(shí),字符串指令每次操作后遞減ESI和EDI,為0時(shí)遞增。 |
| OF | 11 | 溢出標(biāo)志。有符號(hào)數(shù)發(fā)生溢出時(shí),該標(biāo)志為1,否則為0。 |
?
?
?
?
用按位與操作就可以得知某個(gè)標(biāo)志是否為1。例如,要檢查OF是否為1:
?
1?if?((context.EFlags?&?0x400)?!=?0)?{2?
3????std::wcout?<<?TEXT("OF?");
4?
5?}
十六進(jìn)制數(shù)0x400只有第11位是1,其余位都是0。對(duì)于其它的標(biāo)志也是用同樣的方法進(jìn)行判斷。
?
讀取內(nèi)存內(nèi)容
我對(duì)Windows自帶的16位調(diào)試器DEBUG的d命令印象很深刻,這個(gè)命令可以以十六進(jìn)制和ASCII編碼顯示進(jìn)程內(nèi)存的內(nèi)容,用于觀察數(shù)據(jù)段的數(shù)據(jù)。現(xiàn)在我要在調(diào)試器中添加類似的功能。
?
讀取進(jìn)程的內(nèi)存使用ReadProcessMemory函數(shù),該函數(shù)聲明如下:
1?BOOL?WINAPI?ReadProcessMemory(2?????HANDLE?hProcess,??????????????????//進(jìn)程句柄
3?????LPCVOID?lpBaseAddress,????????????//要讀取的地址
4?????LPVOID?lpBuffer,??????????????????//一個(gè)緩沖區(qū)的指針,保存讀取到的內(nèi)容
5?????SIZE_T?nSize,?????????????????????//要讀取的字節(jié)數(shù)
6?????SIZE_T*?lpNumberOfBytesRead???????//一個(gè)變量的指針,保存實(shí)際讀取到的字節(jié)數(shù)
7?);
?
要想成功讀取到進(jìn)程的內(nèi)存,需要兩個(gè)條件:一是hProcess句柄具有PROCESS_VM_READ的權(quán)限;二是由lpBaseAddress和nSize指定的內(nèi)存范圍必須位于用戶模式地址空間內(nèi),而且是已分配的。
?
對(duì)于調(diào)試器來(lái)說(shuō),第一個(gè)條件很容易滿足,因?yàn)檎{(diào)試器對(duì)被調(diào)試進(jìn)程具有完整的權(quán)限,可以對(duì)其進(jìn)行任意操作。
?
第二個(gè)條件意味著我們不能讀取進(jìn)程任意地址的內(nèi)存,而是有一個(gè)限制。Windows將進(jìn)程的虛擬地址空間分成了四個(gè)分區(qū),如下表所示:(來(lái)自《Windows核心編程(第5版)》)
?
| 分區(qū) | 地址范圍 |
| 空指針賦值分區(qū) | 0x00000000~0x0000FFFF |
| 用戶模式分區(qū) | 0x00010000~0x7FFEFFFF |
| 64KB禁入分區(qū) | 0x7FFF0000~0x7FFFFFFF |
| 內(nèi)核模式分區(qū) | 0x80000000~0xFFFFFFFF |
?
空指針賦值分區(qū)主要為了幫助程序員檢測(cè)對(duì)空指針的訪問(wèn),任何對(duì)這一分區(qū)的讀取或?qū)懭氩僮鞫紩?huì)引發(fā)異常。64KB禁入分區(qū)正如其名字所言,是禁止訪問(wèn)的,由Windows保留。內(nèi)核模式分區(qū)由Windows的內(nèi)核部分使用,運(yùn)行于用戶態(tài)的進(jìn)程不能訪問(wèn)這一區(qū)域。進(jìn)程只能訪問(wèn)用戶模式分區(qū)的內(nèi)存,對(duì)于其它分區(qū)的訪問(wèn)將會(huì)引發(fā)ACCESS_VIOLATION異常。
?
另外,并不是用戶模式分區(qū)的任意部分都可以訪問(wèn)。我們知道,在32位保護(hù)模式下,進(jìn)程的4GB地址空間是虛擬的,在物理內(nèi)存中不存在。如果要使用某一部分地址空間的話,必須先向操作系統(tǒng)提交申請(qǐng),讓操作系統(tǒng)為這部分地址空間分配物理內(nèi)存。只有經(jīng)過(guò)分配之后的地址空間才是可訪問(wèn)的,試圖訪問(wèn)未分配的地址空間仍然會(huì)引發(fā)ACCESS_VIOLATION異常。
?
下圖是ReadProcessMemory調(diào)用成功的情況,灰色部分是用戶模式地址空間中已分配的部分,虛線部分是由lpBaseAddress和nSize指定的范圍:
?
?
只有虛線部分的長(zhǎng)度小于等于灰色部分的長(zhǎng)度時(shí),ReadProcessMemory才會(huì)成功。
?
以下的幾幅圖是ReadProcessMemory調(diào)用失敗的情況:
?
?
這引出了一個(gè)問(wèn)題:如何處理ReadProcessMemory失敗的情況?每個(gè)人的想法或許都不同,在這里我的處理方式是:對(duì)于導(dǎo)致ReadProcessMemory失敗的字節(jié),以“??”來(lái)表示。如下圖所示:
?
d 3FFFFFD0表示顯示從地址0x3FFFFFD0開(kāi)始的內(nèi)存。由于0x40000000之前的地址空間是未分配的,所以前面的48個(gè)字節(jié)顯示“??”。
?
要注意的是這種處理方式是以字節(jié)為單位的,也就是說(shuō)對(duì)于每個(gè)字節(jié)都要調(diào)用一次ReadProcessMemory。如果要顯示128個(gè)字節(jié),則要調(diào)用ReadProcessMemory128次。你肯定會(huì)認(rèn)為這樣做的話效率會(huì)很低,不過(guò)經(jīng)過(guò)實(shí)際的使用情況來(lái)看,顯示速度是可以接受的,與調(diào)用一次ReadProcessMemory讀取所有內(nèi)存的做法幾乎沒(méi)有差別。
?
具體的做法可以參考示例代碼。如果你有更好的做法,不妨一起分享一下。
?
至于右邊的字符顯示,我只使用了ASCII字符集對(duì)字節(jié)進(jìn)行解碼,而沒(méi)有用ANSI字符集。原因是中文字符使用兩個(gè)字節(jié)表示,如果一個(gè)中文字符的第一個(gè)字節(jié)位于一行的末尾,而第二個(gè)字節(jié)位于下一行的頭部,這種情況該如何處理呢?想不出好的解決方法,因此只好放棄ANSI字符集了。
?
0x00~0x1F和0x81~0xFF之間的ASCII字符不能顯示,我分別以“.”和“?”來(lái)代替。
?
示例代碼
這次MiniDebugger的結(jié)構(gòu)作了很大的改進(jìn),主要為了支持與用戶的交互以及以后功能的添加。要注意,該調(diào)試器只支持單線程程序,如果用它來(lái)調(diào)試多線程程序,會(huì)導(dǎo)致兩者都陷入阻塞狀態(tài)。當(dāng)然,要改成支持多線程也不是很難,你可以嘗試這么做。代碼中都作了注釋,這里就不再贅述了。下面是當(dāng)前支持的命令:
?
s path
啟動(dòng)被調(diào)試進(jìn)程,開(kāi)始進(jìn)行調(diào)試。如果路徑中有空格,則應(yīng)該用雙引號(hào)括住路徑。例如:s C:\windows\notepad.exe。
?
t
結(jié)束被調(diào)試進(jìn)程,停止調(diào)試。
?
g [c]
繼續(xù)被調(diào)試進(jìn)程的執(zhí)行。如果不帶參數(shù)c,則表示未處理異常;帶參數(shù)c則表示已處理了異常。
?
r
顯示被調(diào)試進(jìn)程的寄存器的值。
?
d [address] [length]
顯示被調(diào)試進(jìn)程的內(nèi)存。如果省略了length參數(shù),則顯示128個(gè)字節(jié);如果兩個(gè)參數(shù)都省略,則顯示EIP地址處的128個(gè)字節(jié)。
?
q
結(jié)束調(diào)試并退出調(diào)試器。
?
如果想讓被調(diào)試進(jìn)程運(yùn)行到某處停下來(lái),以便測(cè)試各種命令,可以在代碼中加入__asm int 3;語(yǔ)句,或者拋出一個(gè)異常,讓調(diào)試器捕捉到異常即可。
?
http://files.cnblogs.com/zplutor/MiniDebugger4.rar
轉(zhuǎn)載于:https://www.cnblogs.com/M-Mr/p/3969985.html
總結(jié)
以上是生活随笔為你收集整理的[Win32]一个调试器的实现(四)读取寄存器和内存的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Delphi字符串处理函数
- 下一篇: ubuntu下 apache phpmy