MS-CRT的malloc以及MS的HeapAlloc–本質基礎上的改進

科技小能手發表於2017-11-12

微軟的CRT實現了malloc,但是閱讀原始碼之後發現竟然是如此簡單,debug版本的還有點意思,release版本的幾乎就是每次呼叫首先將一個資料頭的長度附加於所需長度其上,然後呼叫HeapAlloc,成功後將該頭帶領的結構體一同連結進一個全域性的連結串列,free的時候將該元素從全域性連結串列摘除,然後呼叫HeapFree,這麼簡單也太不像微軟的作風了吧,一點分配策略都沒有體現,僅僅是將HeapXXX做了一個包裝,儼然成為了一系列標準C函式。

其實windows的使用者空間記憶體管理使用的機制都在dll中實現,標準c本身就是一個包裝,很多意義上為了跨平臺,linux的glibc實現了malloc,而windows中對應的實現模組就是kernel32.dll,不管是glibc的linux方式還是kernel32.dll的windows方式都是在使用者空間實現的記憶體管理方式,多數情況也就是堆管理方式,其實堆只在使用者空間有意義,而棧對核心和使用者空間都有意義,glibc一直在努力向posix靠攏,而kernel32.dll卻始終只封裝自己獨有的win32的api實現,這顯然是兩種截然不同的方式,glibc趨向於開放而kernel32.dll的實現卻只能靠別的方式得到。

不管怎麼說,kernel32.dll實現了堆的管理,只要通過HeapAlloc和HeapFree來體現的,對比AT&T的實現,發現其基本的原理並沒有改變,本質沒有變,仍然是維護一個全域性的空閒連結串列,將分配出去的記憶體塊從空閒連結串列摘除,釋放的時候在重新加回空閒連結串列之前要確定一下能否和相鄰的塊合併,本質就是這樣,windows的實現並沒有變,windows實現的HeapAlloc改進的地方在於細化了全域性的空閒連結串列,不再像AT&T那樣的方式完全隨意分配和隨意回收,而是加入了固定的大小的子連結串列,其實就是一個吊鏈結構,類似於linux核心的夥伴系統,全域性上有一個連結串列,該連結串列的元素是一個子連結串列,該全域性連結串列一共N個元素,第0個元素代表的子連結串列中元素是不確定大小的大記憶體塊,也就是都大於一個值,但是不確定具體是多少,這個子連結串列用於分配稍微大一些的記憶體塊,以下對於從第1個元素到第N-1個元素的每一個元素索引i,該元素代表的子連結串列代表的是大小為i*8個位元組的記憶體塊組成的空閒連結串列,實際上這才是真正的空閒連結串列,整個系統中擁有N條空閒連結串列,和AT&T的實現一樣,被分配出去的記憶體塊將不再留在空閒連結串列,那麼就可以由資料區來儲存空閒連結串列的前向和後向索引指標,因為資料區因為對齊因素最小8個位元組,一個指標4個位元組正好夠兩個指標使用,這樣的話就不必另外開闢空間儲存前向後向指標了,節省了空間開銷,這一點上要比AT&T的有進步,AT&T的資料結構雖然巧妙,但是考慮到被使用的非空閒塊,next指標實際上沒有什麼用,不過帶來的演算法的簡便足以彌補這點不起眼的損耗。為了在回收的時候可以嘗試和相鄰的記憶體塊合併,需要一個頭部來儲存該記憶體塊的資訊,比如是否空閒,誰和自己相鄰以及相鄰塊的大小等等,是否空閒只要是說檢查相鄰的塊是否空閒,因此尋找誰是和自己相鄰的塊就是必須要解決的問題了,主要就是要找到這個 塊的頭部,這個問題初看起來很困難,實際上很簡單,只要知道堆就是由一個一個的記憶體塊組成的並且是在虛擬記憶體中順序排列開的就可以了,只是將所有的空閒記憶體塊連線成了所謂的邏輯連結串列,即使一塊記憶體被分配了,它的位置也還是不會變化的,那麼既然所有記憶體塊是順序排列開的,只要知道相鄰塊的大小和自己的大小就可以找到相鄰塊了,要找前面的一塊,就需要前面一塊的大小,這樣當前塊的地址減去前面一塊的大小,再減去頭的大小就是前一塊的頭部了,而要是找後面的一塊,很簡單隻需要將指標往後移動一個頭的大小和當前塊的大小就可以了,找到了相鄰的塊,是否空閒的資訊就保留在塊的頭部,頭部當然還有別的有用的資訊,如果空閒,那麼就可以合併了,合併了之後就是一個新的空閒塊了,大小為兩個合併塊的大小總和加上一個頭部的大小,然後再根據新的大小確定將之加入到哪一個全域性連結串列的子連結串列,如果足夠大了,那就加入到0號元素代表的連結串列,如果不夠大並且大小為M,那麼就加入到第M/8號元素代表的連結串列,分配過程自然要比釋放簡單,沒有那麼多整理操作,就是一個簡單的查詢過程,如果需要的記憶體塊太大,那麼從0號元素的連結串列分配,如果比較小就從相應大小連結串列分配,如果對應大小連結串列沒有空閒記憶體塊,那麼就在稍微大一號的連結串列搜尋,直到找到為止,然後分出去自己需要的大小的那個塊,將剩下的重新按照其大小排入相應大小的空閒連結串列,注意以上操作都有對齊操作,這樣會更加簡單和高效,如果實在沒有找到空閒記憶體,那麼就只有向作業系統索要了,也就是擴充套件堆的大小。

windows的debug版本的crt對堆的保護採用了一種很傻很天真的做法,就是分配的時候在記憶體塊的末尾加入固定長度並且固定資料的資料,然後在釋放的時候檢查這些固定的資料是不是被改變了,如果被改變了就說明堆出錯了,反之只能認為沒有,為何說很傻呢,我要是黑客並且我知道填充的是0xcd的話,那麼我完全可以填充成0xcd以欺騙windows,更幸運的是release版本根本就不檢查堆溢位,debug版本之所以檢查並不是為了安全,如果從安全考慮的話,再安全的機制都可能被攻破,相反windows的debug版本crt採用的這種溢位檢測是為了讓程式設計師在開發的時候就檢查出會出現的錯誤,主要是為了糾錯而不是防禦。最終,安全的重任在程式設計師而不是作業系統,作業系統只能保證自己提供機制的內在安全,也就是說這種安全是內秉的,比如我託付給作業系統的檔案不應該被你看到等等,任何防護諸如緩衝區溢位攻擊的作業系統都是失敗的,作業系統核心根本防不住的,最好的保護就是程式設計師寫出健壯的程式,並且程式安全的責任是程式設計師的而不是作業系統的。

以上就是windows的堆管理邏輯,和AT&T相比,它增加了很多規則的東西,從理論上將所謂規則的意義不是很大,然而計算機的和諧不僅僅是理論的和諧,更多的是執行上的高效,因此從執行上講,規則的含義就變得重要了,規則的好處在於很多策略都是內定的,靜態的,因此就可以將很多需要權衡的操作變成簡單的查詢,也就是說不規則的機制往往需要在策略上做很多優化其效率才差強人意,比如做一些啟發式演算法等等,windows的Heap管理做到了一定的規則,但是也僅僅做到了這些,如果在這個基礎上新增一些策略就更好了,windows不知道為何沒有做到這些而僅僅實現了一個簡單的規則方案,然而glibc做到了,且聽下文分解。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1274093


相關文章