你是否有過這樣的疑問:我們編寫的源代碼,經過了現代強大IDE的處理,最終生成了可執行文件,那麼集成開發環境到底執行了哪些具體的動作,經歷了哪些過程生成了可執行文件呢?帶着這個疑問,我們一起來看一看。
從源代碼到可執行文件,中間經歷了這幾個階段:預處理、編譯、彙編、鏈接。
預處理主要的工作爲:
以linux爲例,通過gcc -E a.c -o a.i即可生成預處理後的文件,通過查看預處理後的文件可以定位源碼中包含的宏定義引用、頭文件插入是否正確。源文件經過預處理,做完準備工作後就可以進行編譯了。
編譯階段是整個目標文件生成過程中相對較複雜的階段,編譯階段需要進行將源碼進行掃描、語法分析、語義分析、源代碼優化、目標代碼生成、目標代碼優化這些步驟,最終生成彙編文件。
首先我們來看一看掃描的過程:掃描主要是將源代碼中的標識符、關鍵字、字面量(包含數字、字符串等)、特殊符號(如加號、乘號、等號等)輸入到掃描器,掃描器根據不同用戶設定的語法規則進行源碼符號的掃描。
完成源碼掃描之後,語法分析器就會分析掃描器中的記號,從而形成「語法樹」(由語言分析器產生的以表達式爲節點的樹)。比如:a = (2 + 1) * c這個表達式,將會生成如下語法樹:
我們可以看到,整個語法樹是一個賦值表達式,標識符變量和數字不能由其他的表達式組成,所以作爲葉子節點,左子樹只有一個葉子節點,右子樹中有加法表達式和乘法表達式。在語法分析階段,大多數的語法規則優先級及含義就被確定下來了。當語法分析器發現掃描其中的一些非法規則時(比如括號不匹配、表達式中缺少操作符等)就會報編譯錯誤。
語法分析完後就是語義分析,語義分析主要是進行聲明和類型的匹配、類型轉換。在語法分析階段,語法分析器只能確定這個表達式有沒有語法錯誤,而不會進行語義判斷,比如兩個地址變量(指針)相乘有沒有意義。語義分析器對語法分析樹中的每個節點進行類型分析轉換及標識,進行聲明和類型匹配的分析,如果類型不匹配,語義分析器會先進行分析,比如一個浮點類型賦值給整型,語義分析器需要完成這個步驟同時進行提示;一個浮點類型賦值給一個指針就會報編譯錯誤;函數聲明和實現不一致會報編譯錯誤。可以在編譯階段確定語義的叫靜態語義,在運行階段確定語義的叫動態語義,比如除0就是動態語義錯誤。
接下來就是源代碼優化,現代編譯器有許多不同層次的優化,比如某些變量在多次訪問時爲了訪問速度的提升,會暫時放在寄存器中不進行回寫;在編譯階段可以確定某些表達式值的就會直接使用值來簡化運算等等。上面我們提到的表達式a = (1 + 2) * c就會直接被優化爲a = 3 * c;
源代碼優化完成後需要生成目標代碼和目標代碼優化。生成目標代碼就是代碼生成器將經過上述步驟後的代碼翻譯成彙編代碼,這個過程非常依賴目標機器(不同的機器有不同的指令集、字長、寄存器等)。代碼生成器生成彙編代碼後,目標代碼優化器會將目標代碼進行優化,比如選擇合適的尋址方式、刪除多餘指令、使用更簡便的方式替換運算方式等。
使用gcc -S a.i -o a.s可以完成上述步驟。
彙編器的工作其實比較簡單,就是將編譯通過後的目標代碼通過一定的規則翻譯成機器代碼。但是有個問題,比如在表達式中引用了其他文件中的變量或者調用了其他文件中的函數,在彙編階段是無法確認這些變量或函數的地址的,所以在編譯階段會將引用其他地方的變量或函數地址填充一個隨機值(一般爲0),然後在鏈接過程中更新所有引用這些變量或函數的地方。
使用gcc -c a.s -o a.o來完成彙編。
現代程序語言設計中,基本上程序都是分層分模塊設計的,所以在本模塊中應用其他模塊的變量/函數非常常見。那麼鏈接器怎樣做到確定這些符號的地址呢?
最早的鏈接器是通過人工計算相應的地址偏移,然後進行填充,但這個對於程序員來說簡直不能忍受,於是人們使用符號標識來表示對應的變量及代碼段,在變量或函數在多個模塊間引用時,可以通過符號來標識地址跳轉,最後由鏈接器來更新這些標識符的地址。所以鏈接器的主要工作就是地址和空間分配、符號決議(符號標識和地址對應)和重定位(符號標識地址修正更新,重定位)。
使用gcc a.o b.o -o a.out(不指定輸出文件名則默認爲a.out)完成目標文件與庫鏈接生成可執行文件的過程。
好了,編譯和鏈接的簡單介紹就到這裏了,如果有什麼不對的或者大家感興趣的內容歡迎留言評論哦。