跟濤哥一起學嵌入式 20:一段C語言代碼編譯、運行全過程解析

很多嵌入式初學者,不明白一個簡單的C語言程序,是如何通過一步步編譯、運行變成一個可運行的可執行文件的,程序到底是如何運行的?運行的過程中需要什麼環境支持?

今天就跟大家一起捋一捋這個流程,搞清程序編譯、鏈接、加載、運行的整個脈絡,以及在運行過程中的內存佈局、堆棧變化。

1. 程序的編譯、鏈接過程

就以hello.c爲例:從一個C語言源文件,到生成最後的可執行文件,基本流程如下;

  1. C 源文件: 編寫一個簡單的helloworld程序
  2. 預處理:生成預處理後的C源文件 hello.i
  3. 編譯:將C源文件翻譯成彙編文件 hello.s
  4. 彙編:將彙編文件彙編成目標文件 hello.o
  5. 鏈接:將目標文件鏈接成可執行文件

 

v2-68ecff5a17655eccc352a250668349b7_b.jpg

 

v2-066a2052f74294700bd07ca2b08b887e_b.jpg

爲了加深對這個過程的理解,我們可以在Linux環境下面,通過gcc命令精確控制每一個編譯、鏈接過程

        $  gcc  -E  hello.c  >  hello.i           //會生成預處理後的C源文件hello.i
$  gcc  -S  hello.i                       //將hello.i編譯成彙編文件hello.s
$  gcc  -c  hello.s                       //將彙編文件hello.s彙編成hello.o
$  gcc hello.o  -o hello                  //將目標文件鏈接成可執行文件hello
$  ./hello                               // 運行可執行文件hello
      

2. 程序的執行過程

當我們在shell交互環境下敲擊 $ ./hello,這個hello程序到底是怎麼運行的呢?

很簡單。shell會首先通過系統調用fork創建一個子進程,然後從磁盤上將可執行文件hello的代碼段、數據段加載(map)到這個子進程的地址空間內,接下來,在操作系統調度器的調度下,各個進程輪流佔用CPU,就可以直接執行了。

 

v2-e7d087cdfb2ca4dc76b27c925da1d11b_b.jpg

在操作系統層面,對於每一個進程,在內核中都會有一個task_struct的結構體來描述它,裏面存儲進程的各種信息,各個結構體構成一個鏈表,操作系統通過調度器來輪流執行每個進程,如上圖所示。

3. 進程的虛擬空間和物理空間

每個進程使用的都是虛擬地址,地址空間0~4G,都是相同的。但是CPU在實際執行過程中,對於每個進程相同的虛擬地址,會映射到物理內存中的不同位置。每個進程都有自己的進程頁表,在這個頁表裏有該進程虛擬地址和物理地址的對應關係。

 

v2-42b9f205aa398b39ff7c2201d47fe79f_b.jpg

CPU內部有一個叫MMU的硬件部件會根據這個映射關係,直接將虛擬地址轉換成物理地址,如下圖所示。

 

v2-8097d92d89b5f46771d0754001f06908_b.jpg

使用虛擬地址的好處之一就是:爲每個進程提供一個獨立的、私有的物理地址空間,保護每個進程的空間不會被其它進程破壞。同時通過MMU對內存讀寫權限進行管理、保障系統的安全運行。如下圖所示,每個進程在我們的物理內存(DDR)上,都有各自獨立的內存空間:一個進程崩潰了,一般情況下,不會影響到系統,不會影響到其它進程的運行。

 

v2-ac19f225d5462440b75a2dd27bcd327b_b.jpg

4. 進程棧

棧是C語言運行的基礎。沒有棧,C語言函數是無法運行的:這是因爲函數調用過程中的返回地址、參數傳遞、函數內的局部變量都是在棧中存儲的,沒有棧,C語言函數就無法運行。

Linux進程中的代碼也是由一個個函數組成的,所以在運行進程之前,我們要首先初始化棧,如下圖所示:

 

v2-51748c3f41ce1bfb06e69574b486af4a_b.jpg

在程序運行過程中,通過棧指針,我們就可以將函數內的局部變量、返回地址保存在棧中。隨着函數不斷地調用、函數退出,而不斷地入棧、出棧。

棧是一種數據結構,CPU的寄存器一般來講,在設計的時候,會自動入棧出棧、自動增減棧的地址。比如ARM中的入棧出棧操作,當我們使用push/pop入棧出棧的時候,CPU的寄存器SP,即棧指針會自動增減地址,一直指向棧頂,這些都是指令集的實現,即CPU內部硬件電路的實現。關於棧的進一步解釋,可以看看我以前的回答:

 

5. 用戶棧、內核棧、中斷棧

在Linux環境下,進程一般分爲兩種模式,用戶態和內核態。甭管是什麼態,只要你是C語言,運行C代碼就必須指定棧,否則C代碼就無法運行。所以棧又分爲用戶棧和內核棧。

用戶棧的虛擬地址空間在用戶空間,內核棧的地址在內核空間。它們都是虛擬地址,最後通過MMU映射到物理內存的不同區域。

有時候,你還會看到中斷棧的字眼,千萬別被它嚇到。中斷程序、中斷函數也是C語言,也是妖他媽生的,想運行中斷處理程序也必須需要棧的支持,一般這種棧叫做中斷棧。它可以使一個獨立的中斷棧,也可以佔用進程棧的空間,跟進程棧共享。

6. 小結

以上只是簡單介紹一下一個C語言從編譯、鏈接、運行、到進程創建、內存堆棧的大致流程。實際過程比這個更復雜一些、更深一些,限於篇幅的關係,很多細節無法一一細講。

以上文字和圖片,是根據《C語言嵌入式Linux高級編程》視頻教程改編而成。

 

 

發佈了92 篇原創文章 · 獲贊 38 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章