原始碼要執行,必須先轉成二進位制的機器碼。這是編譯器的任務。
比如,下面這段原始碼(假定檔名叫做test.c)。
要先用編譯器處理一下,才能執行。#include
int main(
void)
對於複雜的專案,編譯過程還必須分成三步。$ gcc test.c
$ ./a.out
hello, world!
這些命令到底在幹什麼?大多數的書籍和資料,都語焉不詳,只說這樣就可以編譯了,沒有進一步的解釋。$ .
/configure
$ make
$ make install
本文將介紹編譯器的工作過程,也就是上面這三個命令各自的任務。我主要參考了alex smith的文章《building c projects》。需要宣告的是,本文主要針對gcc編譯器,也就是針對c和c++,不一定適用於其他語言的編譯。
編譯器在開始工作之前,需要知道當前的系統環境,比如標準庫在**、軟體的安裝位置在**、需要安裝哪些元件等等。這是因為不同計算機的系統環境不一樣,通過指定編譯引數,編譯器就可以靈活適應環境,編譯出各種環境都能執行的機器碼。這個確定編譯引數的步驟,就叫做"配置"(configure)。
這些配置資訊儲存在乙個配置檔案之中,約定俗成是乙個叫做configure的指令碼檔案。通常它是由autoconf工具生成的。編譯器通過執行這個指令碼,獲知編譯引數。
configure指令碼已經盡量考慮到不同系統的差異,並且對各種編譯引數給出了預設值。如果使用者的系統環境比較特別,或者有一些特定的需求,就需要手動向configure指令碼提供編譯引數。
上面**是php原始碼的一種編譯配置,使用者指定安裝後的檔案儲存在www目錄,並且編譯時加入mysql模組的支援。$ .
/configure --prefix=
/www --with-mysql
原始碼肯定會用到標準庫函式(standard library)和標頭檔案(header)。它們可以存放在系統的任意目錄中,編譯器實際上沒辦法自動檢測它們的位置,只有通過配置檔案才能知道。
編譯的第二步,就是從配置檔案中知道標準庫和標頭檔案的位置。一般來說,配置檔案會給出乙個清單,列出幾個具體的目錄。等到編譯時,編譯器就按順序到這幾個目錄中,尋找目標。
對於大型專案來說,原始碼檔案之間往往存在依賴關係,編譯器需要確定編譯的先後順序。假定a檔案依賴於b檔案,編譯器應該保證做到下面兩點。
(1)只有在b檔案編譯完成後,才開始編譯a檔案。編譯順序儲存在乙個叫做makefile的檔案中,裡面列出哪個檔案先編譯,哪個檔案後編譯。而makefile檔案由configure指令碼執行生成,這就是為什麼編譯時configure必須首先執行的原因。(2)當b檔案發生變化時,a檔案會被重新編譯。
在確定依賴關係的同時,編譯器也確定了,編譯時會用到哪些標頭檔案。
不同的原始碼檔案,可能引用同乙個標頭檔案(比如stdio.h)。編譯的時候,標頭檔案也必須一起編譯。為了節省時間,編譯器會在編譯原始碼之前,先編譯標頭檔案。這保證了標頭檔案只需編譯一次,不必每次用到的時候,都重新編譯了。
不過,並不是標頭檔案的所有內容,都會被預編譯。用來宣告巨集的#define命令,就不會被預編譯。
預編譯完成後,編譯器就開始替換掉原始碼中bash的標頭檔案和巨集。以本文開頭的那段原始碼為例,它包含標頭檔案stdio.h,替換後的樣子如下。
為了便於閱讀,上面**只擷取了標頭檔案中與原始碼相關的那部分,即fputs和file的宣告,省略了stdio.h的其他部分(因為它們非常長)。另外,上面**的標頭檔案沒有經過預編譯,而實際上,插入原始碼的是預編譯後的結果。編譯器在這一步還會移除注釋。extern int fputs(
const char *
, file *);
extern file *stdout;
int main(
void)
這一步稱為"預處理"(preprocessing),因為完成之後,就要開始真正的處理了。
預處理之後,編譯器就開始生成機器碼。對於某些編譯器來說,還存在乙個中間步驟,會先把原始碼轉為彙編碼(assembly),然後再把彙編碼轉為機器碼。
下面是本文開頭的那段原始碼轉成的彙編碼。
這種轉碼後的檔案稱為物件檔案(object file)。.file "test.c"
.section .rodata
.lc0:
.string "hello, world!\n"
.text
.globl main
.type main, @function
main:
.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
物件檔案還不能執行,必須進一步轉成可執行檔案。如果你仔細看上一步的轉碼結果,會發現其中引用了stdout函式和fwrite函式。也就是說,程式要正常執行,除了上面的**以外,還必須有stdout和fwrite這兩個函式的**,它們是由c語言的標準庫提供的。
編譯器的下一步工作,就是把外部函式的**(通常是字尾名為.lib和.a的檔案),新增到可執行檔案中。這就叫做連線(linking)。這種通過拷貝,將外部函式庫新增到可執行檔案的方式,叫做靜態連線(static linking),後文會提到還有動態連線(dynamic linking)。
make命令的作用,就是從第四步標頭檔案預編譯開始,一直到做完這一步。
表面上,這一步很簡單,就是將可執行檔案(連帶相關的資料檔案)拷貝過去就行了。但是實際上,這一步還必須完成建立目錄、儲存檔案、設定許可權等步驟。這整個的儲存過程就稱為"安裝"(installation)。
可執行檔案安裝後,必須以某種方式通知作業系統,讓其知道可以使用這個程式了。比如,我們安裝了乙個文字閱讀程式,往往希望雙擊txt檔案,該程式就會自動執行。
這些事情就叫做"作業系統連線"。make install命令,就用來完成"安裝"和"作業系統連線"這兩步。
寫到這裡,原始碼編譯的整個過程就基本完成了。但是只有很少一部分使用者,願意耐著性子,從頭到尾做一遍這個過程。事實上,如果你只有原始碼可以交給使用者,他們會認定你是乙個不友好的傢伙。大部分使用者要的是乙個二進位制的可執行程式,立刻就能執行。這就要求開發者,將上一步生成的可執行檔案,做成可以分發的安裝包。
所以,編譯器還必須有生成安裝包的功能。通常是將可執行檔案(連帶相關的資料檔案),以某種目錄結構,儲存成壓縮檔案包,交給使用者。
正常情況下,到這一步,程式已經可以執行了。至於執行期間(runtime)發生的事情,與編譯器一概無關。但是,開發者可以在編譯階段選擇可執行檔案連線外部函式庫的方式,到底是靜態連線(編譯時連線),還是動態連線(執行時連線)。所以,最後還要提一下,什麼叫做動態連線。
前面已經說過,靜態連線就是把外部函式庫,拷貝到可執行檔案中。這樣做的好處是,適用範圍比較廣,不用擔心使用者機器缺少某個庫檔案;缺點是安裝包會比較大,而且多個應用程式之間,無法共享庫檔案。動態連線的做法正好相反,外部函式庫不進入安裝包,只在執行時動態引用。好處是安裝包會比較小,多個應用程式可以共享庫檔案;缺點是使用者必須事先安裝好庫檔案,而且版本和安裝位置都必須符合要求,否則就不能正常執行。
現實中,大部分軟體採用動態連線,共享庫檔案。這種動態共享的庫檔案,linux平台是字尾名為.so的檔案,windows平台是.dll檔案,mac平台是.dylib檔案。
(文章完)
出處:
編譯器工作過程
原始碼要執行,必須先轉成二進位制的機器碼。這是編譯器的任務。比如,下面這段原始碼 假定檔名叫做test.c 要先用編譯器處理一下,才能執行。這些命令到底在幹什麼?大多數的書籍和資料,都語焉不詳,只說這樣就可以編譯了,沒有進一步的解釋。本文將介紹編譯器的工作過程,也就是上面這三個命令各自的任務。我主要...
編譯器的工作過程
日期 2014年11月11日 原始碼要執行,必須先轉成二進位制的機器碼。這是編譯器的任務。比如,下面這段原始碼 假定檔名叫做test.c include int main void 要先用編譯器處理一下,才能執行。gcc test.c a.out hello,world 對於複雜的專案,編譯過程還必...
編譯器的工作過程
日期 2014年11月11日 原始碼要執行,必須先轉成二進位制的機器碼。這是編譯器的任務。比如,下面這段原始碼 假定檔名叫做test.c include int main void 要先用編譯器處理一下,才能執行。gcc test.c a.out hello,world 對於複雜的專案,編譯過程還必...