一個源程序到一個可執行程序的過程:預編譯、編譯、彙編、連接。
其中,編譯是主要部分,其中又分爲六個部分:詞法分析、語法分析、語義分析、中間代碼生成、目標代碼生成和優化。
連接中,分爲靜態連接和動態連接,本文主要是靜態連接。前端
1、預編譯:主要處理源代碼文件中的以「#」開頭的預編譯指令。處理規則見下
1.刪除全部的#define,展開全部的宏定義。
2.處理全部的條件預編譯指令,如「#if」、「#endif」、「#ifdef」、「#elif」和「#else」。
3.處理「#include」預編譯指令,將文件內容替換到它的位置,這個過程是遞歸進行的,文件中包含其餘文件。
4.刪除全部的註釋,「//」和「/**/」。
5.保留全部的#pragma 編譯器指令,編譯器須要用到他們,如:#pragma once 是爲了防止有文件被重複引用。
6.添加行號和文件標識,便於編譯時編譯器產生調試用的行號信息,和編譯時產生編譯錯誤或警告是可以顯示行號。程序員
C語言的宏替換和文件包含的工做,不納入編譯器的範圍,而是交給獨立的預處理器。
C語言中源代碼文件的文件擴展名爲.c,頭文件的文件擴展名爲.h,經預編譯以後,生成xxx.i文件。
在C++,源代碼文件的擴展名是.cpp或.cxx,頭文件的文件擴展名爲.hpp,經預編譯以後,生成xxx.ii文件。算法
2、編譯:把預編譯以後生成的xxx.i或xxx.ii文件,進行一系列詞法分析、語法分析、語義分析及優化後,生成相應的彙編代碼文件。編程
(結合程序來講明編譯的幾個步驟)
有C語言的源代碼以下:
arr[3] = (a+4)*(3+8);後端
1.詞法分析:利用相似於「有限狀態機」的算法,將源代碼程序輸入到掃描機中,將其中的字符序列分割成一系列的記號。
以上的一行C語言程序,一共有16個空字符,經掃描機掃描以後,產生了16個記號。lex能夠實現詞法分析。見下表:數組
見上圖:
詞法分析產生的記號分類有:關鍵字、標識符、字面量(數字、字符串)、特殊符號(加號、等號等)markdown
2.語法分析:語法分析器對由掃描器產生的記號,進行語法分析,產生語法樹。由語法分析器輸出的語法樹是一種以表達式爲節點的樹。上述的代碼就是
各類表達式的組合:賦值表達式、加法表達式、乘法表達式、數組表達式和括號表達式組成的複雜表達式。yacc能夠實現語法分析,根據用戶給定的規則(不一樣的編程語言對應不一樣的語法規則)對記號表進行解析。編程語言
見上圖:
整個語句被看做是一個「賦值表達式」,「=」左邊是一個「數組表達式」,右邊是一個「乘法表達式」。數組表達式又由兩個符號表達式組成,符號表達式就是最小的表達式,以後同理。模塊化
在語法分析的同時,就把運算符的優先級肯定了下來,若是出現表達式不合法,——各類括號不匹配、表達式中缺乏操做,編譯器就會報錯。函數
3.語義分析:語法分析器只是完成了對錶達式語法層面的分析,語義分析器則對表達式是否有意義進行判斷,其分析的語義是靜態語義——在編譯期能分期的語義,相對應的動態語義是在運行期才能肯定的語義。
其中,靜態語義一般包括:聲明和類型的匹配,類型的轉換,那麼語義分析就會對這些方面進行檢查,例如將一個int型賦值給int*型時,語義分析程序會發現這個類型不匹配,編譯器就會報錯。
通過語義分析階段以後,全部的符號都被標識了類型(若是有些類型須要作隱式轉化,語義分析程序會在語法樹中插入相應的轉換節點),見下圖:
這個語句中的類型都是int型,無須作轉換。
4.優化:*源代碼級別的一個優化過程*,例如該語句中的(3+8)的值能夠在編譯期肯定,源代碼優化器會將整個語法樹轉換成中間代碼——語法樹的順序表示,十分接近目標代碼。
中間代碼有不少種類型,最多見的是「三地址碼」和「P-代碼」,其中三地址碼的基本形式爲:x = y op z,表示將變量y和z進行op操做後,賦值給x,op操做能夠是加減乘除等。
經優化以後的語法樹爲:
該語句的三地址碼:
t1 = 3 + 8;
t2 = a + 4;
t3 = t2 * t1;
arr[3] = t3;
t1由數字11代替,省去t3,經優化或的三地址碼爲:
t2 = a +4;
t2 = t2 + 11;
arr[3] = t2;
另外一個關於中間代碼的要點:中間代碼使得編譯器能夠被分紅前端和後端,編譯器前端負責產生與機器無關的中間代碼,編譯器後端將中間代碼轉換爲機器代碼。
源代碼優化去產生中間代碼標誌着下面的過程都屬於編譯器後端,後端主要包括:代碼生成器和目標代碼優化器。
5.目標代碼生成:由代碼生成器將中間代碼轉換成目標機器代碼,生成一系列的代碼序列——彙編語言表示。
6.目標代碼優化:目標代碼優化器對上述的目標機器代碼進行優化:尋找合適的尋址方式、使用位移來替代乘法運算、刪除多餘的指令等。
上述的六個步驟完畢以後,編譯過程也就告一段落了。最終產生了由彙編語言編寫的目標代碼。
gcc把預編譯和編譯兩個步驟合併成一個步驟。對於C語言的代碼,是用「cc1」這個程序來完成這兩步,對於C++代碼,對應的程序爲「cc1plus」。gcc這個命令只是後臺程序的包裝,根據不一樣的參數去調用:預編譯編譯程序——cc1,彙編器——as,鏈接器——ld。
C語言的代碼,經編譯後產生的文件名爲xxx.s。
3、彙編:將彙編代碼轉變成機器能夠執行的指令(機器碼文件)。
彙編器的彙編過程相對於編譯器來講更簡單,沒有複雜的語法,也沒有語義,更不須要作指令優化,只是根據彙編指令和機器指令的對照表一一翻譯過來,彙編過程有彙編器as完成。
經彙編以後,產生目標文件(與可執行文件格式幾乎同樣)xxx.o(Windows下)、xxx.obj(Linux下)。
可是,通過預編譯、編譯、彙編以後,生成機器能夠執行的目標文件以後,還有一個問題——變量a和數組arr的地址尚未肯定。這就須要連接器來搞定啦~
4、連接:
一、歷史過程:曾經,程序猿門在編程時,使用紙帶做爲最原始的存儲設備,每當程序須要修改時,都要從新紮一條紙帶,扎孔的表示1,不扎的是0,一串串1和0就組成了各類各樣的指令——跳轉等等….
每一次的修改都很是痛苦,因此先知們就發明了彙編語言,這種編程語言方便之處在於符號的引用,表示跳轉指令再也不須要記住一串串0和1,終於可使用符號——foo來表示這個動做了!
隨着彙編語言的普及,程序的代碼量也就開始快速膨脹了,彙編語言說它也撐不住了….不過還好,高級編程語言Fortran、C、C++等一個接一個地問世,語言愈來愈方便了,追求perfect的人們就想:代碼咋寫更好呢?可不能夠把代碼按照功能的不一樣,分紅不一樣的部分,便於往後的修改和重複使用呢?
有了這個啓發,程序猿們愈來愈駕輕就熟,他們開始把代碼按照功能和性質劃分,分別造成不一樣的功能模塊,不一樣的模塊之間又按照各類結構來組織。
發展到現在,軟件的規模愈來愈大,代碼動輒數百萬行代碼,放在一個模塊那是萬萬不行的,維護起來會很是麻煩,全部如今的大型軟件每每擁有成千上萬的模塊,
模塊之間相互獨立又相互依賴。
新的問題來了,一個程序被分割成這麼多模塊,最後要怎麼把這些模塊組合造成一個單一的程序?
答案就是:模塊之間,符號的引用!
這就像是一張畫有大樹的拼圖,葉子、枝幹、根系都零散的分佈在那些拼圖碎片上,想要看到完整的大樹,咱們就會耐心地把那些碎片拼合在一塊兒。
這些模塊之間一樣如此,它們依靠那些凸起和凹陷聯繫在一塊兒,最終組合成一個完整的程序,這樣的過程稱爲——連接。
這樣基於符號的模塊化,使得連接過程在整個程序開發中顯得十分重要和突出…..
二、下面就靜態連接,進行分析。
1.連接:「組裝」模塊的過程。
2.連接的內容:把各個模塊之間相互引用的部分都處理好,使得各個模塊之間可以正確地銜接。(就像拼圖,凸起和凹槽的位置必定一一對應,不然…)
3.連接的過程:地址和空間的分配、符號決議(也叫「符號綁定」,傾向於動態連接)和重定位
以gcc編譯器爲例,看基本的連接過程:
.c文件通過編譯器、彙編器以後獲得目標文件.o,目標文件再與庫進行連接獲得可執行文件.out。
庫其實就是一組目標文件的打包,這些目標文件中都是一些經常使用的代碼。
咱們在fun.c模塊中定義了函數foo(),在main.c模塊中引用了foo()函數,在編譯過程中,編譯器並不知道main.c中foo()的地址,因此將調用foo()的指令的目標地址部分擱置,
等到了連接的階段,連接器會去找到foo()定義的那個模塊,在main.o中填入正確的函數地址,這個修改地址的過程被叫作「重定位」,每一個被修正的地方叫「重定位入口」。
以上就是一個程序從源代碼到可執行程序的大體過程,這是博主根據《程序員的自我修養——連接、裝載與庫》來整理的,有興趣的同窗能夠本身去琢磨琢磨~