"hello world"可以說是所有程序員閉着眼睛都能寫出來的代碼:
#include <stdio.h> int main() { printf("hello world\n"); return 0; }
編譯運行一氣呵成。而每當有人問起:從源碼到可執行程序有哪些步驟,大多數程序員面對這個問題也能脫口而出:預處理(Prepressing)、編譯(Compilation)、匯編(Assembly)和鏈接(Linking)。
不過很多人不了解其中都做了哪些處理,今天就帶大家來好好聊一聊。
預編譯
處理的第一步,是將源碼文件.c和頭文件.h編譯成一個.i文件。
預編譯過程主要是做了以下一些工作:
將所有的#define刪除,展開所有的宏定義
處理條件編譯指令,比如#ifdef、#ifndef、#if、#endif等等
處理#include指令,將頭文件插入到該指令的位置。這個過程是遞歸的,也就是說被包含的頭文件還可能包含其他頭文件
刪除所有注釋
添加行號和文件名標識,比如# 2 "main2.c" 2 ,以便於編譯產生錯誤或者警告的時候能夠顯示行號以及編譯器產生調試用的行號信息
保留所有的#pragma編譯器指令,因為編譯器要使用它們。#pragma的作用是設定編譯器的狀態或者是指示編譯器完成一些特定的動作
編譯
編譯過程就是將預處理完的文件進行詞法分析、語法分析、語義分析和優化后產生相應的匯編代碼文件。
詞法分析
詞法分析主要使用詞法分析器(也叫掃描器),將源代碼的字符序列分割成一系列的符號(Token)。比如如下一段程序:
int array = (index + 4) * 2;
經過掃描以后,產生11個記號:
int 關鍵字
array 標識符
= 賦值操作符
( 左小括號
index 標識符
+ 加號
4 數字
) 右小括號
* 乘號
2 數字
; 語句結束
語法分析產生的記號一般可以分為:關鍵字,標識符,字面量(包括數字和字符串等)和特殊符號(加號減號等)。在識別記號的同時,掃描器也完成其他工作,比如講標識符存放到符號表,講數字字符串常量存放到文字表,以備后面的步驟使用。
語法分析
接下來語法分析器將對由掃描器產生的記號進行語法分析,從而產生語法樹。整個分析過程采用了上下文無關語法的分析手段。
語義分析
這個階段由語義分析器來完成。語法分析僅僅完成了對表達式的語法層面的分析,他並不了解這個語句是不是真的有意義。比如兩個指針相乘是沒有意義的,但是在語法上是合法的。
編譯器可以分析的語義是靜態語義,即在編譯器就可以確定的語義;與之對應的是動態語義,即在運行期才可以確定的語義。
靜態語義通常包括聲明和類型的匹配,類型的轉換。比如一個浮點型表達式賦值給整形表達式的時候,語義分析會完成浮點型到整形的轉換。動態語義使之運行期出現的語義相關問題,比如除數是0的時候會報運行期語義錯誤。
源代碼優化
現代編譯器有很多層的優化,往往在源代碼級別會有一個優化過程。源代碼優化器會在源碼級別進行優化,比如一行代碼:
array[index] = (index + 4) * (2 + 6);
在這行代碼中,(2+6)這個表達式就可以被優化掉,因為他的值在編譯器就可以確定。
在進行了語法分析和語義分析階段的工作之后,有的編譯程序將源程序變成一種內部表示形式,這種內部表示形式叫做中間語言或中間表示或中間代碼。所謂“中間代碼”是一種結構簡單、含義明確的記號系統,這種記號系統復雜性介於源程序語言和機器語言之間,容易將它翻譯成目標代碼。
中間代碼使得編譯器可以分為前端和后端,前端負責產生機器無關的中間代碼,編譯器后端將中間代碼轉換成目標機器代碼。這樣對於一些跨平台的編程語言,他們可以針對不同平台使用同一個前端和針對不同平台的數個后端。
目標代碼生成與優化
源碼級優化器產生中間代碼標志着下面的過程都屬於編輯器的后端。編譯器后端主要包括代碼生成器和目標代碼優化器。代碼生成器將中間代碼轉換成目標機器代碼,然后目標代碼優化器進行代碼優化,比如選擇合適的尋址方式、食用為宜來代替乘法運算,刪除多余的指令。
經過掃描、詞法分析、語法分析、語義分析、源代碼優化、代碼生成和目標代碼優化,源代碼終於被編譯成了目標代碼。但是現在還有一個問題:目標代碼中有的變量定義在其他模塊,我們該怎么辦?
事實上,定義在其他模塊的變量和函數在最終運行時的絕對地址都要在鏈接的時候才能確定。所以現代編譯器可以將一個源代碼文件編譯成一個未鏈接的目標文件,然后由鏈接器最終將這些目標文件鏈接起來形成可執行文件。
鏈接
暫時挖個坑