linux上,C代碼被編譯鏈接成可執行文件後,被載入運行的過程具體是怎樣的?

《深入理解計算機系統》一書在提到載入器是如何工作時講到「……父進程生成一個子進程,它是父進程的一個複製品。子進程通過execve系統調用啟動載入器。載入器刪除子進程現有的虛擬存儲器段,並創建一組新的代碼、數據、堆和棧段。新的棧和堆段被初始化為零。通過將虛擬地址空間中的頁映射到可執行文件的頁大小的片(chunk),新的代碼和數據段被初始化為可執行文件的內容。最後,載入器跳轉到_start地址,它最終會調用main函數。除了一些頭部信息,在載入過程中沒有任何從磁碟到存儲器的數據拷貝。直到CPU引用一個被映射的虛擬頁才會進行拷貝,此時,操作系統利用它的頁面調度機制自動將頁面從磁碟傳送到存儲器。」

我的問題是:為何直到CPU引用一個被映射的虛擬頁時才會進行拷貝,那之前載入時新的代碼和和數據段被初始化為可執行文件的內容是怎麼進行的?因為可執行文件不是一直在磁碟上的嗎?


linux 2.4的execve內核實現

通過open打開目標文件,創建了一個file對象表示此文件

接著用一個linux_bin_prm結構來存儲可執行文件的路徑, 運行的參數和環境變數等

從可執行文件讀取128位元組(保存類似elf相關信息)放在bprm的緩衝區中,運行所需的參數和環境變數等也已經搜集在bprm.

接著Linux內核對其所支持的每種可執行的程序類型都有struct linux_binfmt.根據讀取到的128位元組(elf)

判斷文件格式,執行不同的load_binary函數,比如假設文件時a.out.執行的就是load_aout_binary

然後根據elf(也就是讀取的128位元組),獲取代碼段,數據段,bss段的位置,另外調用do_brk為代碼段與數據段分配虛擬地址

然後就把這兩部分從文件中讀到指定的位置(do_brk)進來,這樣就有了vm_struct結構,對應的頁目錄表項,頁表項,頁面,而且頁面已經放入了文件中的代碼段和數據段

set_brk函數為可執行文件的bss段分配空間並建立起頁面映射

在用戶空間的堆棧區頂部為進程建立起一個虛擬區間,並將執行參數以及環境變數所佔的物理頁面與此虛擬區間建立起映射,設置系統堆棧中的eip,esp,返回就是entry開始執行了


虛擬內存和頁面文件功能是CPU提供的,跟載入進程沒有任何關係。總的來講無非就是

1、讀進內存

2、運行某個固定名字的函數初始化一大堆你也不知道它存在的全局變數

3、找到unresolved external symbol就去看看其他你引用了的靜態鏈接庫裡面有沒有,沒有就爆炸


載入時只是做了一個虛擬空間和可執行文件之間的映射,並沒有實際分配物理內存,更沒有從磁碟讀取文件內容到內存。代碼段,數據段,堆,棧等這時候都只是存在於虛擬空間。程序開始執行後,先訪問虛擬空間的代碼段,操作系統的內存管理單元(MMU)通過TLB和Page Table查找虛擬頁面(Page)所對應的物理內存頁框(Page Frame)時,發現還沒有對應的物理頁框,也就是發生了缺頁中斷(Page Fault),這時候操作系統才會分配物理內存,並將程序的可執行文件讀入內存中,開始執行程序代碼。堆棧中的內存也是在需要訪問的時候,發生Page Fault,然後才分配物理內存。這樣做好處很明顯,需要用到的時候才分配物理內存,可以節省很多不必要的浪費。我做過一個實驗,用perf觀察缺頁中斷的次數,int numbers[N],然後for循環給numbers[i](0&<=i&<=N)賦值,當N比較小的時候,每次缺頁中斷的時候只分配一個物理頁框,這樣就會頻繁的缺頁,當N到了一個臨界值的時候,會一次性分配多個物理頁框,就減少了缺頁中斷次數,提升程序效率。


&>&>那之前載入時新的代碼和和數據段被初始化為可執行文件的內容是怎麼進行的

映射就算是初始化了,沒有讀代碼和數據內容。內核從可執行文件讀取映射所需的內容即可,也就是各段的虛擬地址及其在執行文件中的偏移。初始的映射,只有操作系統內核必須理解,cpu的虛存機制則不一定知道多少,這依賴體系,要安排成對每個頁面訪問時失敗併產生異常,此時內核根據異常信息確定訪問失敗的頁面,再從文件中讀取該頁內容,修改mmu 映射,再異常返回,重啟失敗的指令即可。


哎呀作者都叫你別心急了。

