《编译与反编译技术》—第1章1.7节C语言程序的编译流程
本節書摘來自華章出版社《編譯與反編譯技術》一書中的第1章,第1.7節C語言程序的編譯流程,作者龐建民,陶紅偉,劉曉楠,岳峰,更多章節內容可以訪問云棲社區“華章計算機”公眾號查看。
1.7 C語言程序的編譯流程
本節以C語言程序的編譯流程為例,介紹實際的C語言編譯器是如何運作的。通常把整個代碼的編譯流程分為編譯過程和鏈接過程。
1.編譯過程
編譯過程可分為編譯預處理、編譯與優化、匯編等階段。
(1)編譯預處理
編譯預處理即讀取C源程序,對其中的偽指令(以#開頭的指令)和特殊符號進行處理。主要包括以下幾個方面:
1)宏定義指令,如# define Name TokenString、# undef等。對于前一個偽指令,預編譯所要做的是將程序中的所有Name用TokenString替換,但作為字符串常量的Name則不被替換。對于后一個偽指令,則將取消對某個宏的定義,使以后該串的出現不再被替換。
2)條件編譯指令,如# ifdef、# ifndef、# else、# elif、# endif等。這些偽指令的引入使得程序員可以通過定義不同的宏來決定編譯程序對哪些代碼進行處理。預編譯程序將根據有關的文件,將那些不必要的代碼過濾掉。
3)頭文件包含指令,如# include "FileName"或者# include <FileName>等。在頭文件中一般用偽指令# define定義了大量的宏,還有對各種外部符號的聲明。采用頭文件的目的是使某些定義可以供多個不同的C源程序使用。因為在需要用到這些定義的C源程序中,只需加上一條# include語句,而不必再在此文件中將這些定義重復一遍。預編譯程序將把頭文件中的定義統統都加入它所產生的輸出文件中,以供編譯程序對之進行處理。注意,這個過程是遞歸進行的,也就是說,被包含的文件可能還包含其他文件。包含到C源程序中的頭文件可以是系統提供的,這些頭文件一般放在/usr/include目錄下,在# include中使用它們要用尖括號(<? >)。另外開發人員也可以定義自己的頭文件,這些文件一般與C源程序放在同一目錄下,此時在# include中要用雙引號(" ")。
4)特殊符號。例如在源程序中出現的LINE標識將被解釋為當前行號(十進制數),FILE則被解釋為當前被編譯的C源程序的名稱。預編譯程序對于在源程序中出現的這些串將用合適的值進行替換。預編譯程序所完成的基本上是對源程序的“替代”工作。經過此種替代,生成一個沒有宏定義、沒有條件編譯指令、沒有特殊符號的輸出文件。這個文件的含義與沒有經過預處理的源文件是相同的,但內容有所不同。下一步,此輸出文件將作為編譯程序的輸入而被翻譯成為機器指令序列。
5)刪除注釋。刪除所有的注釋“//…”和“/*…*/”。
6)保留所有的#pragma編譯器指令。以#pragma開始的編譯器指令必須保留,因為編譯器需要使用它們。
經過預編譯后的.i文件不包含任何宏定義,因為所有的宏已經被展開,并且包含的文件也已經被插入.i文件中。所以,當無法判斷宏定義是否正確或頭文件包含是否正確時,可以查看預編譯后的文件來確定。
(2)編譯與優化
經過預編譯得到的輸出文件中只有常量、變量的定義,以及C語言的關鍵字,如main、if、else、for、while、{、}、+、-、*、\等。編譯程序所要做的工作就是通過詞法分析和語法分析,在確認所有的指令都符合語法規則之后,將其翻譯成等價的中間代碼表示或匯編代碼。優化處理涉及的問題不僅同編譯技術本身有關,而且同機器的硬件環境也有關。優化中的一種是對中間代碼的優化。另一種優化則主要是針對目標代碼的生成而進行的。對于前一種優化,主要的工作是刪除公共表達式、循環優化(代碼外提、強度削弱、變換循環控制條件、已知量的合并等)、復寫傳播,以及無用賦值的刪除等。后一種類型的優化同機器的硬件結構密切相關,最主要的是考慮如何充分利用機器的各個硬件寄存器存放有關變量的值,以減少對內存的訪問次數。另外,如何根據機器硬件執行指令的特點(如流水線、RISC、CISC、VLIW等)而對指令進行一些調整使目標代碼比較短,執行的效率比較高,也是優化的一個重要任務。經過優化得到的匯編代碼序列必須經過匯編程序的匯編轉換成相應的機器指令序列,方能被機器執行。
(3)匯編
匯編過程是把匯編語言代碼翻譯成目標機器指令的過程。對于待編譯處理的每一個C語言源程序,都將經過這一處理過程而得到相應的目標文件。目標文件中所存放的也就是與源程序等效的機器語言代碼。目標文件由段組成,通常一個目標文件中至少有兩個段:①代碼段。該段中所包含的主要是程序的機器指令,一般是可讀和可執行的,但卻不可寫。②數據段。主要存放程序中要用到的各種全局變量或靜態的數據,一般是可讀、可寫、可執行的。
UNIX環境下主要有三種類型的目標文件:①可重定位文件,其中包含適合于其他目標文件鏈接以創建一個可執行的或者共享的目標文件的代碼和數據。②共享的目標文件,這種文件存放了適合于在兩種上下文里鏈接的代碼和數據。第一種是靜態鏈接程序,可把它與其他可重定位文件共享的目標文件一起處理來創建另一個目標文件;第二種是動態鏈接程序,將它與另一個可執行文件及其他共享目標文件結合到一起,創建一個進程映像。③可執行文件,它包含了一個可以被操作系統通過創建一個進程來執行的文件。匯編程序生成的實際上是第一種類型的目標文件。對于后兩種還需要其他的一些處理方能得到,這就是鏈接程序的工作了。
2.鏈接過程
由匯編程序生成的目標文件并不能立即被執行,其中可能還有許多沒有解決的問題。例如,某個源文件中的函數可能引用了另一個源文件中定義的某個符號(如變量或者函數調用等);在程序中可能調用了某個庫文件中的函數,等等。所有的這些問題,都需要經過鏈接程序的處理方能得以解決。鏈接程序的主要工作就是將有關的目標文件彼此相連接,亦即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成為一個能夠被操作系統裝入執行的統一整體。根據開發人員指定的與庫函數的鏈接方式的不同,鏈接處理通常可分為兩種:①靜態鏈接。在該方式下,函數的代碼將從其所在的靜態鏈接庫中被復制到可執行程序中。這樣當該程序被執行時,這些代碼將被裝入該進程的虛擬地址空間中。靜態鏈接庫實際上是一個目標文件的集合,其中的每個文件含有庫中的一個或者一組相關函數的代碼。②動態鏈接。在該方式下,函數的代碼被放到稱作動態鏈接庫或共享對象的某個目標文件中。鏈接程序此時所做的只是在最終的可執行程序中記錄下共享對象的名字以及一些少量的登記信息。在該可執行文件被執行時,動態鏈接庫的全部內容將被映射到運行時相應進程的虛地址空間。動態鏈接程序將根據可執行程序中記錄的信息找到相應的函數代碼。對于可執行文件中的函數調用,可分別采用動態鏈接或靜態鏈接的方法。使用動態鏈接能夠使最終的可執行文件比較短小,并且當共享對象被多個進程使用時能節約一些內存,因為在內存中只需要保存一份此共享對象的代碼。但并不是使用動態鏈接就一定比使用靜態鏈接要優越,在某些情況下動態鏈接可能帶來一些性能上的
損失。
3. GCC的編譯鏈接
在Linux中使用的GCC編譯器是把以上幾個過程進行了捆綁,使用戶只使用一次命令就完成編譯工作,這確實很方便,但對于初學者了解編譯過程卻很不利。GCC代理的編譯流程如下:①預編譯,將.c文件轉化成.i文件,使用的GCC命令是gcc –E (對應于預處理命令cpp);②編譯,將.c/.h文件轉換成.s文件,使用的gcc命令是gcc –S(對應于編譯命令cc1,實際上,現在版本的GCC使用cc1將預編譯和編譯兩個步驟合成為一個步驟;③匯編,將.s文件轉化成.o文件,使用的GCC命令是gcc –c (對應于匯編命令as);④鏈接,將.o文件轉化成可執行程序,使用的GCC命令是gcc(對應于鏈接命令ld)。
以名為hello.c的程序為例,編譯流程主要經歷如圖1-2所示的四個過程。
?
圖1-2 C語言程序編譯流程圖
例如,hello.c為:
#include <stdio.h>
Int main(int argc,char *argv[])
{
??? printf("hello world\n");
??? return 0;
}
運行gcc –S hello.c可以得到hello.s文件,其內容為:
.file "hello.c"
.def? ___main; .scl? 2; .type 32; .endef
.section? .rdata,"dr"
LC0:
.ascii "hello world\0"
.text
.globl _main
.def _main; .scl? 2; .type 32; .endef
_main:?
LFB6:?
.cfi_startproc?
pushl?? %ebp?
.cfi_def_cfa_offset 8
? ...
所有以字符“.”開頭的行都是指導匯編器和鏈接器的命令,其他行則是被翻譯成匯編語言的代碼。
C語言編譯的整個過程是比較復雜的,涉及的編譯器知識、硬件知識、工具鏈知識非常多。一般情況下,只需要知道其分成編譯和鏈接兩個階段,編譯階段是將源程序(*.c)轉換成為目標代碼(一般是obj文件),鏈接階段是將源程序轉換成的目標代碼(obj文件)與程序里面調用的庫函數對應的代碼鏈接起來形成對應的可執行文件(exe文件),其他的都需要在實踐中多多體會才能有更加深入的理解。
總結
以上是生活随笔為你收集整理的《编译与反编译技术》—第1章1.7节C语言程序的编译流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《计算机组成原理》----2.3 二进
- 下一篇: 《音乐达人秀:Adobe Auditio