1. 程式人生 > >一個可執行檔案是怎麼來的

一個可執行檔案是怎麼來的

一個可執行檔案的生成一般都要經過下面幾個步驟:

編輯 、預處理 、 編譯、優化、彙編 、 連線   ——>可執行檔案

下面將從這幾個步驟一個一個來分析他們的具體內容。

1. 編輯

編輯這個過程其實挺簡單的,但也是最講究的,它直接體現了一個程式設計者的程式設計習慣,以及影響到別人對程式的閱讀感受,所以有必要總結一下。

(1) 註釋要規範,多用 /*.....*/ ,少用// ,邏輯複雜的函式要註明函式的功能以及每個引數的含義,全域性變數以及結構體要註明用處

(2) 一定要注意縮排,tab設定為4個空格會看起來更緊湊

(3) 分支語句對應的兩個大括號要獨佔一行,而且儘量靠近行的開頭

(4) 注意程式的結構性和層次性

(5) 大型程式應該對函式的功能以及模組進行歸類

(6) 使用一個好的,適合自己的編輯器

2. 預處理

預處理其實就是對所有原始碼進行整合的一個過程,它將該程式所涉及到的所有程式碼,包括標頭檔案、巨集定義、條件編譯和執行程式碼,都整合為一個整體。

預處理過程會完成以下工作:

(1) 檔案包含:包括兩種格式    #include <my.h>      #include "my.h"

                  第一種方法是用尖括號把標頭檔案括起來,這種格式告訴預處理程式在編譯器自帶的或外部庫的標頭檔案中搜索被包含的標頭檔案。

                  第二種方法是用雙引號把標頭檔案括起來,這種格式告訴預處理程式在當前被編譯的應用程式的原始碼檔案中搜索被包含的標頭檔案,如果找不到,再搜尋編譯器自帶的標頭檔案。

                  在預處理時,會將對應檔案的全部內容插入並替換該#include語句, 如果這個標頭檔案還包含另外一個頭檔案,那麼另外一個頭檔案也會先替換呼叫它的的#include語句

(2) 巨集替換:將函式中使用到巨集的地方,都使用對應的值進行替換,這些值主要是#define 命令宣告的

(3) 條件編譯:將不符合條件編譯的語句刪除,保留符合條件編譯的語句。比如#if 0 ... #endif \  #if defined....  #endif   等條件編譯語句,不符合對應條件的語句將會被丟掉,而保留符合條件編譯的部分

(4) 特殊符號:預編譯程式可以識別一些特殊的符號,例如在源程式中出現的LINE標識將被解釋為當前行號(十進位制數),FILE則被解釋為當前被編譯的C源程式的名稱。預編譯程式對於在源程式中出現的這些串將用合適的值進行替換。

(5) 整理:刪除程式中的註釋和多餘的空白字元


3.優化階段

優化處理是編譯系統中一項比較艱深的技術。它涉及到的問題不僅同編譯技術本身有關,而且同機器的硬體環境也有很大的關係。優化一部分是對中間程式碼的優化。這種優化不依賴於具體的計算機。另一種優化則主要針對目的碼的生成而進行的。上圖中,我們將優化階段放在編譯程式的後面,這是一種比較籠統的表示。

對於前一種優化,主要的工作是刪除公共表示式、迴圈優化(程式碼外提、強度削弱、變換迴圈控制條件、已知量的合併等)、複寫傳播,以及無用賦值的刪除,等等。

後一種型別的優化同機器的硬體結構密切相關,最主要的是考慮是如何充分利用機器的各個硬體暫存器存放的有關變數的值,以減少對於記憶體的訪問次數。另外,如何根據機器硬體執行指令的特點(如流水線、RISC、CISC、VLIW等)而對指令進行一些調整使目的碼比較短,執行的效率比較高,也是一個重要的研究課題。

經過優化得到的彙編程式碼必須經過彙編程式的彙編轉換成相應的機器指令,方可能被機器執行。

4.彙編過程

彙編過程實際上指把組合語言程式碼翻譯成目標機器指令的過程。對於被翻譯系統處理的每一個C語言源程式,都將最終經過這一處理而得到相應的目標檔案。目標檔案中所存放的也就是與源程式等效的目標的機器語言程式碼。

 目標檔案由段組成。通常一個目標檔案中至少有兩個段:

程式碼段  該段中所包含的主要是程式的指令。該段一般是可讀和可執行的,但一般卻不可寫。  

資料段  主要存放程式中要用到的各種全域性變數或靜態的資料。一般資料段都是可讀,可寫,可執行的。

UNIX環境下主要有三種類型的目標檔案:

(1)可重定位檔案  其中包含有適合於其它目標檔案連結來建立一個可執行的或者共享的目標檔案的程式碼和資料。

 (2)共享的目標檔案  這種檔案存放了適合於在兩種上下文裡連結的程式碼和資料。第一種事連結程式可把它與其它可重定位檔案及共享的目標檔案一起處理來建立另一個目標檔案;第二種是動態連結程式將它與另一個可執行檔案及其它的共享目標檔案結合到一起,建立一個程序映象。

 (3)可執行檔案  它包含了一個可以被作業系統建立一個程序來執行之的檔案。

彙編程式生成的實際上是第一種型別的目標檔案。對於後兩種還需要其他的一些處理方能得到,這個就是連結程式的工作了。

5.連結程式

由彙編程式生成的目標檔案並不能立即就被執行,其中可能還有許多沒有解決的問題。例如,某個原始檔中的函式可能引用了另一個原始檔中定義的某個符號(如變數或者函式呼叫等);在程式中可能呼叫了某個庫檔案中的函式,等等。所有的這些問題,都需要經連結程式的處理方能得以解決。

連結程式的主要工作就是將有關的目標檔案彼此相連線,也即將在一個檔案中引用的符號同該符號在另外一個檔案中的定義連線起來,使得所有的這些目標檔案成為一個能夠被作業系統裝入執行的統一整體。

根據開發人員指定的同庫函式的連結方式的不同,連結處理可分為兩種:

(1)靜態連結  在這種連結方式下,函式的程式碼將從其所在地靜態連結庫中被拷貝到最終的可執行程式中。這樣該程式在被執行時這些程式碼將被裝入到該程序的虛擬地址空間中。靜態連結庫實際上是一個目標檔案的集合,其中的每個檔案含有庫中的一個或者一組相關函式的程式碼。

 (2)動態連結  在此種方式下,函式的程式碼被放到稱作是動態連結庫或共享物件的某個目標檔案中。連結程式此時所作的只是在最終的可執行程式中記錄下共享物件的名字以及其它少量的登記資訊。在此可執行檔案被執行時,動態連結庫的全部內容將被對映到執行時相應程序的虛地址空間。動態連結程式將根據可執行程式中記錄的資訊找到相應的函式程式碼。

     對於可執行檔案中的函式呼叫,可分別採用動態連結或靜態連結的方法。使用動態連結能夠使最終的可執行檔案比較短小,並且當共享物件被多個程序使用時能節約一些記憶體,因為在記憶體中只需要儲存一份此共享物件的程式碼。但並不是使用動態連結就一定比使用靜態連結要優越。在某些情況下動態連結可能帶來一些效能上損害。

  經過上述五個過程,C源程式就最終被轉換成可執行檔案了

上面5個步驟分別對應了gcc的幾個選項 -E  -S -c  和 ld工具, gcc的-o 選項可以看作是一個重定向選項,和shell中的> 類比, -o後面接的檔名就是輸出檔案, gcc的輸入檔案一般放在命令的最後,或者放在-c的後面

gcc -E:是預處理選項,比如 gcc -E main.c -o main.E 將會生成對應原始檔的彙編結果,注意預處理過程是不產生對應的輸出檔案的,它會將預處理後的內容顯示到螢幕和輸送到編譯階段,所以如果需要儲存預編譯的內容,需要用-o選項進行重定向儲存

gcc -S:是編譯選項,這個選項會將預處理好的原始碼編譯成組合語言,比如gcc -S main.c -o main.S ,注意 -S會預設執行-E選項的過程

gcc -c: 是彙編選項,這個選項將原始碼彙編成對應的目標檔案(*.o),並且以原始檔的字首命名, 比如gcc -c main.c 將生成 main.o , gcc -c main.S 也將生成main.o檔案, 當gcc只有這個選項的時候將預設執行前面的-E  -S選項

ld: ld工具是連線工具,ld -Tmain.lds 0x0000 main.o -o main 它將前面產生的目標檔案連線成可執行檔案,至於目標檔案,我們也可以使用ar工具或者gcc -shared 製作不同的靜態庫和共享庫

如果編譯一個原始檔時,gcc沒有帶任何引數,那麼會將上面的選項全部執行

下面將用一個實際例子來解釋上面的幾個步驟:

(1) 首先編輯一個簡單的檔案 main.c

#include <stdio.h>
#define A 1
#define B 2

int main()
{
    printf("a+b=%d \n", A, B);
    return 0;
}

(2) 執行gcc -E main.c -o main.i ,生成預處理檔案main.i,下面是main.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;
}
可以看到預處理階段,將巨集進行了替換

(3)執行gcc -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)執行 gcc -c main.s -o main.o 將生成目標檔案 main.o

(5)執行ld -Tmain.lds -o main 將連線成可執行檔案 main

main.lds是連線指令碼,它定義了整個程式編譯之後的連線過程,決定了一個可執行程式的各個段的儲存位置,

關於.lds 的內容可自尋查詢