可執行文件的裝載與進程小結
進程的虛擬地址空間
??每個程序被運行起來之后都擁有自己獨立的虛擬地址空間,這個虛擬地址空間的大小是CPU的位數決定的。比如,32位的硬件平臺決定了虛擬地址空間的地址為(2^32-1),也就是我們常說的4GB虛擬內存的大小。
??需要注意的是,分配的4GB的虛擬空間并不是全部給進程的,比如,linux下1GB給操作系統,余下的3GB中基本上都分配給進程,但是3GB中的其中小部分要分配給其他用途;win下面按照2GB、2GB進行類似的劃分。-
裝載的方式
- 裝載的基本思想
??將程序最常用的部分駐留在內存。最常用的方法是頁映射,如下。 - 頁映射
??要完成頁映射就要將內存和磁盤中的數據和指令按“頁”為單位劃分成若干頁,以后所有的裝載和操作的單位就是頁。下圖就是可執行文件(虛擬空間)與物理內存的映射(不考慮程序運行的虛擬地址空間):
??關于頁的操作有很多種情況,比如“內存滿時的頁置換”、“頁錯誤”等等情況下采取的種種策略(FIFO、LUR)這里不再贅述。
- 裝載的基本思想
-
從操作系統的角度看可執行文件的裝載方法
- 進程的創建
??從操作系統的角度看,一個進程最關鍵的特征就是他有獨立的虛擬地址空間,這使得它有別于其他進程,上述的映射關系直接使用物理地址進行操作,那么每次頁裝入的時候就要就行重定位,所以我們需要引入進程的虛擬運行地址空間。那么,下面就說一下從操作系統角度看一個程序被執行的大致過程:
??1.首先是創建程序對應的虛擬地址空間。即進行虛擬地址空間與程序執行的物理內存的映射(方向是進程虛擬空間到進程物理內存)。我們知道一個虛擬空間由一組頁映射函數將虛擬空間各個頁映射至相應的物理空間。此處所謂的“創建”并不是創建空間,而是創建虛擬空間到物理內存空間的映射函數所需要的一系列的數據結構,對于Linux就是創建一個“頁目錄”結構即可,并不需要設置虛擬頁到物理頁的映射關系。linux下將虛擬空間的各個頁映射至相應的物理空間,實際上只是分配了一個頁目錄(Page Directory)就可以了,并且不用設置頁映射關系,這些映射關系到后面程序發生頁錯誤的時候再進行設置。
??2.讀取可執行文件頭,建立進程虛擬地址空間和可執行文件的映射關系。這一步將可執行文件空間與虛擬空間關聯起來(方向是可執行文件虛擬空間到進程虛擬空間),使得發生缺頁錯誤時,OS能夠知道到可執行文件中的哪個位置去找到所需要加載到物理內存的內容;這種映射關系只是保存在操作系統內部的一個數據結構。Linux中將進程虛擬空間中的一個段叫做虛擬內存區域(VMA,Virtual Memory Area);在Windows中將這個叫做虛擬段(Virtual Section)。
??【注意】由于可執行文件在裝載的時候實際上是被映射的虛擬空間,所以可執行文件很多時候被稱作映射文件。進程虛擬地址空間和可執行文件的映射關系如下:
??3.設置CPU的指令寄存器為可執行文件的入口地址,啟動運行:OS將控制權交給了進程。從進程的角度看這一步可以簡單的認為操作系統執行了一條跳轉指令,直接跳轉到可執行文件的入口(ELF文件頭中保存了入口地址項)。 - 頁錯誤
??完成上述三個步驟之后,其實OS僅僅只是可執行文件與進程虛存之間建立起了映射——即通常意義上所說的程序加載到了內存,實際上這里說的是程序完全加載到了虛擬內存,但是代碼和數據根本就沒有加載到物理內存中,進程虛存與物理內存空間的映射關系其實也沒有建立起來(上面也說了在“頁錯誤階段進行映射關系的設置”),這樣程序一旦開始執行,將會立即出現缺頁錯誤,即程序將要訪問的進程虛存地址并沒有映射到物理內存空間的某個page(虛擬頁),(頁錯誤的處理線程執行)此時OS會重新接管系統控制權,查詢剛才第二步保存的可執行文件到進程虛存映射關系的數據結構,找到所缺的虛擬頁對應于可執行文件中的偏移,然后在進程物理內存分配一個物理頁,將可執行文件中的內容從磁盤讀入到內存中,并將這個物理頁(進程物理內存)與該虛擬頁(進程虛存)建立起映射,然后OS將控制權重新交給進程,程序繼續執行。如下圖所示:
- 進程的創建
-
進程虛存空間分布
- ELF文件在映射到進程虛存的過程中是以系統的頁作為單位的,那么每個段在映射時的長度都應該是系統頁長度的整數倍;如果不是那么多余部分也將占用一頁。這樣的話內存浪費是大問題。
-
ELF文件中, 段的權限只有為數不多的幾種組合:
1.以代碼段為代表的權限為可讀可執行的段
2.以數據段和BSS段為代表的權限為可讀可寫的段
3.以只讀數據段為代表的權限為只讀的段。
對于相同權限的段,把它們合并到一起當作一個段進行映射。如下圖,".text"和".init"段都是可讀可執行的,則進行合并,形成一個"segment":
-
堆和棧
kernel使用VMA劃分來管理進程的虛擬地址空間。典型的進程包括代碼:
1.代碼VMA(RE屬性,有映像文件)
2.數據VMA(RWE屬性,有映像文件)
3.堆VMA(RWE屬性,無映像文件,向上擴展)
4.棧VMA(RW屬性,無映像文件,向下擴展)
如下圖所示:
【需要注意】其實DATA segment對應的就是DATA VMA;CODE segment對應的就是CODE VMA。幾乎在每一個進程的VMS視圖中都可以看見[heap]和[stack]這兩個VMA,但是這兩個VMA在可執行文件中都沒有對應的segment存在,所以它們被稱之為匿名VMA。malloc()庫函數就是從堆VMA中分配空間。
-
Linux內核裝載ELF過程
??Linux環境下,fork系統調用將會創建一個與當前task完全一樣的新task,直到應用程序調用exec*系列的Glibc庫函數最終調用execve()系統調用之后,Linux內核才開始真正裝載ELF可執行文件(映像文件)。execve內核入口為sys_execve(),隨之調用do_execve()將查找這個可執行文件,如果找到則讀取ELF可執行文件的前128個字節,然后調用search_binary_handle()通過ELF文件頭中的e_ident得到可執行文件的Magic Number,判斷出這是一個什么類型的可執行文件,并調用不同可執行文件的裝載處理程序,對于ELF可執行文件而言,其裝載處理程序為load_elf_binary(),這個函數將會把execve系統調用的返回地址修改為ELF可執行文件的入口點,對于靜態鏈接得到的ELF文件即文件頭中定義的e_entry,對于動態鏈接得到的ELF可執行文件則是動態鏈接器。一步一步返回到sys_execve()之后,因為返回地址已經被修改為了ELF程序入口地址了,所以系統調用返回到用戶態之后,EIP指令寄存器將直接跳轉到ELF程序入口地址,程序開始執行,裝載完成。
ELF文件的裝載過程:fork -> execve() -> sys_execve() -> do_execve()
do_execve() 讀取文件的前128個字節判斷文件的格式(一般根據魔數來判斷,比如elf的頭四個字節為:0x7F, e, l, f)。
??然后調用search_binary_handle()去搜索和匹配合適的可執行文件裝載處理過程,對于elf則調用load_elf_binary():- 檢查ELF可執行文件格式的有效性
- 尋找動態鏈接的“.interp”段,設置動態連接器路徑
- 根據ELF可執行文件的程序頭表的描述,對ELF文件進行映射,比如代碼、數據、只讀數據。
- 根據ELF進程環境,比如進程啟動是EDX寄存器的地址應該是DT_FINI的地址。
- 將系統調用的返回地址修改成ELF可執行文件的入口點,這個入口點取決于程序的鏈接方式,靜態ELF可執行文件為e_entry所指的地址,對于動態ELF入口點為動態連接器。
Load_elf_binary()執行完畢,返回至do_execve()再返回至sys_execve(),最后一步的系統調用返回地址改成了被裝在的ELF程序入口地址。當sys_execve()系統調用從內核態返回到用戶態時,EIP寄存器直接跳轉到了ELF程序的入口地址,新程序開始執行。