在 Xcode 中按下 command + B
不出意外的話, 就會顯示小錘子
小錘子下面是 Build Succeeded
Build 就是構(gòu)建
其實構(gòu)建是一個很復(fù)雜的過程
大致的分為 預(yù)處理 編譯 匯編 鏈接 四個步驟
最近看了 程序員的自我修養(yǎng) 這本書和一些相關(guān)的博客
整理一下構(gòu)建過程的細(xì)節(jié)
預(yù)編譯
在構(gòu)建的第一步是預(yù)編譯
預(yù)編譯做的事比較簡單, 大概是:
- 將所有的
#define刪除, 并且展開所有宏定義 - 處理所有預(yù)編譯指令, 比如
#if#ifdef#elif#else#endif - 處理
#include預(yù)編譯指令, 將被包含的文件插入到該預(yù)編譯指令位置 - 刪除所有注釋
/* */// - 添加行號和文件標(biāo)識, 以便于編譯時編譯器產(chǎn)生調(diào)試用的行號信息以及用于編譯時產(chǎn)生編譯錯誤或警告時顯示行號
- 保留
#pragma指令, 編譯器會用到他們
編譯
編譯過程就是將預(yù)處理完的文件進(jìn)行一系列操作 : 詞法分析 語法分析 語義分析 優(yōu)化并生成匯編代碼
這個過程是構(gòu)建程序最核心的部分, 也是最復(fù)雜的部分.
-
詞法分析
首先源代碼被輸入到掃描器, 掃描器進(jìn)行詞法分析, 運用一種類似有限狀態(tài)機(jī)的算法將源代碼分割成記號
例如這段代碼array[index] = index + 4會被分割成這樣 :
| 記號 | 類型 |
|---|---|
| array | 標(biāo)識符 |
| [ | 左方括號 |
| index | 標(biāo)識符 |
| ] | 右方括號 |
| = | 賦值 |
| ( | 左圓括號 |
| index | 標(biāo)識符 |
| + | 加號 |
| 4 | 數(shù)字 |
| ) | 右圓括號 |
| * | 乘號 |
| ( | 左圓括號 |
| 2 | 數(shù)字 |
| + | 加號 |
| 6 | 數(shù)字 |
| ) | 右圓括號 |
詞法分析產(chǎn)生的記號一般分為以下幾類 : 關(guān)鍵字 標(biāo)識符 字面量(數(shù)字 字符串) 特殊符號(加號 等號),
與此同時, 掃描器還完成了其他工作 : 將標(biāo)識符存放到符號表, 將數(shù)字字符串常量存放到文字表等.
-
語法分析
接下來語法分析器將對掃描器產(chǎn)生的記號進(jìn)行語法分析, 從而產(chǎn)生語法樹, 語法樹是以表達(dá)式為節(jié)點的樹 :
語法樹
圖中可以看出, 符號和數(shù)字是最小表達(dá)式, 他們不是有其他表達(dá)式組成的, 所以他們通常作為整個語法樹的葉節(jié)點.
在語法分析的同事, 很多運算符號的優(yōu)先級也被定義下來, 比如乘法優(yōu)先級比加法要高, 還有一些符號有多重含義, 比如*既可以代表乘法也可以代表對指針取內(nèi)容, 語法分析階段會對這些內(nèi)容進(jìn)行區(qū)分, 如果出現(xiàn)不合法的表達(dá)式, 編譯器會報錯. -
語義分析
語義分析由語義分析器完成, 語法分析僅是對表達(dá)式的語法層面的分析, 但是它并不了解這個語義是否真正有含義, 比如 C 語言里面兩個指針做乘法運算是沒有意義的, 但是這個語法卻是合法的.
編譯器能分析的是靜態(tài)語義, 也就是能在編譯期就確定的語義, 通常包括聲明和類型的匹配, 類型的轉(zhuǎn)換. 與之對應(yīng)的是動態(tài)語義, 就是在運行期才能確定的語義, 比如在運行時將 0 作為除數(shù)是不合法的.
經(jīng)過語義分析后, 語法樹的表達(dá)式被標(biāo)識了類型 :
語義分析后的語法樹 -
代碼優(yōu)化
現(xiàn)在的編譯器有著很多層的優(yōu)化, 往往在源碼級就會有一個優(yōu)化過程, 由源碼級優(yōu)化器完成, 比如 (2+6) 這個表達(dá)式, 在編譯器就可以被確定, 生成如下語法樹 :

