1. 程式人生 > >編譯和連結的過程

編譯和連結的過程

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

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

使用 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),然後將目標檔案和庫一起連結形成最後的可執行檔案。庫其實就是一組目標檔案的包,就是一些最常用的程式碼變異成目標檔案後打包存放。最常見的庫就是執行時庫,它是支援程式執行的基本函式的集合。