天天看點

了解程式記憶體

記憶體對很多人來說感覺是個很熟悉的東西,因為我們在用VC調試程式時,很多時候都會察看記憶體中變量的值。但是,很多時候我們的思維也就是以局限在有源碼的子產品,當遇到一些跨子產品或是沒有源代碼的Bug,我們還是無從下手。是以,很有必要我們要對整個程式記憶體有個比較全局性的認識,這樣遇到任何問題,你都能從容面對。

我這裡以32位的程式為例,我們知道32位程式總共有4G記憶體空間,其中低2G是使用者位址空間,高2G是核心位址空間,下面我們借助WinDbg工具來分析低2G使用者位址空間的記憶體分布。

因為所有程式的記憶體分布都大同小異,我這裡用WinDbg分析任務管理器程序的記憶體分布。打開任務管理器,然後用WinDbg Attach到taskmgr.exe程序。

輸入 !address -summary 察看記憶體的使用情況, 結果如下:

了解程式記憶體

從上圖可以看到,程式記憶體根據使用情況大緻分為:

Free - 沒有被使用的

Image - 加載到記憶體的子產品(dll,exe等)

MappedFile - 記憶體映射檔案

unclassified - 實際上應該是堆(heap)

Stack - 堆棧

TEB - 線程環境塊(thread environment block)

PEB - 程序環境塊(process environment block)

記憶體根據使用類型又可以分為:

MEM_IMAGE  - 加載到記憶體的子產品(dll, exe等)

MEM_MAPPED - 記憶體映射

MEM_PRIVATE - 私有(stack, heap, teb, peb等)

記憶體根據使用狀态又可分為:

MEM_FREE - 空閑

MEM_COMMIT - 已經送出

MEM_RESERVE - 保留

根據頁面屬性又可分為隻讀,可讀寫,可執行,寫時拷貝等。

實際上我們可以通過!address指令來檢視更詳細的記憶體使用情況:

了解程式記憶體

可以看到上面列出了所有2G使用者空間的頁面使用情況(截圖隻是開始的一部分),我們可以根據某個位址來分析該位址屬于那塊記憶體區域。當然也可以通過指令來分析某個位址所屬的記憶體區域, 比如輸入!address 7c554來分析位址7c554的情況,會顯示:

了解程式記憶體

上面告訴我們7c554是某個堆棧(Stack)空間的位址.

對我們程式來說最常接觸的記憶體應該是: Module, Heap, Stack,接下來依次分析.

(1)Module

Module在上面被叫住Image,實際上就是被加載到記憶體的Exe和DLL檔案, 我們可以通過lm指令來檢視所有的子產品分布情況:

了解程式記憶體

上面可以看到每個子產品的記憶體起始位址,那麼各個子產品具體内部又是如何分布,它和磁盤上的DLL(exe)檔案又是什麼關系呢?

實際上記憶體的中DLL和磁盤上的DLL檔案非常相似,系統在加載時隻是根據頁面大小(一般4K)作了一些對齊,另外有些資料節如果運作時用不到(比如dll的重定位節)就不會被加載.

我們在!address檢視記憶體空間時,可以看到taskmgr.exe子產品的記憶體分布如下:

了解程式記憶體

上面可以看到taskmgr.exe子產品在記憶體中分為4塊,第一塊是隻讀的, 實際上是PE檔案頭;第二塊是可執行的,實際上就是代碼節(.text);第三塊是可讀寫的,實際上資料節(.Data); 最後一塊也是隻讀的,實際上資源節(.rsrc)。

要詳細的了解taskmgr.exe子產品的檔案頭屬性,可以通過!dh [module address]來檢視, 輸入!dh 1000000,檢視結果:

了解程式記憶體
了解程式記憶體

上面的運作結果可以驗證我們關于taskmgr.exe子產品内部分布的猜想.

(2)Heap

Heap實際上就是堆,我們所有new(malloc)出來的記憶體就是分布在堆裡,每個程式會有若幹個堆,有些是系統建立的,也有的是C/C++運作庫建立的,當然我們自己也可以建立私有堆.我們可以通過!heap指令來檢視堆的使用情況.

了解程式記憶體

可以看到taskmgr.exe一共有9個堆。

!heap指令非常強大,通過開啟頁堆功能,可以很友善的讓我們跟蹤所有堆記憶體的配置設定和使用情況,以後有機會再細說heap相關的.

(3)Stack

Stack即我們通常所說的棧,我們的局部變量就是配置設定在棧上面。說到棧就要說到線程,我們的代碼都是通過線程跑起來的,每個線程包含2塊東西,一塊是線程核心對象,還有一塊就是堆棧,線程運作過程也是堆棧不斷壓棧和出棧的過程。

我們可以通!address -f:stack 來檢視堆棧的分布情況:

了解程式記憶體

從上圖我們可以看到taskmgr.exe一共有4個線程, 對應着4個堆棧, 同時也可以看到每個堆棧記憶體的起始位址。

如果有興趣,我們也可以看下每個線程的堆棧情況, 輸入~* kp

了解程式記憶體

可以看到相應的4個線程堆棧,最後一個線程(debugBreakPoint)看起來有些奇怪,實際上它是調試器為調試而插入的,不是真正的屬于taskmgr.exe, 是以任務管理器實際上一共應該有3個線程.

通過上面的介紹,相信大家對程式記憶體有了比較全局的了解,以後大家分析問題,遇到一個位址,首先要判斷這個位址分布在哪裡:

如果是Image上,那麼是在哪個子產品中,這個位址是屬于該子產品的代碼段(.text)還是資料段(.data),如果是代碼段,又是屬于哪個函數?

如果是Heap上,那麼究竟是在哪個堆裡面,是我們new出來的嗎,是在什麼時候new的(new時堆棧狀況)?

如果是在Stack上,那麼究竟是屬于哪個線程的堆棧,當時線程的堆棧是怎麼樣?

總之,程式在記憶體中運作,隻有你真正了解了記憶體,你才能真正懂計算機。

繼續閱讀