HeapAlloc內部算法

大家都知道在VC中使用new操作符時(如果沒有重載的話),其內部實際上調用了標準的malloc,而根據工程設置中選擇的運行庫的不同(是否多線程,是否調試,是否Dll等)調用了不同的malloc版本,多線程版malloc內部使用了Win32的關鍵區,而調試版會額外多申請一些內存用來保存調試信息,這些直接看VC附帶的crt源碼就可以看到。所有版本的malloc的共同點是最後都調用了Win32的API函數HeapAlloc。

關於HeapAlloc的算法記得很多年前一個後來去了微軟的大牛給我講過,但當時我還是個菜鳥(現在沒那麼菜了)沒記住,於是決定自己研究一下。方法是用HeapCreate自己建一個堆,用HeapAlloc從裏面申請內存,同時用VC的內存查看盯着heap所在內存區看,觀察Alloc和Free的時候內存都發生了哪些變化。其間從網上找到這篇文檔幫助比較大,節省了不少時間 XPSP2 Heap Exploitation.ppt(用google搜第一條就是)

Heap頭的結構基本沒搞明白(也不需要太關心),只知道在偏移地址0x28的位置記錄的是剩餘空間大小(單位是8字節),然後從0x178開始是一個free list 表(數組),一共128個單元,每個單元的數組下標代表這個鏈上每個節點(free chunk)是多大(包括chunk header所佔空間,單位是8字節)。所以記錄的是0~1024(128×8)大小的free chunk鏈,每個單元是一個循環鏈表,Flink和Plink指針共佔用8個字節。第0號單元比較特殊,它維護的是大小>=1k的chunk的list,按chunk大小升序排列。下圖摘自前面提到的文檔。

每個chunk,包括free chunk和已分配出去的chunk,都會帶有一個8字節的header結構,如下圖

chunk header之後就是chunk的用戶數據區,用戶數據區之後緊跟了8個值總是爲0xab的字節,然後會有0~7個不用的字節用於8字節對齊,之後是8個總是爲0的字節。用戶數據區後面的這16~23字節就我觀察到的始終沒有變過,所以我也不知道是做什麼用的。注意這裏提到的size單位都是8字節,另外Previous chunk size不是指在free list中的previous,而是物理內存的前面一塊的大小,這個值是在chunk回收時會用到,因爲可能需要和前一個free chunk粘合,下面會提到。Unused bytes就是這個chunk中除了用戶數據之外的字節數(就是header的8字節加上用戶數據之後的16~23個字節)

如果這個chunk是free chunk(就是還沒有被分配或者已經回收的chunk),在chunk header之後,也就是用戶數據區中的前8個字節用來保存它在free list中的Next指針和Prev指針。之所以使用用戶數據區,是因爲當chunk被分配出去以後就已經不在free list中了,當然就無需再保存這兩個指針。而且,從前面說過的chunk結構來看是一定能容下這兩個指針的。

 

好了,數據結構清楚了,開始說算法。

先說Alloc算法:

根據用戶提交的申請內存字節數,考慮chunk header等結構本身的內存開銷,決定這個chunk需要多大size,把這個size除以8就得到在free list表裏對應的下標(超過127就是0),從這個下標對應的free list開始往下標增大的方向找(超過127回到0,順着0號free list找)也就是在所有free chunk中找到第一個size大等於要求值的chunk,如果找到大小正合適的chunk,那很好,把chunk從list中摘出來,把chunk header的flag字節的busy位置上,然後返回該chunk用戶數據區的首地址,搞定。

如果一個都沒找到就返回空指針(如果HeapAlloc給的option參數要求拋異常則拋異常)。

如果找到的free chunk比要求的size大,先把它從free list中摘出,然後把這個chunk分割成兩部分,前面size那麼大的形成一個chunk,剩下的部分形成一個碎片chunk(因爲都是8字節對齊的,所以剩下的空間至少夠一個chunk header)。前一個chunk當然就是分配給用戶的chunk了,把header中的Self size設置一下,previous chunk size不會變。碎片chunk需要加回free list表,找到它所屬的相應大小的free list。這裏要注意的是碎片的self size和previous chunk size(就是給用戶的chunk 的size)都要設置,並且碎片後面一個chunk(除非後面沒了)的previous chunk size要設成碎片chunk的大小。最後把用戶要求的chunk設置buzy後返回數據區指針。

Free的算法。

Free的算法比較特殊的是內存的粘合,要釋放一個chunk時,看看前後的chunk(根據自己的size和preivious chunk size很容易找到相應的chunk header)是不是free(判busy位)的,如果是則把它(們)和當前chunk粘合,具體粘合算法就不說了,無非是把前後的self size和previous chunk size改改。注意粘合前要先把被粘合的chunk從free list中摘除。我想就是這個粘合導致的free chunk從free list的摘除操作才導致free list需要雙鏈結構(單鏈的話得從鏈頭開始找太慢),如果象一般內存池那樣不需要粘合的話,free list只要單鏈就夠了,因爲總是從鏈頭添加和刪除節點。最後粘合(或沒有粘合)後得到的chunk根據大小被加入free list。

 

其它

一個heap剛被創建時,只有0號下標(也就是> 1K)的free list中有一個free chunk,包含了這個堆裏所有可以被分配的內存。

開始提到的那個文檔中提到一個 lookaside表,大致就是可以對於每種大小的free chunk都緩存幾個在lookaside表中(而不會被粘合)這樣能一定程度上加快速度,但是就我觀察到的似乎沒有用到這個算法(或者和堆的創建參數或者堆的大小有關?沒有仔細研究)

下標爲1,2,3的free list實際上永遠不會被直接分配出去,因爲一個有效(包含用戶數據)的chunk至少會佔用32個字節(size爲4),這幾個list保存的都是等待被粘合的碎片

 

呵呵,研究了兩個晚上,好多細節都沒有深究。如果發現有問題麻煩通知我:)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章