1. 程式人生 > >C程式碼變成可執行檔案的過程

C程式碼變成可執行檔案的過程

C程式碼是如何變成程式的

C語言是一門典型的編譯語言,原始碼檔案需要編譯成目的碼檔案才能執行。可以認為程式檔案就是編譯好的目的碼檔案。以GCC的編譯過程為例。GCC的翻譯過程可以分成四個階段:前處理器、編譯器、彙編器、連結器,執行這四個階段的程式一起構成了一個編譯系統。

GCC編譯系統

前處理器

前處理器(cpp)負責對原始碼進行文字處理。它根據以字元#開頭的命令,修改原始的C程式碼。如:
1. #include

//main.c
#include ”a.h”
#include ”b.h”
//a.h
#include ”b.h”
void func_a();
//b.h
void
func_b();

上面提到了前處理器在處理#include時是直接的文字插入,處理後的main.i檔案的內容是:

//main.i
void func_b();
void func_a();
void func_b();

b.h的內容被載入了兩次!這個例子足夠簡單,出現這種問題不會發生錯誤,但如果b.h檔案很大,重複載入後可能會出現很多問題,還會導致編譯時間的延長。這種情況下我們可以使用header guard來防止標頭檔案被重複載入,中間省略的部分即標頭檔案的正式內容:

#ifndef XXX_YYY_ZZZ
#define XXX_YYY_ZZZ
...
#endif

其中XXX_YYY_ZZZ是你自定義的巨集名字。如果為每個標頭檔案選擇一個不重複的巨集名字,這個巨集組合保證了每個標頭檔案只會被一個程式碼檔案載入一次,因為第二次載入時XXX_YYY_ZZZ巨集已經定義過了,就直接跳到了#endif的後面。

編譯階段

編譯器(ccl)將文字檔案hello.i翻譯成文字檔案hello.s,它包含一個組合語言程式。組合語言程式中的每條語句都以一種標準的文字格式確切地描述了一條低階機器語言指令。組合語言為不同高階語言的不同編譯器提供了通用的輸出語言,例如C編譯器和Fortran編譯器產生的輸出檔案用的都是一樣的組合語言。
例如,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

所有以字元.開頭的行都是指導彙編器和連結器的命令,其它行則是被翻譯成組合語言的程式碼。

彙編階段

接下來,彙編器(as)將hello.s翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程式的格式,並將結果儲存在目標檔案hello.o中。hello.o檔案是一個二進位制檔案,它的位元組編碼是機器語言指令而不是字元,如果我們在文字編輯器中開啟hello.o檔案,看到的將是一堆亂碼。執行gcc –c hello.c可以得到hello.o檔案,它是二進位制格式,無法直接檢視,可以用反彙編器來檢視它的編碼:objdump –d code.o以一種典型的可重定位目標格式ELF為例。ELF檔案的頭部資料包含了:
1. 生成該檔案的系統的字的大小和位元組順序。
2. 幫助連結器語法分析和解釋目標檔案資訊的資料。ELF檔案中包含的資料可分成幾個節,每個節的位置和大小是由節頭部表描述的:
1. text 機器程式碼
2. rodata 只讀資料,比如雙引號括起的字串等。
3. data 已初始化的全域性變數。
4. bss 未初始化的全域性變數。在ELF檔案中它只是佔位符,在目標檔案中不佔據實際的空間。
5. symtab 一個符號表,存放在程式中定義和引用的函式和全域性變數的資訊。
6. rel.text 一個.text節中位置的列表,當連結器進行連結時,需要修改這些位置。
7. rel.data 被引用或定義的全域性變數的重定位資訊,依賴於其它模組資訊的已初始化的全域性變數,其值在連結時需要被修改。
8. debug 除錯符號表。
9. line 機器程式碼與原始檔行號的對應關係,只有在-g選項時才會產生。
10. .strtab 一個字串表,包括.symtab和.debug中的符號表,以及每個節的名字。

典型的ELF可重定位目標檔案

連結階段

連結器(ld)負責將多個可重定位目標檔案(.o檔案)合併為一個可執行檔案,如hello程式檔案就是由hello.o和printf.o檔案合併得來的。合併過程中連結器負責解析符號表,並修改不同編譯模組間的引用資訊,如hello.o的main函式呼叫printf函式時,機器程式碼的跳轉位置直到連結階段才會確定,連結器會將跳轉位置修改為printf函式的入口位置。連結器解析本地符號的引用是非常簡單的。編譯器只允許每個模組中每個本地符號只有一個定義。不過,對全域性符號的解析就很複雜。如果連結器在所有模組中都找不到某個符號時,它就輸出”undefined reference”錯誤資訊並終止。如果所有符號的解析都順利完成,連結器最後會輸出所有符號的引用位置都確定了的可執行檔案。