微軟的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