其實直接在語法樹上做優(yōu)化比較困難, 所以源碼優(yōu)化器往往把整個語法樹轉(zhuǎn)換成中間代碼, 他是語法樹的順序表示, 已經(jīng)非常接近目標(biāo)代碼, 但是他一般跟目標(biāo)機(jī)器和運行時環(huán)境是無關(guān)的, 比如他不包含數(shù)據(jù)的尺寸, 變量地址和寄存器的名字等. 中間代碼使編譯器被分為前端和后端, 編譯器前端負(fù)責(zé)生產(chǎn)與機(jī)器無關(guān)的中間代碼, 編譯器后端將中間代碼轉(zhuǎn)換成目標(biāo)代碼. 這樣對于一些可以跨平臺的編譯器而言, 他們可以針對不同的平臺使用一個前端數(shù)個后端.
-
目標(biāo)代碼生成
源代碼優(yōu)化器產(chǎn)生中間代碼后的過程屬于編譯器后端, 主要包括代碼生成器和目標(biāo)代碼優(yōu)化器.
代碼生成器將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器代碼, 這個過程十分依賴于目標(biāo)機(jī)器, 因為不同的目標(biāo)機(jī)器有著不同的字長, 寄存器, 整數(shù)數(shù)據(jù)類型和浮點數(shù)據(jù)類型等.
目標(biāo)機(jī)器代碼再由目標(biāo)代碼優(yōu)化器進(jìn)行優(yōu)化, 比如選擇合適的尋址方式, 使用位移來代替運算, 刪除多余指令等.
鏈接
經(jīng)過 掃描 語法分析 語義分析 源代碼優(yōu)化 目標(biāo)代碼生成 目標(biāo)代碼優(yōu)化 這一系列操作, 源碼被編譯成了目標(biāo)代碼, 但是目標(biāo)代碼有一個問題, index 和 array 的地址還沒有確定, 如果 index 和 array 和源代碼在同一個編譯單元, 那么編譯器可以為他們分配空間, 如果定義在別的程序模塊就沒辦法了.
現(xiàn)在程序的代碼規(guī)模往往很大, 所以每個程序會被分為多個模塊, 這樣做的好處是每個模塊之間相互依賴又相互獨立, 而且模塊可以單獨開發(fā)編譯測試, 便于重用. 但是隨之而來的問題就是模塊之間怎么通信, 模塊之間的通信包括函數(shù)的調(diào)用和變量的訪問, 函數(shù)的訪問需要知道函數(shù)的地址, 變量的訪問需要知道變量的地址.
-
模塊拼裝 --- 靜態(tài)鏈接
我們把每個源代碼模塊獨立的編譯, 然后將他們組裝起來, 這個組裝的過程就叫鏈接, 連接過程包括了地址分配, 符號決議和重定位.
模塊間的通信是地址的相互訪問, 解決這個問題的方式就是模塊間符號的引用, 模塊中符號表分為已定義符號集合D,和一個未定義符合集合U, 未定的符號將引用其他模塊中的符號.
在連接的過程中, 每個模塊會去其他模塊中尋找自己未定義的那些符號的定義, 這個過程就是符號決議.
在未找到符號符號之前, 模塊先把這個未定義符號的地址置為 0, 當(dāng)在其他模塊中找到了該符號的定義的時候, 會重新給這個符號賦值地址, 這個過程就是重定位.
靜態(tài)鏈接的進(jìn)本過程和作用 : 比如在程序 main.c 模塊中使用另一個模塊 func.c 中的函數(shù) foo(). 我們再 main.c 模塊中調(diào)用 foo 的時候必須知道 foo 這個函數(shù)的地址, 但是由于每個模塊是單獨編譯的, main.c 編譯的時候并不知道 foo 函數(shù)的地址, 所以他暫時把這個指令擱置(地址置 0), 等到最后連接的時候由連接器將指令的目標(biāo)地址修正, 如果沒有連接器, 我們需要手動修正 foo 的地址, 而且每次編譯后地址可能會改變. 連接器在連接的時候會根據(jù)所引用的符號 foo, 自動取相應(yīng)的 func.c 模塊查找 foo 的地址, 然后將 main.c 模塊中所引用的 foo 指令進(jìn)行重定位,

