C語言Hello World 第二重

本節將為你揭開程序從文本文件到輸出這個過程的神秘面紗。


1. 源碼文件到可執行文件

Paste_Image.png

通過預處理、編譯、匯編、鏈接四個步驟,生成了a.out這個默認可執行目標文件,要想在Unix系統上執行該文件,在shell中輸入

unix> ./a.out

Shell是一個命令解釋器,它是與系統內核交互的窗口。
當前輸入命令不是系統內置命令,Shell就會假設這是一個可執行文件,它將加載并運行這個文件。最終的效果就是Shell會打印出“Hello World”。

補充:Linux的目標文件格式有4種,分別是** a.out COFF PE ELF **

2. 硬件體系中加載、執行a.out的過程

下面在系統的整體硬件組成架構中,簡單分析下“Hello World”從文本文件到屏幕上輸出的過程:

Paste_Image.png
Paste_Image.png
Paste_Image.png
Paste_Image.png

3. 從軟件層來剖析a.out的執行流程

當Shell加載并運行a.out的時候,并不是直接操作鍵盤、顯示器、磁盤或者主存,而是依靠操作系統提供的服務。操作系統可以理解為應用程序和硬件之間的一層軟件:

Paste_Image.png

操作系統提供兩個基本功能:

  • 防止硬件被失控的程序濫用
  • 向應用程序提供簡單一致的機制來控制復雜而通常大相徑庭的低級硬件設備

操作系統通過幾種抽象概念來實現這兩個功能--進程、虛擬存儲器、文件。

Paste_Image.png

3.1 進程

當a.out被加載到內存執行的時候,操作系統會提供一種假象,好像系統只有這一個程序在執行,并且獨占處理器、主存和I/O設備。這個假象是通過進程的概念來實現的。

進程是操作系統對一個正在運行的程序的一種抽象。
進程的地址空間模型如下圖:

Paste_Image.png

實際上系統中會有多個進程,它們通過上下文(進程運行所需的所有狀態信息)切換交替獲取CPU執行時間片,直到執行結束,因為在單處理器系統中,任意時刻只能執行一個進程代碼。

以a.out在Shell中執行為例:

Paste_Image.png

此時有兩個并發進程 Shella.out,開始只有Shell進程在運行,即等待用戶的命令行輸入。當輸入./a.out命令,并敲擊回車,Shell發現不是內置命令,會認為是一個可執行文件。此時會調用fork()拷貝父進程,并在子進程中調用execve()系統調用,刪除子進程現有的虛擬存儲器段,并創建新的代碼、數據、堆和棧段。新的堆和棧初始化為零。通過虛擬地址空間映射到可執行文件頁的大小片段。執行a.out這個進程,完畢后,控制權交還給Shell進程。

在此詳細闡述一下上面黑體字的過程:

  • fork
    當fork()被當前進程調用,內核為新進程分配一個全局唯一的PID,并對父進程做一個原樣拷貝。既然是拷貝父進程,那進程如何擁有獨立的進程空間呢?*原因是當fork調用中,雖然是父進程的原樣拷貝,但是它將兩個進程中的每個頁面都標記為只讀,并將兩個進程中的每個區域結構都標記為私有的寫時拷貝,當這兩個進程中的任一一個發起寫操作時,寫時拷貝機制就會創建新的頁面,因此,也就為每個進程保持了私有的地址空間抽象概念。 *
    同樣的用一幅圖來表述寫時拷貝機制:
  1. 首先兩個進程都映射了私有的寫時拷貝對象


    Paste_Image.png
  2. 比如當子進程進行寫操作時,寫時拷貝機制生效,新的頁面創建

