1. 程式人生 > >記憶體地址空間佈局

記憶體地址空間佈局

在多工作業系統中的每一個程序都執行在一個屬於它自己的記憶體沙盤中。這個沙盤就是虛擬地址空間(virtual address space)。

1 32位虛擬記憶體佈局

在32位模式下虛擬地址空間總是一個4GB的記憶體地址塊。這些虛擬地址通過頁表(page table)對映到實體記憶體,頁表由作業系統維護並被處理器引用。每一個程序擁有一套屬於它自己的頁表,但是還有一個隱情。只要虛擬地址被使用,那麼它就會作用於這臺機器上執行的所有軟體,包括核心本身。因此一部分虛擬地址必須保留給核心使用:

wps_clip_image-30045

圖 1

這並不意味著核心使用了那麼多的實體記憶體,僅表示它可支配這麼大的地址空間,可根據核心需要,將其對映到實體記憶體。核心空間在頁表中擁有較高的特權級(ring 2或以下),因此只要使用者態的程式試圖訪問這些頁,就會導致一個頁錯誤(page fault),使用者程式不可訪問核心頁。在

Linux中,核心空間是持續存在的,並且在所有程序中都對映到同樣的實體記憶體。核心程式碼和資料總是可定址的,隨時準備處理中斷和系統呼叫。與此相反,使用者模式地址空間的對映隨程序切換的發生而不斷變化:

wps_clip_image-17631

圖 2

圖2中,藍色區域表示對映到實體記憶體的虛擬地址,而白色區域表示未對映的部分。在上面的例子中,Firefox使用了相當多的虛擬地址空間,因為它是傳說中的吃記憶體大戶。地址空間中的各個條帶對應於不同的記憶體段(memory segment),如:堆、棧之類的。記住,這些段只是簡單的記憶體地址範圍,與Intel處理器的段沒有關係。

1.1 32位經典記憶體佈局

wps_clip_image-10604

圖 3

32位經典記憶體佈局,程式起始1GB地址為核心空間,接下來是向下增長的棧空間和由0×40000000向上增長的mmap地址。而堆地址是從底部開始,去除ELF、程式碼段、資料段、常量段之後的地址並向上增長。但是這種佈局有幾個問題,首先是容易遭受溢位攻擊;其次是,堆地址空間只有不到1G有木有?如果mmap記憶體比較少地址很浪費有木有?所以後來就有了另一種記憶體佈局

1.2 32位預設記憶體佈局

wps_clip_image-19772

圖 4

當計算機開心、安全、可愛、正常的運轉時,幾乎每一個程序的各個段的起始虛擬地址都與圖4完全一致,這也給遠端發掘程式安全漏洞打開了方便之門。一個發掘過程往往需要引用絕對記憶體地址:棧地址,庫函式地址等。遠端攻擊者必須依賴地址空間佈局的一致性,摸索著選擇這些地址。如果讓他們猜個正著,有人就會被整了。因此,地址空間的隨機排布方式逐漸流行起來。Linux通過對棧、記憶體對映段、堆的起始地址加上隨機的偏移量來打亂佈局。不幸的是,32位地址空間相當緊湊,給隨機化所留下的空當不大,削弱了這種技巧的效果。

程序地址空間中最頂部的段是棧,大多數程式語言將之用於儲存區域性變數和函式引數。呼叫一個方法或函式會將一個新的棧楨(stack frame)壓入棧中。棧楨在函式返回時被清理。也許是因為資料嚴格的遵從LIFO的順序,這個簡單的設計意味著不必使用複雜的資料結構來追蹤棧的內容,只需要一個簡單的指標指向棧的頂端即可。因此壓棧(pushing)和退棧(popping)過程非常迅速、準確。另外,持續的重用棧空間有助於使活躍的棧記憶體保持在CPU快取中,從而加速訪問。程序中的每一個執行緒都有屬於自己的棧。

通過不斷向棧中壓入的資料,超出其容量就有會耗盡棧所對應的記憶體區域。這將觸發一個頁故障(page fault),並被Linux的expand_stack()處理,它會呼叫acct_stack_growth()來檢查是否還有合適的地方用於棧的增長。如果棧的大小低於RLIMIT_STACK(通常是8MB),那麼一般情況下棧會被加長,程式繼續愉快的執行,感覺不到發生了什麼事情。這是一種將棧擴充套件至所需大小的常規機制。然而,如果達到了最大的棧空間大小,就會棧溢位(stack overflow),程式收到一個段錯誤(Segmentation Fault)。當映射了的棧區域擴充套件到所需的大小後,它就不會再收縮回去,即使棧不那麼滿了。這就好比聯邦預算,它總是在增長的。

動態棧增長是唯一一種訪問未對映記憶體區域(圖中白色區域)而被允許的情形。其它任何對未對映記憶體區域的訪問都會觸發頁故障,從而導致段錯誤。一些被對映的區域是隻讀的,因此企圖寫這些區域也會導致段錯誤。

記憶體對映段

