Linux進程內存佈局(翻譯)

Anatomy of a Program in Memoryhtml

在一個多任務OS中,每一個進程都運行在它本身的內存沙箱中。這個沙箱就是虛擬地址空間,在32位下就是一塊容量爲4GB的內存地址。內核將這些虛擬地址按頁表(page table)映射爲物理內存,並交由CPU訪問。每一個進程有本身的頁表集,但有一點要注意。虛擬地址一旦被啓用,就會應用到機器上全部運行的程序上,也包括內核本身。所以虛擬地址空間必須爲內核預留一部分(不然就沒辦法和內核交互了):
linux

給內核預留那麼多空間,並非說內核真的使用了那麼多物理內存,只是內核能夠這部分虛擬地址自由的映射到某片物理內存上(而不用和其它虛擬地址遵循相同的規則)。內核空間在頁表中被標記爲專屬的特權代碼(ring 2或更低),用戶態的程序訪問它時就會產生頁錯誤(page fault)。Linux的內核空間在全部進程中都映射到相同的物理內存上。內核代碼和數據老是可尋址的(老是能夠找到的),任意時刻均可處理中斷和系統調用。做爲對比,當進程切換時,用戶態地址空間的映射就會發生改變:
算法

藍色區域表示映射到物理內存的虛擬地址空間,而白色區域則表示未映射的空間。在這個例子中,Firefox驚人的內存需求讓它使用的虛擬地址遠遠超過了其自身的地址空間。內存地址空間是按堆、棧這樣的內存段進行管理的。要記住內存段就是簡單的一個內存地址範圍,並且與Intel風格的段沒有任何關係。下面是一個Linux進程的標準內存段佈局:
編程

若是計算過程輕鬆愉快、準確無誤,那麼上圖顯示的內存段起始虛擬地址在幾乎每一個進程中都是同樣的。這致使了遠程利用安全漏洞變得很是容易。一次漏洞探測一般須要引用內存的絕對地址:一個棧上地址,一個庫函數的地址,等等。遠程攻擊者只能盲目的選擇這樣的地址,期望地址空間都是同樣的。若是這種狀況真發生了,用戶就悲劇了。所以地址空間隨機化變的很流行。Linux會給mmap段、和的起始地址一個隨機偏離。不幸的是,32位地址空間實在是太緊湊了,只給隨機化很小的空間,妨礙了它的效果ubuntu

進程地址空間的最上面是棧,棧裏保存了局部變量,以及大多數編程語言中的函數參數。一次方法或函數調用就會向棧增長一個棧幀(stack frame)。當函數返回時棧幀就會被銷燬。由於數據遵循嚴格的「後入先出」順序,這種簡單的設計意味着不須要複雜的數據結構就能追蹤到棧的上下文——棧頂的一個指針就搞定。棧的Push和pop操做所以變得快速且肯定。同時,棧區域的穩定重用也有助於棧內存在CPU緩存中保持活躍,加快了訪問速度。進程中的每一個線程都有本身的棧。c#

若是推入棧的數據過多,可能會耗盡棧映射的地址區域。這會致使一次頁錯誤,Linux將其處理爲一次expand_stack()調用,其實是調用acct_stack_growth()檢查當前是否能夠增長棧大小。若是棧大小小於RLIMIT_STACK(一般8MB)就能夠繼續增加,程序會正常繼續,不會察覺到什麼。這是棧大小調整的默認處理。可是,若是棧大小達到了上限,就會發生棧溢出,程序會接到一次段錯誤(Segmentation Fault)。相對的,當棧變小時,不會縮減棧大小。這就像聯邦預算,只增不減(笑)。緩存

在訪問到未映射內存區(上圖中的白色部分)時,只有動態棧增加多是合法的。其它方式訪問到未映射區域時都會引起一次頁錯誤,進而致使段錯誤。一些映射區是隻讀的,對它們的寫操做也會致使段錯誤。安全

在棧的下面就是mmap段,內核在這裏將文件內容映射爲內存。任何應用均可以經過Linux下的mmap()或Windows下的CreateFileMapping()/MapViewOfFile()申請一片mmap區域。mmap是一種高效便捷的文件I/O方式,被用於加載動態連接庫。咱們一樣能夠建立一塊與文件沒有關係的mmap區,用來存放程序的數據。在Linux中,若是你經過malloc()申請一大塊內存,glibc會返回一塊匿名的mmap內存塊,而不是用堆內存。「大」意思是比MMAP_THRESHOLD大,一般是128KB,能夠調用mallopt()修改。數據結構

接下來咱們開始說堆。堆提供了運行時的內存分配,這點和棧相似;數據的生命期與執行分配的函數生命期不一致,這點與棧不一樣。大多數語言都提供了堆的管理。所以知足內存請求須要語言的運行環境和內核協做完成。C語言中分配堆的接口是malloc()族函數,而在有gc的語言(例如C#)其接口則是關鍵字new。app

若是堆內存足夠,語言運行時環境就能夠處理內存請求,不須要內核介入,或者能夠經過brk()系統調用去增大堆內存。堆的管理很複雜,在面對程序雜亂的分配方式時,須要使用在速度和內存使用率上努力作平衡的微妙算法。知足一次堆請求的時間差別能夠很是大。實時系統須要特殊用途的分配器來處理這個問題。使用時堆也會變的很碎片化,見下圖:

最後,咱們看一下最下面的內存段:BSS、data、代碼段。在C裏面BSS和data段都是存儲靜態變量(全局)的數據。區別是BSS段存儲沒有初始化的靜態變量,即在代碼中沒有初始值的靜態變量。BSS區是匿名的:不映射自任何文件。若是代碼中有static int cntActiveUsers;,那麼cntActiveUsers就在BSS段。

而data段則存放代碼中顯示初始化了的靜態變量。這塊內存區不是匿名的,它映射自程序鏡像中包含對應靜態變量的文件。這種映射是私有映射,即內存中的變化不會反映到文件中。若是不這樣,改變一個全局變量的值就會修改你磁盤中的鏡像文件,這就太荒謬了!

下圖中的data示例有些取巧,使用了一個指針。指針gonzo的內容——4字節的地址——就存在data段。而它指向的字符串則不是,字符串存放在text段。text段是隻讀的,存放字符串字面值等不會執行的代碼。text段也會映射程序文件,但對它的寫操做會引起段錯誤。這會避免一些指針bug,但不像第一時間在C代碼中避免指針bug那麼有效。下圖顯示了這些段和示例變量:

你能夠讀/proc/pid_of_process/maps來檢查一個Linux進程的內存區。一個內存段可能包含多個內存區。例如,每一個mmap映射的文件都會在mmap段有一個單獨的內存區,而動態庫還會有在BSS和data段的內存區。下篇文章會剖析「內存區」意味着什麼。有時人們也會用「data段」代指整個data + BSS + 堆。

你能夠用nmobjdump命令檢查鏡像文件中的符號、它們的地址、所屬的段,等等。最後,上面說的虛擬地址佈局是Linux的「靈活」佈局,也是近年來Linux的默認佈局。它假設RLIMIT_STACK有值。不然Linux會回到下圖的「經典」佈局:

相關文章
相關標籤/搜索