Paste_Image.png
  • execve
    上面黑體字部分可以用一幅圖來表述:


    Paste_Image.png

    execve()函數在當前進程中加載并運行包含在可執行目標文件a.out中的程序,用a.out程序有效替代了當前程序。加載并運行a.out需要以下幾個步驟:

  1. 刪除已存在的用戶區域。刪除當前進程虛擬地址的用戶部分中的已存在的區域結構。
  2. 映射私有區域。為新程序的文本、數據、bss和棧區域創建新的區域結構。所有這些新的區域都是私有的、寫時拷貝的。文本和數據區域被映射為a.out文件中的文本和數據區。bbs區域是請求二進制零的,映射到匿名文件,其大小包含在a.out中。棧和堆區域也是請求二進制零的,初始長度為零。上圖Linux運行時存儲器映像 概括了私有區域的不同映射。
  3. 映射共享區域。如果a.out程序與共享對象(或目標)鏈接,比如標準庫C庫libc.so,那么這些對象都是動態鏈接到這個程序的,然后再映射到用戶虛擬地址空間中共享區域內。
  4. 設置程序計數器(PC)。execve做的最后一件事就是設置當前進程上下文中的程序計數器,使之指向文本區域的入口點。
    下一次調度到這個進程時,它將從這個入口點開始執行。Linux講根據需要換入代碼和數據頁面。

3.2 虛擬存儲器

要理解虛擬存儲器,需要先明白三個概念:物理地址、虛擬地址地址空間

  • 物理地址
    計算機的主存被組織成一個由M個連續的自己大小的單元組成的數組,每個字節都有一個唯一的物理地址。

    Paste_Image.png

    上圖所示是直接從地址4開始讀取4個字節(比如32位系統中的一條指令)。
    上圖中的物理地址空間大小是2的M次方。

  • 虛擬地址
    虛擬地址是CPU生成的,這個地址在被傳送到存儲器之前,會先轉換成物理地址。CPU芯片上有一個 存儲器管理單元(Memory Management Unit, MMU) 專門負責地址翻譯。

    Paste_Image.png

    虛擬地址空間的大小是2的N次方,由系統的的地址總線條數決定,或32,或64位。

那為何要使用虛擬存儲器呢?
想像一下,如果每個進程直接使用物理存儲器,共享主存會帶來存儲器管理的各種挑戰,比如進程增多需要的存儲器就越大,進程之間的相互讀寫對方的地址空間,引起程序運行的失敗等等。

為了更加有效地管理存儲器并且少出錯,現代系統提供了對主存的抽象概念--虛擬存儲器。
虛擬存儲器是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互,它為每個進程提供一個大的、一致的和私有的地址空間。
虛擬存儲器提供了3個重要的能力:

  • 它將主存看成是一個存儲在磁盤上的地址空間的告訴緩存,在主存中只保存活動區域,并根據需要在磁盤和主存之間來回傳送數據,通過這種方式,它高效地使用主存。*
    此處與cache有個相似的特點,就是程序的局部性決定了這種方式的有效性。而且主存與磁盤之間還有個交換區(swap),它用來存放被替換出來的頁面。*
  • 它為每個進程提供了一致的地址空間,從而簡化了存儲器管理。
  • 它保護了滅個進程地址空間不被其它進程破壞。

虛擬存儲器雖然對程序員是透明的,也就說程序員不必關心虛擬器的存在,它會默默地為你工作。但是我們也要明白虛擬存儲器的細節與原理,因為 虛擬存儲器貫穿了程序執行的流程;虛擬存儲器提供了強大的功能,比如它能幫助你理解NIO;幫助你理解段錯誤為何會發生。

虛擬存儲器很復雜,具體細節可以參考 操作系統概念深入理解計算機系統 這兩本書。

3.3 文件

文件就是存放在磁盤上的實體,比如hello.c就是磁盤上的一個文本文件,a.out就是磁盤上的二進制文件。程序在執行過程中,虛擬存儲器機制會根據需要從磁盤上的文件加載所需的數據。
一個進程可以打開多個文件,并且持有它們的文件句柄。

這第二重境界,相信能幫助你揭開一個程序從文本文件到屏幕上輸出這個過程的神秘面紗。

參考資料:

  1. 操作系統概念第7版
  1. 深入理解計算機系統
  2. Linux內核剖析0.12
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容