在多任務操做系統中的每個進程都運行在一個屬於它本身的內存沙盤中。這個沙盤就是虛擬地址空間(virtual address space)。html
在32位模式下虛擬地址空間老是一個4GB的內存地址塊。這些虛擬地址經過頁表(page table)映射到物理內存,頁表由操做系統維護並被處理器引用。每個進程擁有一套屬於它本身的頁表,可是還有一個隱情。只要虛擬地址被使用,那麼它就會做用於這臺機器上運行的全部軟件,包括內核自己。所以一部分虛擬地址必須保留給內核使用:linux
圖 1算法
這並不意味着內核使用了那麼多的物理內存,僅表示它可支配這麼大的地址空間,可根據內核須要,將其映射到物理內存。內核空間在頁表中擁有較高的特權級(ring 2或如下),所以只要用戶態的程序試圖訪問這些頁,就會致使一個頁錯誤(page fault),用戶程序不可訪問內核頁。在Linux中,內核空間是持續存在的,而且在全部進程中都映射到一樣的物理內存。內核代碼和數據老是可尋址的,隨時準備處理中斷和系統調用。與此相反,用戶模式地址空間的映射隨進程切換的發生而不斷變化:編程
圖 2c#
圖2中,藍色區域表示映射到物理內存的虛擬地址,而白色區域表示未映射的部分。在上面的例子中,Firefox使用了至關多的虛擬地址空間,由於它是傳說中的吃內存大戶。地址空間中的各個條帶對應於不一樣的內存段(memory segment),如:堆、棧之類的。記住,這些段只是簡單的內存地址範圍,與Intel處理器的段沒有關係。緩存
圖 3安全
32位經典內存佈局,程序起始1GB地址爲內核空間,接下來是向下增加的棧空間和由0×40000000向上增加的mmap地址。而堆地址是從底部開始,去除ELF、代碼段、數據段、常量段以後的地址並向上增加。可是這種佈局有幾個問題,首先是容易遭受溢出攻擊;其次是,堆地址空間只有不到1G有木有?若是mmap內存比較少地址很浪費有木有?因此後來就有了另外一種內存佈局數據結構
圖 4架構
當計算機開心、安全、可愛、正常的運轉時,幾乎每個進程的各個段的起始虛擬地址都與圖4徹底一致,這也給遠程發掘程序安全漏洞打開了方便之門。一個發掘過程每每須要引用絕對內存地址:棧地址,庫函數地址等。遠程攻擊者必須依賴地址空間佈局的一致性,摸索着選擇這些地址。若是讓他們猜個正着,有人就會被整了。所以,地址空間的隨機排布方式逐漸流行起來。Linux經過對棧、內存映射段、堆的起始地址加上隨機的偏移量來打亂佈局。不幸的是,32位地址空間至關緊湊,給隨機化所留下的空當不大,削弱了這種技巧的效果。app
進程地址空間中最頂部的段是棧,大多數編程語言將之用於存儲局部變量和函數參數。調用一個方法或函數會將一個新的棧楨(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()系統調用(實現)來分配請求所需的內存塊。堆管理是很複雜的,須要精細的算法,應付咱們程序中雜亂的分配模式,優化速度和內存使用效率。處理一個堆請求所需的時間會大幅度的變更。實時系統經過特殊目的分配器來解決這個問題。堆也可能會變得零零碎碎,以下圖所示:
圖 5
最後,咱們來看看最底部的內存段:BSS,數據段,代碼段。在C語言中,BSS和數據段保存的都是靜態(全局)變量的內容。區別在於BSS保存的是未被初始化的靜態變量內容,它們的值不是直接在程序的源代碼中設定的。BSS內存區域是匿名的:它不映射到任何文件。若是你寫static int cntActiveUsers,則cntActiveUsers的內容就會保存在BSS中。
另外一方面,數據段保存在源代碼中已經初始化了的靜態變量內容。這個內存區域不是匿名的。它映射了一部分的程序二進制鏡像,也就是源代碼中指定了初始值的靜態變量。因此,若是你寫static int cntWorkerBees = 10,則cntWorkerBees的內容就保存在數據段中了,並且初始值爲10。儘管數據段映射了一個文件,但它是一個私有內存映射,這意味着更改此處的內存不會影響到被映射的文件。也必須如此,不然給全局變量賦值將會改動你硬盤上的二進制鏡像,這是不可想象的。
下圖中數據段的例子更加複雜,由於它用了一個指針。在此狀況下,指針gonzo(4字節內存地址)自己的值保存在數據段中。而它所指向的實際字符串則不在這裏。這個字符串保存在代碼段中,代碼段是隻讀的,保存了你所有的代碼外加零零碎碎的東西,好比字符串字面值。代碼段將你的二進制文件也映射到了內存中,但對此區域的寫操做都會使你的程序收到段錯誤。這有助於防範指針錯誤,雖然不像在C語言編程時就注意防範來得那麼有效。下圖展現了這些段以及咱們例子中的變量:
圖 6
你能夠經過閱讀文件/proc/pid_of_process/maps來檢驗一個Linux進程中的內存區域。記住一個段可能包含許多區域。好比,每一個內存映射文件在mmap段中都有屬於本身的區域,動態庫擁有相似BSS和數據段的額外區域。下一篇文章講說明這些「區域」(area)的真正含義。有時人們提到「數據段」,指的就是所有的數據段+ BSS + 堆。
64位系統的尋址空間比較大,因此仍然沿用了32位的經典佈局,可是加上了隨機的mmap起始地址,以防止溢出攻擊。反正一時半會是用不了這麼大的內存地址了,因此至少N多年不會變了。
首先, 目前大部分的操做系統和應用程序並不須要16EB( 264 )如此巨大的地址空間, 實現64位長的地址只會增長系統的複雜度和地址轉換的成本, 帶不來任何好處. 因此目前的x86-64架構CPU都遵循AMD的Canonical form, 即只有虛擬地址的最低48位纔會在地址轉換時被使用, 且任何虛擬地址的48位至63位必須與47位一致(sign extension). 也就是說, 總的虛擬地址空間爲256TB( 248 ).
而後, 在這256TB的虛擬內存空間中, 0000000000000000 - 00007fffffffffff(128TB)爲用戶空間, ffff800000000000 - ffffffffffffffff(128TB)爲內核空間. 這裏須要注意的是, 內核空間中有不少空洞, 越過第一個空洞後, ffff880000000000 - ffffc7ffffffffff(64TB)纔是直接映射物理內存的區域, 也就是說默認的PAGE_OFFSET爲ffff880000000000. 從這裏咱們也能夠看出, 這麼大的直接映射區域足夠映射全部的物理內存, 因此目前x86-64架構下是不存在高端內存, 也就是ZONE_HIGHMEM這個區域的(參考這篇).