摘要:所以在將目標文件鏈接成可執行文件的時候,鏈接器會盡量把相同或相似權限屬性的section分配在同一空間,在程序頭表中,將一個或多個屬性類似的section合併爲一個segment,然後在裝載的時候,將這個segment映射到進程虛擬地址空間中的一個VMA中。Linux會爲這個進程創建一個新的虛擬地址空間,然後會讀取可執行文件的文件頭,建立虛擬地址空間與可執行文件的映射關係,然後將CPU的指令指針寄存器設置成可執行文件的入口地址,然後CPU就會從這裏取指令執行。

作者簡介:

本文由西郵陳莉君教授研一學生賀東昇編輯,梁金榮、張孝家校對

Linux可執行文件與進程的虛擬地址空間

一個可執行文件被執行的同時也伴隨着一個新的進程的創建。Linux會爲這個進程創建一個新的虛擬地址空間,然後會讀取可執行文件的文件頭,建立虛擬地址空間與可執行文件的映射關係,然後將CPU的指令指針寄存器設置成可執行文件的入口地址,然後CPU就會從這裏取指令執行。

一個可執行文件包含可被CPU執行的指令和待處理的數據,上CPU之前,指令和數據全部被翻譯成成二進制的形式。在可執行的文件的內部,劃分出了一些專門的段,如代碼段,數據段,BSS段等。代碼段中存放的是可執行的二進制指令,數據段存放初始化過的變量,BSS段存放未初始化的變量,從裝載的角度,把這些段稱爲segment。

32位的虛擬地址空間

64位的虛擬地址空間

Proc目錄下的進程虛擬地址空間佈局

Linux在裝載可執行文件的時候,會將這些segment映射到進程的地址空間中。映射的時候,這裏面的segment會對應一個VMA。Linux將進程虛擬地址空間中的一個段叫做虛擬內存區域(VMA)。在/proc目錄下,可以查看一個進程的虛擬地址空間,通過命令

cat /proc/pid/maps

這裏面的每一行都對應一個VMA,每一個VMA都通過 vm_area_struct 結構體來描述。結構體中的 vm_start vm_end 是VMA的起始地址和結束地址,還有其他的一些域來描述VMA的權限等。我們需要關注的是前三個VMA,這是ELF可執行文件的segment映射過來的。可以看到,這裏面並沒有標明哪個是TEXT段,哪個是DATA段和BSS段。但是可以發現,前三個VMA的權限都不一樣。

虛擬地址空間存儲區的分佈

所以,操作系統實際上並不關心可執行文件各個段所包含的的實際內容,OS只關心一些跟裝載相關的問題,最主要的是段的權限(可讀,可寫,可執行)。

ELF文件中,段的權限往往只有爲數不多的幾種組合,基本上就3種:

  1. 以代碼段爲代表的權限爲可讀可執行的段

  2. 以數據段和BSS段爲代表的權限爲可讀可寫的段

  3. 以只讀數據段爲代表的權限爲只讀的段

ELF可執行文件中有兩個概念,分別是段(segment)和節(section)。通過readelf -S name.elf可以查看ELF可執行文件的節頭表,這裏面有所有節的信息

在將目標文件鏈接成可執行文件的時候,鏈接器會盡量把相同權限屬性的段分配在同一空間。比如可讀可執行的段都放在一起,這種段的典型是代碼段;可讀可寫的段都放在一起,這種段的典型是數據段。在ELF中,把這些屬性相似的,又連在一起的段叫做一個“segment”,而系統正是按照“segment”而不是“section”來映射可執行文件的。

可以使用命令  readelf -l name.elf 來查看ELF的段。在ELF的程序頭表,保存着segment的信息

最下面是是段與節的歸屬關係:

可以看到這個可執行文件中共有9個segment。從裝載的角度看,我們只關心兩個“LOAD”型的segment,因爲只有它是需要被映射的,其他諸如“NOTE”,"GNU_STACK"都是在裝載時起輔助作用的。下面的0到8分別對應着上面的一個segment,兩個LOAD類型的segment分別對應着02和03,可以看到每個LOAD類型的segment裏面都包含了許多的section。

ELF將相同或者相似屬性的section合併爲一個segment並映射到一個VMA中,是爲了減少頁面內部碎片,以節省內存空間的使用。因爲在有了虛擬存儲機制以後,裝載的時候採用頁映射的方式。Intel系列的處理器,頁尺寸最小是4096個字節,也就是4KB。當寫的程序很小的時候,每個section可能只有幾十或者幾百個字節,如果每個section都佔用一個頁的話,對內存的浪費是海量的。所以在將目標文件鏈接成可執行文件的時候,鏈接器會盡量把相同或相似權限屬性的section分配在同一空間,在程序頭表中,將一個或多個屬性類似的section合併爲一個segment,然後在裝載的時候,將這個segment映射到進程虛擬地址空間中的一個VMA中。

ELF可執行文件與進程虛擬地址空間的映射關係

很明顯,屬性相同或相似的section會被歸類到一個segment,並且被映射到同一個VMA。

總的來說,“segment”和“section”是從不同的角度來劃分同一個ELF文件。這個在ELF文件中被稱爲不同的視圖(view),從section的角度來看ELF文件就是鏈接視圖(Linking View),從segment的角度來看就是執行視圖(Execution View)。當我們在談到ELF裝載時,段專門指segment,而在其他的情況下,段指的是section。

在實際的映射過程中,只發現有代碼段映射的VMA,有數據段映射的VMA,卻沒有BSS段映射的VMA。

如果仔細觀察程序頭表,查看兩個LOAD型的segment,會發現一些映射的細節。

FileSiz表示segment在ELF文件中所佔的大小,MemSiz表示segment在進程虛擬地址空間中所佔的大小。可以發現,MemSiz比FileSiz多出了0x20個字節,十六進制的20對應的十進制是32。再來看一下這個ELF可執行文件中BSS段的大小。

可以看到,BSS段的大小正好是十進制的32,。這說明在實際映射的時候,數據段在內存中所分配的空間大小超過實際的大小,超出去的這部分空間就是BSS段,並沒有爲BSS段進行專門的映射,這就是爲什麼在查看程序頭表時,只看到了兩個LOAD類型的段,而不是三個,BSS段已經被合併到了數據類型的段裏面。

這樣做的好處就是在構造ELF可執行文件時,不需要再額外設立BSS的segment了,只需把數據segment的內存擴大,那些額外的部分就是BSS。而這部分多出的BSS空間,會被全部填充爲0 。在C語言中,沒有初始化的全局變量和一些靜態變量會被默認初始化爲0 ,這就是原因,因爲它們會被分配到BSS段上,被一次性初始化爲0。

最後我們通過一個打印變量地址的小程序進行驗證,仔細觀察沒有初始化的全局變量和一些靜態變量的線性地址。

視頻講解

這部分內容錄了一個視頻,因爲這是我第一次錄講解視頻,沒有什麼經驗,如果視頻內容有任何問題還希望各路大神指出,不勝感激。

  • 騰訊視頻:

相關文章