在棧的下方,是我們的記憶體對映段。此處,核心將檔案的內容直接對映到記憶體。任何應用程式都可以通過Linux的mmap()系統呼叫(實現)或Windows的CreateFileMapping() / MapViewOfFile()請求這種對映。記憶體對映是一種方便高效的檔案I/O方式,所以它被用於載入動態庫。建立一個不對應於任何檔案的匿名記憶體對映也是可能的,此方法用於存放程式的資料。在Linux中,如果你通過malloc()請求一大塊記憶體,C執行庫將會建立這樣一個匿名對映而不是使用堆記憶體。‘大塊’意味著比MMAP_THRESHOLD還大,預設是128KB,可以通過mallopt()調整。

說到堆,它是接下來的一塊地址空間。與棧一樣,堆用於執行時記憶體分配;但不同點是,堆用於儲存那些生存期與函式呼叫無關的資料。大部分語言都提供了堆管理功能。因此,滿足記憶體請求就成了語言執行時庫及核心共同的任務。在C語言中,堆分配的介面是malloc()系列函式,而在具有垃圾收集功能的語言(如C#)中,此介面是new關鍵字。

如果堆中有足夠的空間來滿足記憶體請求,它就可以被語言執行時庫處理而不需要核心參與。否則,堆會被擴大,通過brk()系統呼叫(實現)來分配請求所需的記憶體塊。堆管理是很複雜的,需要精細的演算法,應付我們程式中雜亂的分配模式,優化速度和記憶體使用效率。處理一個堆請求所需的時間會大幅度的變動。實時系統通過特殊目的分配器來解決這個問題。堆也可能會變得零零碎碎,如下圖所示:

wps_clip_image-19798

圖 5

BSS 資料段 程式碼段

最後,我們來看看最底部的記憶體段:BSS,資料段,程式碼段。在C語言中,BSS和資料段儲存的都是靜態(全域性)變數的內容。區別在於BSS儲存的是未被初始化的靜態變數內容,它們的值不是直接在程式的原始碼中設定的。BSS記憶體區域是匿名的:它不對映到任何檔案。如果你寫static int cntActiveUsers,則cntActiveUsers的內容就會儲存在BSS中。

另一方面,資料段儲存在原始碼中已經初始化了的靜態變數內容。這個記憶體區域不是匿名的。它映射了一部分的程式二進位制映象,也就是原始碼中指定了初始值的靜態變數。所以,如果你寫static int cntWorkerBees = 10,則cntWorkerBees的內容就儲存在資料段中了,而且初始值為10。儘管資料段映射了一個檔案,但它是一個私有記憶體對映,這意味著更改此處的記憶體不會影響到被對映的檔案。也必須如此,否則給全域性變數賦值將會改動你硬碟上的二進位制映象,這是不可想象的。

下圖中資料段的例子更加複雜,因為它用了一個指標。在此情況下,指標gonzo(4位元組記憶體地址)本身的值儲存在資料段中。而它所指向的實際字串則不在這裡。這個字串儲存在程式碼段中,程式碼段是隻讀的,儲存了你全部的程式碼外加零零碎碎的東西,比如字串字面值。程式碼段將你的二進位制檔案也對映到了記憶體中,但對此區域的寫操作都會使你的程式收到段錯誤。這有助於防範指標錯誤,雖然不像在C語言程式設計時就注意防範來得那麼有效。下圖展示了這些段以及我們例子中的變數:

wps_clip_image-10904

圖 6

你可以通過閱讀檔案/proc/pid_of_process/maps來檢驗一個Linux程序中的記憶體區域。記住一個段可能包含許多區域。比如,每個記憶體對映檔案在mmap段中都有屬於自己的區域,動態庫擁有類似BSS和資料段的額外區域。下一篇文章講說明這些“區域”(area)的真正含義。有時人們提到“資料段”,指的就是全部的資料段+ BSS + 堆。

2 64位虛擬記憶體佈局

64位系統的定址空間比較大,所以仍然沿用了32位的經典佈局,但是加上了隨機的mmap起始地址,以防止溢位攻擊。反正一時半會是用不了這麼大的記憶體地址了,所以至少N多年不會變了。

首先, 目前大部分的作業系統和應用程式並不需要16EB( 264 )如此巨大的地址空間, 實現64位長的地址只會增加系統的複雜度和地址轉換的成本, 帶不來任何好處. 所以目前的x86-64架構CPU都遵循AMD的Canonical form, 即只有虛擬地址的最低48位才會在地址轉換時被使用, 且任何虛擬地址的48位至63位必須與47位一致(sign extension). 也就是說, 總的虛擬地址空間為256TB( 248 ).

wps_clip_image-22928 
圖 7

然後, 在這256TB的虛擬記憶體空間中, 0000000000000000 - 00007fffffffffff(128TB)為使用者空間, ffff800000000000 - ffffffffffffffff(128TB)為核心空間. 這裡需要注意的是, 核心空間中有很多空洞, 越過第一個空洞後, ffff880000000000 - ffffc7ffffffffff(64TB)才是直接對映實體記憶體的區域, 也就是說預設的PAGE_OFFSET為ffff880000000000. 從這裡我們也可以看出, 這麼大的直接對映區域足夠對映所有的實體記憶體, 所以目前x86-64架構下是不存在高階記憶體, 也就是ZONE_HIGHMEM這個區域的(參考上篇)