編譯器工作過程簡析

編譯器工作過程簡析

源碼要運行,必須先轉成二進制的機器碼。這是編譯器的任務。
整個過程包含如下的步驟:
第一步 配置(configure)
第二步 確定標準庫和頭文件的位置
第三步 確定依賴關係
第四步 頭文件的預編譯(precompilation)
第五步 預處理(Preprocessing)
第六步 編譯(Compilation)
第七步 連接(Linking)
第八步 安裝(Installation)
第九步 操作系統連接
第十步 生成安裝包

比如,下面這段源碼(假定文件名叫做test.c):

#include <stdio.h>

int main(void){
  fputs("Hello, world!\n", stdout);
  return 0;
}

要先用編譯器處理一下才能運行,編譯步驟如下:

$ gcc test.c
$ ./a.out
Hello, world!

對於複雜的項目,編譯過程還必須分成三步。

$ ./configure
$ make  
$ make install

這些命令到底在幹什麼?大多數的書籍和資料,都語焉不詳,只說這樣就可以編譯了,沒有進一步的解釋。

本文將介紹編譯器的工作過程,也就是上面這三個命令各自的任務。我主要參考了Alex Smith的文章《Building C Projects》。需要聲明的是,本文主要針對gcc編譯器,也就是針對C和C++,不一定適用於其他語言的編譯。

第一步 配置(configure)

編譯器在開始工作之前,需要知道當前的系統環境,比如標準庫在哪裏、軟件的安裝位置在哪裏、需要安裝哪些組件等等。這是因爲不同計算機的系統環境不一樣,通過指定編譯參數,編譯器就可以靈活適應環境,編譯出各種環境都能運行的機器碼。這個確定編譯參數的步驟,就叫做"配置"(configure)。

這些配置信息保存在一個配置文件之中,約定俗成是一個叫做configure的腳本文件。通常它是由autoconf工具生成的。編譯器通過運行這個腳本,獲知編譯參數。

configure腳本已經儘量考慮到不同系統的差異,並且對各種編譯參數給出了默認值。如果用戶的系統環境比較特別,或者有一些特定的需求,就需要手動向configure腳本提供編譯參數:

$ ./configure --prefix=/www --with-mysql

上面代碼是php源碼的一種編譯配置,用戶指定安裝後的文件保存在www目錄,並且編譯時加入mysql模塊的支持。

第二步 確定標準庫和頭文件的位置

源碼肯定會用到標準庫函數(standard library)和頭文件(header)。它們可以存放在系統的任意目錄中,編譯器實際上沒辦法自動檢測它們的位置,只有通過配置文件才能知道。

編譯的第二步,就是從配置文件中知道標準庫和頭文件的位置。一般來說,配置文件會給出一個清單,列出幾個具體的目錄。等到編譯時,編譯器就按順序到這幾個目錄中,尋找目標。

第三步 確定依賴關係

對於大型項目來說,源碼文件之間往往存在依賴關係,編譯器需要確定編譯的先後順序。假定A文件依賴於B文件,編譯器應該保證做到下面兩點。

(1)只有在B文件編譯完成後,纔開始編譯A文件。

(2)當B文件發生變化時,A文件會被重新編譯。

編譯順序保存在一個叫做makefile的文件中,裏面列出哪個文件先編譯,哪個文件後編譯。而makefile文件由configure腳本運行生成,這就是爲什麼編譯時configure必須首先運行的原因。

在確定依賴關係的同時,編譯器也確定了,編譯時會用到哪些頭文件。

第四步 頭文件的預編譯(precompilation)

不同的源碼文件,可能引用同一個頭文件(比如stdio.h)。編譯的時候,頭文件也必須一起編譯。爲了節省時間,編譯器會在編譯源碼之前,先編譯頭文件。這保證了頭文件只需編譯一次,不必每次用到的時候,都重新編譯了。

不過,並不是頭文件的所有內容,都會被預編譯。用來聲明宏的#define命令,就不會被預編譯。

第五步 預處理(Preprocessing)

預編譯完成後,編譯器就開始替換掉源碼中bash的頭文件和宏。以本文開頭的那段源碼爲例,它包含頭文件stdio.h,替換後的樣子如下。

extern int fputs(const char , FILE );
extern FILE *stdout;

int main(void){

fputs("Hello, world!\n", stdout);
return 0;

}
爲了便於閱讀,上面代碼只截取了頭文件中與源碼相關的那部分,即fputs和FILE的聲明,省略了stdio.h的其他部分(因爲它們非常長)。另外,上面代碼的頭文件沒有經過預編譯,而實際上,插入源碼的是預編譯後的結果。編譯器在這一步還會移除註釋。

這一步稱爲"預處理"(Preprocessing),因爲完成之後,就要開始真正的處理了。