Our description of loading is conceptually correct, but intentionally not entirely accurate. To understand how loading really works, you must understand the concepts of processes, virtual memory, and memory mapping, which we haven』t discussed yet. As we encounter these concepts later in Chapters 8 and 9, we will revisit loading and gradually reveal the mystery to you.

硬碟上的可執行文件中有一一對應的segment能被載入到內存中相應的位置。那麼初始化就只要打一串映射表,記錄下硬碟中可執行文件的每個部分的地址和載入內存後的地址,就算初始化完了。

現代操作系統都有虛擬內存機制,它能讓你看上去每個進程是獨享所有內存的。既然是獨享的,初始化以後程序所用的內存就該都是空的,等你讀到哪部分的代碼段或者數據的時候,再把它所在的頁拷貝進內存也不遲。就像買下毛坯房後,你會在設計圖上畫好每個地方放什麼傢具,但擺在你面前仍然是個空房子,什麼時候搬傢具就看心情了。

這個解釋並不嚴謹,只希望題主能夠理解為什麼可執行文件還躺在硬碟上,程序就可以執行了。


先佔坑


mark一下,等下來答(如果我寫的一些內容有問題的話,請各位大神跟我說下,我只看過一些簡單內核的源碼,不知道在比較完整的內核中實現的機制是否一樣,謝謝)

先來解釋一下這段話。

父進程生成一個子進程,它是父進程的一個複製品

父進程通過fork()複製了父進程的pcb(進程式控制制模塊)並且生成了一個子進程的id,這個時候,pcb中的信息依然還是父進程的,包括指向頁目錄的cr3。以及pcb中保存的寄存器的上下文信息。

子進程通過execve系統調用啟動載入器。載入器刪除子進程現有的虛擬存儲器段,並創建一組新的代碼、數據、堆和棧段。新的棧和堆段被初始化為零。

然後子進程通過execve系統調用來給子進程分配資源。比如頁目錄(簡單的內核中,所有的進程可以共享一個頁目錄,也可以調用存儲器管理分頁一個頁面來複制父進程的頁目錄。並且初始化頁目錄的一部分。為什麼要複製父進程的頁目錄?),GDT、LDT、任務表,還要分配內存地址。

分配了資源之後,就要把一些控制代碼運行的信息記錄在PCB或者任務表中,比如GDT選擇子,任務表的選擇子、LDT選擇子、指向頁目錄的地址等等。 還有就是要在PCB的寄存器信息部分填寫程序入口地址,數據段地址,棧段地址。這些信息在程序文件的頭部(應該是在程序編譯過程中生成的),然後文件系統在載入文件的時候,會先把文件放在緩衝區,cpu只要直接在緩衝區直接讀取程序頭部的信息就可以了,不用再次分配一個頁面來存放程序的可執行文件。

這個時候並沒有把程序直接載入到頁中。經過 @李晨曦大神的解說,而是在cpu讀到這一段指令的時候,發現頁目錄中沒有相應的信息,調用缺頁中斷,將程序調進內存中

ps:這一段用於頁面置換,在頁目錄項中有一個控制位,來描述頁是否在內存中。只要設置一下這個位就可以了,這樣cpu讀取指令的過程中,將線性地址轉化成物理地址的過程,只要檢測一下這個位,如果檢測到這個頁沒有在內存中,就會觸發缺頁中斷,將程序讀到內存相應的頁中

還有為什麼要這麼設置呢?這個也是我在思考的問題,希望大神幫忙解惑,謝謝


程序員的自我修養 一站式c編程。可以了解一下。


GDB你值得擁有


運行就是把代碼從硬碟複製到內存的放指令的segment去。然後存個返回地址就讓CPU去運行載入好的指令去了。然後編譯好的程序就運行了從main開始,載入static,variable然後運行main。如果main裡邊又叫了另外一個function的話,接著存一個返回地址然後跳到function去接著載入(push in)variable,運行function。運行完了之後彈出內存(英文叫pop out)CPU就讀到返回地址,然後就返回到main了。main運行完也是pop out variable 找到返回地址,返回到運行程序之前的地方CPU繼續運行程序之前的工作。電腦很傻的,黑客經常通過內存溢出改返回地址讓CPU要轉到黑客設計好的程序哪裡去。。。


推薦閱讀:

如何學習使用桌面 Linux 發行版?
Linux不是開源的嗎,為什麼RedHat的伺服器版那麼貴?
Linux 桌面應用最大的問題是什麼?
C++ 編譯期怎麼判斷一個類是不是純虛類,我有一段代碼,可以達到效果,但不知道為什麼這麼寫?

TAG:編程 | Linux | 計算機科學 | CC |