一個可執(zhí)行文件的生成一般都要經(jīng)過下面幾個步驟:
編輯 、預(yù)處理 、 編譯、優(yōu)化、匯編 、 連接 ——>可執(zhí)行文件
下面將從這幾個步驟一個一個來分析他們的具體內(nèi)容。
編輯
編輯這個過程其實挺簡單的,但也是最講究的,它直接體現(xiàn)了一個編程者的編程習(xí)慣,以及影響到別人對程序的閱讀感受,所以有必要總結(jié)一下。
(1) 注釋要規(guī)范,多用 /...../ ,少用// ,邏輯復(fù)雜的函數(shù)要注明函數(shù)的功能以及每個參數(shù)的含義,全局變量以及結(jié)構(gòu)體要注明用處
(2) 一定要注意縮進,tab設(shè)置為4個空格會看起來更緊湊
(3) 分支語句對應(yīng)的兩個大括號要獨占一行,而且盡量靠近行的開頭
(4) 注意程序的結(jié)構(gòu)性和層次性
(5) 大型程序應(yīng)該對函數(shù)的功能以及模塊進行歸類
(6) 使用一個好的,適合自己的編輯器預(yù)處理
預(yù)處理其實就是對所有源代碼進行整合的一個過程,它將該程序所涉及到的所有代碼,包括頭文件、宏定義、條件編譯和執(zhí)行代碼,都整合為一個整體。
預(yù)處理過程會完成以下工作:
(1) 文件包含:包括兩種格式 #include <my.h> #include "my.h"
第一種方法是用尖括號把頭文件括起來,這種格式告訴預(yù)處理程序在編譯器自帶的或外部庫的頭文件中搜索被包含的頭文件。第二種方法是用雙引號把頭文件括起來,這種格式告訴預(yù)處理程序在當(dāng)前被編譯的應(yīng)用程序的源代碼文件中搜索被包含的頭文件,如果找不到,再搜索編譯器自帶的頭文件。在預(yù)處理時,會將對應(yīng)文件的全部內(nèi)容插入并替換該#include語句, 如果這個頭文件還包含另外一個頭文件,那么另外一個頭文件也會先替換調(diào)用它的的#include語句
(2) 宏替換:將函數(shù)中使用到宏的地方,都使用對應(yīng)的值進行替換,這些值主要是#define 命令聲明的
(3) 條件編譯:將不符合條件編譯的語句刪除,保留符合條件編譯的語句。比如#if 0 ... #endif \ #if defined.... #endif 等條件編譯語句,不符合對應(yīng)條件的語句將會被丟掉,而保留符合條件編譯的部分
(4) 特殊符號:預(yù)編譯程序可以識別一些特殊的符號,例如在源程序中出現(xiàn)的LINE標識將被解釋為當(dāng)前行號(十進制數(shù)),F(xiàn)ILE則被解釋為當(dāng)前被編譯的C源程序的名稱。預(yù)編譯程序?qū)τ谠谠闯绦蛑谐霈F(xiàn)的這些串將用合適的值進行替換。
(5) 整理:刪除程序中的注釋和多余的空白字符
3.優(yōu)化階段
優(yōu)化處理是編譯系統(tǒng)中一項比較艱深的技術(shù)。它涉及到的問題不僅同編譯技術(shù)本身有關(guān),而且同機器的硬件環(huán)境也有很大的關(guān)系。優(yōu)化一部分是對中間代碼的優(yōu)化。這種優(yōu)化不依賴于具體的計算機。另一種優(yōu)化則主要針對目標代碼的生成而進行的。上圖中,我們將優(yōu)化階段放在編譯程序的后面,這是一種比較籠統(tǒng)的表示。
對于前一種優(yōu)化,主要的工作是刪除公共表達式、循環(huán)優(yōu)化(代碼外提、強度削弱、變換循環(huán)控制條件、已知量的合并等)、復(fù)寫傳播,以及無用賦值的刪除,等等。后一種類型的優(yōu)化同機器的硬件結(jié)構(gòu)密切相關(guān),最主要的是考慮是如何充分利用機器的各個硬件寄存器存放的有關(guān)變量的值,以減少對于內(nèi)存的訪問次數(shù)。另外,如何根據(jù)機器硬件執(zhí)行指令的特點(如流水線、RISC、CISC、VLIW等)而對指令進行一些調(diào)整使目標代碼比較短,執(zhí)行的效率比較高,也是一個重要的研究課題。
經(jīng)過優(yōu)化得到的匯編代碼必須經(jīng)過匯編程序的匯編轉(zhuǎn)換成相應(yīng)的機器指令,方可能被機器執(zhí)行。
4.匯編過程
匯編過程實際上指把匯編語言代碼翻譯成目標機器指令的過程。對于被翻譯系統(tǒng)處理的每一個C語言源程序,都將最終經(jīng)過這一處理而得到相應(yīng)的目標文件。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。 目標文件由段組成。通常一個目標文件中至少有兩個段:代碼段 該段中所包含的主要是程序的指令。該段一般是可讀和可執(zhí)行的,但一般卻不可寫。 數(shù)據(jù)段 主要存放程序中要用到的各種全局變量或靜態(tài)的數(shù)據(jù)。一般數(shù)據(jù)段都是可讀,可寫,可執(zhí)行的。
UNIX環(huán)境下主要有三種類型的目標文件:
(1)可重定位文件 其中包含有適合于其它目標文件鏈接來創(chuàng)建一個可執(zhí)行的或者共享的目標文件的代碼和數(shù)據(jù)。
(2)共享的目標文件 這種文件存放了適合于在兩種上下文里鏈接的代碼和數(shù)據(jù)。第一種事鏈接程序可把它與其它可重定位文件及共享的目標文件一起處理來創(chuàng)建另一個目標文件;第二種是動態(tài)鏈接程序?qū)⑺c另一個可執(zhí)行文件及其它的共享目標文件結(jié)合到一起,創(chuàng)建一個進程映象。
(3)可執(zhí)行文件 它包含了一個可以被操作系統(tǒng)創(chuàng)建一個進程來執(zhí)行之的文件。匯編程序生成的實際上是第一種類型的目標文件。對于后兩種還需要其他的一些處理方能得到,這個就是鏈接程序的工作了。
5.鏈接程序
由匯編程序生成的目標文件并不能立即就被執(zhí)行,其中可能還有許多沒有解決的問題。例如,某個源文件中的函數(shù)可能引用了另一個源文件中定義的某個符號(如變量或者函數(shù)調(diào)用等);在程序中可能調(diào)用了某個庫文件中的函數(shù),等等。所有的這些問題,都需要經(jīng)鏈接程序的處理方能得以解決。
鏈接程序的主要工作就是將有關(guān)的目標文件彼此相連接,也即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成為一個能夠被操作系統(tǒng)裝入執(zhí)行的統(tǒng)一整體。
根據(jù)開發(fā)人員指定的同庫函數(shù)的鏈接方式的不同,鏈接處理可分為兩種:
(1)靜態(tài)鏈接 在這種鏈接方式下,函數(shù)的代碼將從其所在地靜態(tài)鏈接庫中被拷貝到最終的可執(zhí)行程序中。這樣該程序在被執(zhí)行時這些代碼將被裝入到該進程的虛擬地址空間中。靜態(tài)鏈接庫實際上是一個目標文件的集合,其中的每個文件含有庫中的一個或者一組相關(guān)函數(shù)的代碼。
(2)動態(tài)鏈接 在此種方式下,函數(shù)的代碼被放到稱作是動態(tài)鏈接庫或共享對象的某個目標文件中。鏈接程序此時所作的只是在最終的可執(zhí)行程序中記錄下共享對象的名字以及其它少量的登記信息。在此可執(zhí)行文件被執(zhí)行時,動態(tài)鏈接庫的全部內(nèi)容將被映射到運行時相應(yīng)進程的虛地址空間。動態(tài)鏈接程序?qū)⒏鶕?jù)可執(zhí)行程序中記錄的信息找到相應(yīng)的函數(shù)代碼。
對于可執(zhí)行文件中的函數(shù)調(diào)用,可分別采用動態(tài)鏈接或靜態(tài)鏈接的方法。使用動態(tài)鏈接能夠使最終的可執(zhí)行文件比較短小,并且當(dāng)共享對象被多個進程使用時能節(jié)約一些內(nèi)存,因為在內(nèi)存中只需要保存一份此共享對象的代碼。但并不是使用動態(tài)鏈接就一定比使用靜態(tài)鏈接要優(yōu)越。在某些情況下動態(tài)鏈接可能帶來一些性能上損害。
經(jīng)過上述五個過程,C源程序就最終被轉(zhuǎn)換成可執(zhí)行文件了
上面5個步驟分別對應(yīng)了gcc的幾個選項 -E -S -c 和 ld工具, gcc的-o 選項可以看作是一個重定向選項,和shell中的> 類比, -o后面接的文件名就是輸出文件, gcc的輸入文件一般放在命令的最后,或者放在-c的后面
gcc -E:是預(yù)處理選項,比如 gcc -E main.c -o main.E 將會生成對應(yīng)源文件的匯編結(jié)果,注意預(yù)處理過程是不產(chǎn)生對應(yīng)的輸出文件的,它會將預(yù)處理后的內(nèi)容顯示到屏幕和輸送到編譯階段,所以如果需要保存預(yù)編譯的內(nèi)容,需要用-o選項進行重定向保存
gcc -S:是編譯選項,這個選項會將預(yù)處理好的源代碼編譯成匯編語言,比如gcc -S main.c -o main.S ,注意 -S會默認執(zhí)行-E選項的過程
gcc -c: 是匯編選項,這個選項將源代碼匯編成對應(yīng)的目標文件(*.o),并且以源文件的前綴命名, 比如gcc -c main.c 將生成 main.o , gcc -c main.S 也將生成main.o文件, 當(dāng)gcc只有這個選項的時候?qū)⒛J執(zhí)行前面的-E -S選項
ld: ld工具是連接工具,ld -Tmain.lds 0x0000 main.o -o main 它將前面產(chǎn)生的目標文件連接成可執(zhí)行文件,至于目標文件,我們也可以使用ar工具或者gcc -shared 制作不同的靜態(tài)庫和共享庫
如果編譯一個源文件時,gcc沒有帶任何參數(shù),那么會將上面的選項全部執(zhí)行
下面將用一個實際例子來解釋上面的幾個步驟:
(1) 首先編輯一個簡單的文件 main.c
#include <stdio.h>
#define A 1
#define B 2
int main()
{
printf("a+b=%d \n", A, B);
return 0;
}
(2) 執(zhí)行g(shù)cc -E main.c -o main.i ,生成預(yù)處理文件main.i,下面是main.i的內(nèi)容
extern char *ctermid (char *__s) __attribute__ ((__nothrow__));
# 886 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));
# 916 "/usr/include/stdio.h" 3 4
# 2 "main.c" 2
int main()
{
printf("a+b=%d \n", 1, 2);
return 0;
}
可以看到預(yù)處理階段,將宏進行了替換
(3)執(zhí)行g(shù)cc -S main.c -o main.s 將生成匯編文件main.s
.file "main.c"
.section .rodata
.LC0:
.string "a+b=%d \n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, %eax
movl $2, 8(%esp)
movl $1, 4(%esp)
movl %eax, (%esp)
call printf
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
.section .note.GNU-stack,"",@progbits
(4)執(zhí)行 gcc -c main.s -o main.o 將生成目標文件 main.o
(5)執(zhí)行l(wèi)d -Tmain.lds -o main 將連接成可執(zhí)行文件 main
main.lds是連接腳本,它定義了整個程序編譯之后的連接過程,決定了一個可執(zhí)行程序的各個段的存儲位置,
關(guān)于.lds 的內(nèi)容可自尋查找
轉(zhuǎn)載出處:http://blog.csdn.net/lee244868149/article/details/49536747