第六步 編譯(Compilation)

預處理之後,編譯器就開始生成機器碼。對於某些編譯器來說,還存在一箇中間步驟,會先把源碼轉爲彙編碼(assembly),然後再把彙編碼轉爲機器碼。

下面是本文開頭的那段源碼轉成的彙編碼。

.file   "test.c"
.section    .rodata.LC0:
.string "Hello, world!\n"
.text    .globl  main    .type   main, @functionmain:.LFB0:
.cfi_startproc
pushq   %rbp    .cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp    .cfi_def_cfa_register 6
movq    stdout(%rip), %rax
movq    %rax, %rcx
movl    $14, %edx
movl    $1, %esi
movl    $.LC0, %edi
call    fwrite
movl    $0, %eax
popq    %rbp    .cfi_def_cfa 7, 8
ret    .cfi_endproc.LFE0:
.size   main, .-main    .ident  "GCC: (Debian 4.9.1-19) 4.9.1"
.section    .note.GNU-stack,"",@progbits

這種轉碼後的文件稱爲對象文件(object file)。

第七步 連接(Linking)

對象文件還不能運行,必須進一步轉成可執行文件。如果你仔細看上一步的轉碼結果,會發現其中引用了stdout函數和fwrite函數。也就是說,程序要正常運行,除了上面的代碼以外,還必須有stdout和fwrite這兩個函數的代碼,它們是由C語言的標準庫提供的。

編譯器的下一步工作,就是把外部函數的代碼(通常是後綴名爲.lib和.a的文件),添加到可執行文件中。這就叫做連接(linking)。這種通過拷貝,將外部函數庫添加到可執行文件的方式,叫做靜態連接(static linking),後文會提到還有動態連接(dynamic linking)。

make命令的作用,就是從第四步頭文件預編譯開始,一直到做完這一步。

第八步 安裝(Installation)

上一步的連接是在內存中進行的,即編譯器在內存中生成了可執行文件。下一步,必須將可執行文件保存到用戶事先指定的安裝目錄。

表面上,這一步很簡單,就是將可執行文件(連帶相關的數據文件)拷貝過去就行了。但是實際上,這一步還必須完成創建目錄、保存文件、設置權限等步驟。這整個的保存過程就稱爲"安裝"(Installation)。

第九步 操作系統連接

可執行文件安裝後,必須以某種方式通知操作系統,讓其知道可以使用這個程序了。比如,我們安裝了一個文本閱讀程序,往往希望雙擊txt文件,該程序就會自動運行。

這就要求在操作系統中,登記這個程序的元數據:文件名、文件描述、關聯後綴名等等。Linux系統中,這些信息通常保存在/usr/share/applications目錄下的.desktop文件中。另外,在Windows操作系統中,還需要在Start啓動菜單中,建立一個快捷方式。

這些事情就叫做"操作系統連接"。make install命令,就用來完成"安裝"和"操作系統連接"這兩步。

第十步 生成安裝包

寫到這裏,源碼編譯的整個過程就基本完成了。但是隻有很少一部分用戶,願意耐着性子,從頭到尾做一遍這個過程。事實上,如果你只有源碼可以交給用戶,他們會認定你是一個不友好的傢伙。大部分用戶要的是一個二進制的可執行程序,立刻就能運行。這就要求開發者,將上一步生成的可執行文件,做成可以分發的安裝包。

所以,編譯器還必須有生成安裝包的功能。通常是將可執行文件(連帶相關的數據文件),以某種目錄結構,保存成壓縮文件包,交給用戶。

第十一步 動態連接(Dynamic linking)

正常情況下,到這一步,程序已經可以運行了。至於運行期間(runtime)發生的事情,與編譯器一概無關。但是,開發者可以在編譯階段選擇可執行文件連接外部函數庫的方式,到底是靜態連接(編譯時連接),還是動態連接(運行時連接)。所以,最後還要提一下,什麼叫做動態連接。

前面已經說過,靜態連接就是把外部函數庫,拷貝到可執行文件中。這樣做的好處是,適用範圍比較廣,不用擔心用戶機器缺少某個庫文件;缺點是安裝包會比較大,而且多個應用程序之間,無法共享庫文件。

動態連接的做法正好相反,外部函數庫不進入安裝包,只在運行時動態引用。好處是安裝包會比較小,多個應用程序可以共享庫文件;缺點是用戶必須事先安裝好庫文件,而且版本和安裝位置都必須符合要求,否則就不能正常運行。

現實中,大部分軟件採用動態連接,共享庫文件。這種動態共享的庫文件,Linux平臺是後綴名爲.so的文件,Windows平臺是.dll文件,Mac平臺是.dylib文件。
出處:http://www.ruanyifeng.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章