segments

一個進程通常由載入一個elf文件啟動,而elf文件是由若干segments組成的,同樣的,進程地址空間也由許多不同屬性的segments組成,但這與硬體意義上的segmentation機制(參考這篇文章)不同,後者在某些體系結構(比如x86)中起重要作用,充當內存中物理地址連續的獨立空間。linux進程中的segment是虛擬地址空間中用於保存數據的區域,只在虛擬地址上連續。

text段包含了當前運行進程的二進位代碼,其起始地址在IA32體系中中通常為0x08048000,在IA64體系中通常為0x0000000000400000(都是虛擬地址哈)。data段存儲已初始化的全局變數,bss段存儲未初始化的全局變數。從上圖可以看出,這3個segments是緊挨者的,因為它們的大小是確定的,不會動態變化。

與之相對應的就是heap段和stack段。heap段存儲動態分配的內存中的數據,stack段用於保存局部變數和實現函數/過程調用的上下文,它們的大小都是會在進程運行過程中發生變化的,因此中間留有空隙,heap向上增長,stack向下增長,因為不知道heap和stack哪個會用的多一些,這樣設置可以最大限度的利用中間的空隙空間。

還有一個段比較特殊,是mmap()系統調用映射出來的。mmap映射的大小也是不確定的。3GB的虛擬地址空間已經很大了,但heap段, stack段,mmap段在動態增長的過程還是有重疊(碰撞)的可能。為了避免重疊發生,通常將mmap映射段的起始地址選在TASK_SIZE/3(也就是1GB)的位置。如果是64位系統,則虛擬地址空間更加巨大,幾乎不可能發生重疊。

如果stack段和mmap段都採用固定的起始地址,這樣實現起來簡單,而且所有linux系統都能保持統一,但是真實的世界不是那麼簡單純潔的,正邪雙方的較量一直存在。對於攻擊者來說,如果他知道你的這些segments的起始地址,那麼他構建惡意代碼(比如通過緩衝區溢出獲得棧內存區域的訪問權,進而惡意操縱棧的內容)就變得容易了。一個可以採用的反制措施就是不為這些segments的起點選擇固定位置,而是在每次新進程啟動時(通過設置PF_RANDOMIZE標誌)隨機改變這些值的設置。

那這些segments的載入順序是怎樣的呢?以下圖為例,首先通過execve()執行elf,則該可執行文件的text段,data段,stack段就建立了,在進程運行過程中,可能需要藉助ld.so載入動態鏈接庫,比如最常用的libc,則libc.so的text段,data段也建立了,而後可能通過mmap()的匿名映射來實現與其他進程的共享內存,還有可能通過brk()來擴大heap段的大小。

vm_area_struct

在linux中,每個segment用一個vm_area_struct(以下簡稱vma)結構體表示。vma是通過一個單項鏈表串起來的,現存的vma按起始地址以遞增次序被歸入鏈表中,每個vma是這個鏈表裡的一個節點。

在用戶空間可通過"/proc/PID/maps"介面來查看一個進程的所有vma在虛擬地址空間的分布情況,其內部實現靠的就是對這個鏈表的遍歷。

同時,vma又通過紅黑樹(red black tree)組織起來,每個vma又是這個紅黑樹里的一個節點。為什麼要同時使用兩種數據結構呢?使用鏈表管理固然簡單方便,但是通過查找鏈表找到與特定地址關聯的vma,其時間複雜度是O(N),而現實應用中,在進程地址空間中查找vma又是非常頻繁的操作(比如發生page fault的時候)。使用紅黑樹的話時間複雜度是O( log_{2}N ),尤其在vma數量很多的時候,可以顯著減少查找所需的時間(數量翻倍,查找次數也僅多一次)。同時,紅黑樹是一種非平衡二叉樹,可以簡化重新平衡樹的過程。

現在我們來看一下vm_area_struct結構體在linux中是如何定義的(為了講解的需要對結構體內元素的分布有所調整):

struct vm_area_struct
{
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
rb_node_t vm_rb;
unsigned long vm_flags;
struct file * vm_file;
unsigned long vm_pgoff;
struct mm_struct * vm_mm;
...
}

其中,vm_start和vm_end分別是這個vma所指向區域的起始地址和結束地址,雖然vma是虛擬地址空間,但最終畢竟是要映射到物理內存上去的,所以也要求是4KB對齊的。

vm_next是指向單向鏈表的下一個vma,vm_rb是作為紅黑樹的一個節點。

vm_flags描述的是vma的屬性,flag可以是VM_READ、VM_WRITE、VM_EXEC、VM_SHARED,分別指定vma的內容是否可以讀、寫、執行,或者由幾個進程共享。前面介紹的頁表PTE中也有類似的Read/Write許可權限制位,那它和vma中的這些標誌位是什麼關係呢?

vma由許多的虛擬pages組成,每個虛擬page需要經過page table的轉換才能找到對應的物理頁面。PTE中的Read/Write位是由軟體設置的,設置依據就是這個page所屬的vma,因此一個vma設置的VM_READ/VM_WRITE屬性會複製到這個vma所含pages的PTE中。之後,硬體MMU就可以在地址翻譯的過程中根據PTE的標誌位來檢測訪問是否合法,這也是為什麼PTE是一個軟體實現的東西,但又必須按照處理器定義的格式去填充,這可以理解為軟硬體之間的一種約定。那可以用軟體去檢測PTE么?當然可以,但肯定沒有用專門的硬體單元來處理更快嘛。

可執行文件和動態鏈接庫的text段和data段是基於elf文件的,mmap對文件的映射也是對應外部存儲介質中這個被映射的文件的,這兩種情況下,vm_file指向這個被映射的文件,進而可獲得該文件的inode信息,而vm_pgoff是這個段在該文件內的偏移,對於text段,一般偏移就是0。對於heap段,stack段以及mmap的匿名映射,沒有與之相對應的文件實體,此時vm_file就為NULL,vm_pgoff的值沒有意義。

那一個進程是怎麼找到它的這些vma的呢?請看下文分解。

參考:

How The Kernel Manages Your Memory


推薦閱讀:
相关文章