天天看點

一個程式從源代碼到可執行程式的過程

一個源程式到一個可執行程式的過程:預編譯、編譯、彙編、連結。

其中,編譯是主要部分,其中又分為六個部分:詞法分析、文法分析、語義分析、中間代碼生成、目标代碼生成和優化。

連結中,分為靜态連結和動态連結,本文主要是靜态連結。

一、預編譯:主要處理源代碼檔案中的以“#”開頭的預編譯指令。處理規則見下

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檔案。

二、編譯:把預編譯之後生成的xxx.i或xxx.ii檔案,進行一系列詞法分析、文法分析、語義分析及優化後,生成相應的彙編代碼檔案。

(結合程式來說明編譯的幾個步驟)

有C語言的源代碼如下:

arr[3] = (a+4)*(3+8);

1.詞法分析:利用類似于“有限狀态機”的算法,将源代碼程式輸入到掃描機中,将其中的字元序列分割成一系列的記号。

以上的一行C語言程式,一共有16個空字元,經掃描機掃描之後,産生了16個記号。lex可以實作詞法分析。見下表:

一個程式從源代碼到可執行程式的過程

見上圖:

詞法分析産生的記号分類有:關鍵字、辨別符、字面量(數字、字元串)、特殊符号(加号、等号等)

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。

三、彙編:将彙編代碼轉變成機器可以執行的指令(機器碼檔案)。

彙編器的彙編過程相對于編譯器來說更簡單,沒有複雜的文法,也沒有語義,更不需要做指令優化,隻是根據彙編指令和機器指令的對照表一一翻譯過來,彙編過程有彙編器as完成。

經彙編之後,産生目标檔案(與可執行檔案格式幾乎一樣)xxx.o(Windows下)、xxx.obj(Linux下)。

但是,經過預編譯、編譯、彙編之後,生成機器可以執行的目标檔案之後,還有一個問題——變量a和數組arr的位址還沒有确定。這就需要連結器來搞定啦~

四、連結:

1、曆史過程:曾經,程式猿門在程式設計時,使用紙帶作為最原始的儲存設備,每當程式需要修改時,都要重新紮一條紙帶,紮孔的表示1,不紮的是0,一串串1和0就組成了各種各樣的指令——跳轉等等….

每一次的修改都非常痛苦,是以先知們就發明了彙編語言,這種程式設計語言友善之處在于符号的引用,表示跳轉指令不再需要記住一串串0和1,終于可以使用符号——foo來表示這個動作了!

随着彙編語言的普及,程式的代碼量也就開始快速膨脹了,彙編語言說它也撐不住了….不過還好,進階程式設計語言Fortran、C、C++等一個接一個地問世,語言越來越友善了,追求perfect的人們就想:代碼咋寫更好呢?可不可以把代碼按照功能的不同,分成不同的部分,便于日後的修改和重複使用呢?

有了這個啟發,程式猿們越來越得心應手,他們開始把代碼按照功能和性質劃分,分别形成不同的功能子產品,不同的子產品之間又按照各種結構來組織。

發展到如今,軟體的規模越來越大,代碼動辄數百萬行代碼,放在一個子產品那是萬萬不行的,維護起來會非常麻煩,所有現在的大型軟體往往擁有成千上萬的子產品,

子產品之間互相獨立又互相依賴。

新的問題來了,一個程式被分割成這麼多子產品,最後要怎麼把這些子產品組合形成一個單一的程式?

答案就是:子產品之間,符号的引用!

這就像是一張畫有大樹的拼圖,葉子、枝幹、根系都零散的分布在那些拼圖碎片上,想要看到完整的大樹,我們就會耐心地把那些碎片拼合在一起。

一個程式從源代碼到可執行程式的過程

這些子產品之間同樣如此,它們依靠那些凸起和凹陷聯系在一起,最終組合成一個完整的程式,這樣的過程稱為——連結。

這樣基于符号的子產品化,使得連結過程在整個程式開發中顯得十分重要和突出…..

2、下面就靜态連結,進行分析。

1.連結:“組裝”子產品的過程。

2.連結的内容:把各個子產品之間互相引用的部分都處理好,使得各個子產品之間能夠正确地銜接。(就像拼圖,凸起和凹槽的位置一定一一對應,否則…)

3.連結的過程:位址和空間的配置設定、符号決議(也叫“符号綁定”,傾向于動态連結)和重定位

以gcc編譯器為例,看基本的連結過程:

一個程式從源代碼到可執行程式的過程

.c檔案經過編譯器、彙編器之後得到目标檔案.o,目标檔案再與庫進行連結得到可執行檔案.out。

庫其實就是一組目标檔案的打包,這些目标檔案中都是一些常用的代碼。

繼續閱讀