編譯和鏈接的過程

程序要運行起來,必須要經過四個步驟:預處理、編譯、彙編和鏈接。接下來通過幾個簡單的例子來詳細講解一下這些過程。

對於上邊用到的幾個選項需要說明一下。

使用 gcc 命令不跟任何的選項的話,會默認執行預處理、編譯、彙編、鏈接這整個過程,如果程序沒有錯,就會得到一個可執行文件,默認爲a.out

-E選項:提示編譯器執行完預處理就停下來,後邊的編譯、彙編、鏈接就先不執行了。

-S選項:提示編譯器執行完編譯就停下來,不去執行彙編和鏈接了。

-c選項:提示編譯器執行完彙編就停下來。

所以,這三個選項相當於是限定了編譯器執行操作的停止時間,而不是單獨的將某一步拎出來執行。

上述程序的執行過程大家應該都很熟悉了,就不浪費口舌了。

一、預處理:

       使用-E選項,表示只進行預編譯,對應生成一個 .i 文件。

預處理過程進行的操作:

  • 將所有的「#define」刪除,並且展開所有的宏定義
  • 處理所有的條件編譯指令,比如「#if」、「#ifdef」、「#elif」、「#else」、「#endif」
  • 處理「#include」預編譯指令,將被包含的頭文件插入到該編譯指令的位置。(這個過程是遞歸進行的,因爲被包含的文件可能還包含了其他文件)
  • 刪除所有的註釋「//」和「/* */」。
  • 添加行號和文件名標識,方便後邊編譯時編譯器產生調試用的行號心意以及編譯時產生編譯錯誤或警告時能夠顯示行號。
  • 保留所有的#pragma編譯指令,因爲編譯器需要使用它們。

使用一個簡單的程序來驗證一下事實是否如上述所說的一樣

編寫一個簡單的程序,然後使用-E選項執行預處理過程,打開生成的 .i 文件與源文件進行比對,結果一目瞭然

       對於給代碼加上行號這個就不在這裏演示了,我們在寫代碼的時候是不會手動添加行號的,我們看到的行號都是自己使用的編輯工具自動加上的,而這些行號編譯系統是看不到的,但是呢,我們發現如果我們哪一行的代碼出現了問題,編譯的時候就會給出提示說哪行的代碼有什麼問題,這就已經證明,編譯器是會自動添加行號的。

二、編譯:

        使用-S選項,表示編譯操作執行完就結束。對應生成一個 .s 文件。

        編譯過程是整個程序構建的核心部分,編譯成功,會將源代碼由文本形式轉換成機器語言,編譯過程就是把預處理完的文件進行一系列詞法分析、語法分析、語義分析以及優化後生成相應的彙編代碼文件。

  • 詞法分析:

        詞法分析是使用一種叫做lex的程序實現詞法掃描,它會按照用戶之前描述好的詞法規則將輸入的字符串分割成一個個記號。產生的記號一般分爲:關鍵字、標識符、字面量(包含數字、字符串等)和特殊符號(運算符、等號等),然後他們放到對應的表中。

  • 語法分析:語法分析器根據用戶給定的語法規則,將詞法分析產生的記號序列進行解析,然後將它們構成一棵語法樹。對於不同的語言,只是其語法規則不一樣。用於語法分析也有一個現成的工具,叫做:yacc。

  • 語義分析:

       語法分析完成了對錶達式語法層面的分析,但是它不瞭解這個語句是否真正有意義。有的語句在語法上是合法的,但是卻是沒有實際的意義,比如說兩個指針的做乘法運算,這個時候就需要進行語義分析,但是編譯器能分析的語義也只有靜態語義。

       靜態語義:在編譯期就可以確定的語義。通常包括聲明與類型的匹配、類型的轉換。比如當一個浮點型的表達式賦值給一個整型的表達式時,其中隱含一個從浮點型到整型的轉換,而語義分析就需要完成這個轉換,再比如,將一個浮點型的表達式賦值給一個指針,這肯定是不行的,語義分析的時候就會發現兩者類型不匹配,編譯器就會報錯。

       動態語義:只有在運行期才能確定的語義。比如說兩個整數做除法,語法上沒問題,類型也匹配,聽着好像沒毛病,但是,如果除數是0的話,這就有問題了,而這個問題事先是不知道的,只有在運行的時候才能發現他是有問題的,這就是動態語義。

  • 中間代碼生成

       我們的代碼是可以進行優化的,對於一些在編譯期間就能確定的值,是會將它進行優化的,比如說上邊例子中的 2+6,在編譯期間就可以確定他的值爲8了,但是直接在語法上進行優化的話比較困難,這時優化器會先將語法樹轉成中間代碼。中間代碼一般與目標機器和運行環境無關。(不包含數據的尺寸、變量地址和寄存器的名字等)。中間代碼在不同的編譯器中有着不同的形式,比較常見的有三地址碼和P-代碼。

       中間代碼使得編譯器可以分爲前端和後端。編譯器前端負責產生於機器無關的中間代碼,編譯器後端將中間代碼換成機器代碼。

  • 目標代碼生成與優化

代碼生成器將中間代碼轉成機器代碼,這個過程是依賴於目標機器的,因爲不同的機器有着不同的字長、寄存器、數據類型等。

最後目標代碼優化器對目標代碼進行優化,比如選擇合適的尋址方式、使用唯一來代替乘除法、刪除出多餘的指令等。

三、彙編

彙編過程調用匯編器as來完成,是用於將彙編代碼轉換成機器可以執行的指令,每一個彙編語句幾乎都對應一條機器指令。

使用命令as hello.s -o hello.o 或者使用gcc -c hello.s -o hello.o來執行到彙編過程結束,對應生成的文件是.o文件。

四、鏈接

鏈接的主要內容就是將各個模塊之間相互引用的部分正確的銜接起來。它的工作就是把一些指令對其他符號地址的引用加以修正。鏈接過程主要包括了地址和空間分配、符號決議和重定向

符號決議:有時候也被叫做符號綁定、名稱綁定、名稱決議、或者地址綁定,其實就是指用符號來去標識一個地址。

                比如說 int a = 6;這樣一句代碼,用a來標識一個塊4個字節大小的空間,空間裏邊存放的內容就是4.

重定位:重新計算各個目標的地址過程叫做重定位。

最基本的鏈接叫做靜態鏈接,就是將每個模塊的源代碼文件編譯成目標文件(Linux:.o  Windows:.obj),然後將目標文件和庫一起鏈接形成最後的可執行文件。庫其實就是一組目標文件的包,就是一些最常用的代碼變異成目標文件後打包存放。最常見的庫就是運行時庫,它是支持程序運行的基本函數的集合。