很多嵌入式初學者,不明白一個簡單的C語言程序,是如何通過一步步編譯、運行變成一個可運行的可執行文件的,程序到底是如何運行的?運行的過程中需要什麼環境支持?
今天就跟大家一起捋一捋這個流程,搞清程序編譯、鏈接、加載、運行的整個脈絡,以及在運行過程中的內存佈局、堆棧變化。
1. 程序的編譯、鏈接過程
就以hello.c爲例:從一個C語言源文件,到生成最後的可執行文件,基本流程如下;
- C 源文件: 編寫一個簡單的helloworld程序
- 預處理:生成預處理後的C源文件 hello.i
- 編譯:將C源文件翻譯成彙編文件 hello.s
- 彙編:將彙編文件彙編成目標文件 hello.o
- 鏈接:將目標文件鏈接成可執行文件
爲了加深對這個過程的理解,我們可以在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,就可以直接執行了。
在操作系統層面,對於每一個進程,在內核中都會有一個task_struct的結構體來描述它,裏面存儲進程的各種信息,各個結構體構成一個鏈表,操作系統通過調度器來輪流執行每個進程,如上圖所示。
3. 進程的虛擬空間和物理空間
每個進程使用的都是虛擬地址,地址空間0~4G,都是相同的。但是CPU在實際執行過程中,對於每個進程相同的虛擬地址,會映射到物理內存中的不同位置。每個進程都有自己的進程頁表,在這個頁表裏有該進程虛擬地址和物理地址的對應關係。
CPU內部有一個叫MMU的硬件部件會根據這個映射關係,直接將虛擬地址轉換成物理地址,如下圖所示。
使用虛擬地址的好處之一就是:爲每個進程提供一個獨立的、私有的物理地址空間,保護每個進程的空間不會被其它進程破壞。同時通過MMU對內存讀寫權限進行管理、保障系統的安全運行。如下圖所示,每個進程在我們的物理內存(DDR)上,都有各自獨立的內存空間:一個進程崩潰了,一般情況下,不會影響到系統,不會影響到其它進程的運行。
4. 進程棧
棧是C語言運行的基礎。沒有棧,C語言函數是無法運行的:這是因爲函數調用過程中的返回地址、參數傳遞、函數內的局部變量都是在棧中存儲的,沒有棧,C語言函數就無法運行。
Linux進程中的代碼也是由一個個函數組成的,所以在運行進程之前,我們要首先初始化棧,如下圖所示:
在程序運行過程中,通過棧指針,我們就可以將函數內的局部變量、返回地址保存在棧中。隨着函數不斷地調用、函數退出,而不斷地入棧、出棧。
棧是一種數據結構,CPU的寄存器一般來講,在設計的時候,會自動入棧出棧、自動增減棧的地址。比如ARM中的入棧出棧操作,當我們使用push/pop入棧出棧的時候,CPU的寄存器SP,即棧指針會自動增減地址,一直指向棧頂,這些都是指令集的實現,即CPU內部硬件電路的實現。關於棧的進一步解釋,可以看看我以前的回答:
5. 用戶棧、內核棧、中斷棧
在Linux環境下,進程一般分爲兩種模式,用戶態和內核態。甭管是什麼態,只要你是C語言,運行C代碼就必須指定棧,否則C代碼就無法運行。所以棧又分爲用戶棧和內核棧。
用戶棧的虛擬地址空間在用戶空間,內核棧的地址在內核空間。它們都是虛擬地址,最後通過MMU映射到物理內存的不同區域。
有時候,你還會看到中斷棧的字眼,千萬別被它嚇到。中斷程序、中斷函數也是C語言,也是妖他媽生的,想運行中斷處理程序也必須需要棧的支持,一般這種棧叫做中斷棧。它可以使一個獨立的中斷棧,也可以佔用進程棧的空間,跟進程棧共享。
6. 小結
以上只是簡單介紹一下一個C語言從編譯、鏈接、運行、到進程創建、內存堆棧的大致流程。實際過程比這個更復雜一些、更深一些,限於篇幅的關係,很多細節無法一一細講。
以上文字和圖片,是根據《C語言嵌入式Linux高級編程》視頻教程改編而成。