資料來源:編譯器與連結器的基本概念 & Wiki & Google大神
作業系統如何執行一個程式?
基本上 CPU 會執行程式指位器(program counter, PC)所在的機械碼,如下圖所示,每當 CPU 擷取(fetch)一條機械碼進來以後(可能需要解碼),它會執行它, 並且移動程式指位器(PC),讓他指向下一條機械碼,如此不斷地循環執行,CPU 便會不斷地循序執行記憶體中的程式了,除非遇到跳躍(jump)指令,PC 就指向其他的機械碼所在的位置,程式自該處繼續執行。基於這個原理,機械碼是記憶體中是連續擺放的,所以同理、.exe 程式檔案中的機械碼也是連續擺放的。
程式執行順序
C/C++ 語言的原始碼(Source Code),是由上而下,一行一行執行的,所以 C 語言的書寫順序,就是執行順序,而 C 語言的流程控制指令會編譯成跳躍指令。
什麼是機械碼(Machine Code / Object code / Binary code )?
本質上 CPU 是許多功能電路的大集合,靠周圍的數百支針腳 (pin) 來選用哪部分電路,以及讀取或輸出資料,而且內部有一些暫存器(register) 暫放計算過程中的參數,如下圖所示,CPU 有一些針腳是用來控制 CPU 應執行的電路,例如這組針腳給它 0100 1010 的話,就是要 enable CPU 內部的加法電路, 對暫存器進行累加的動作。
CPU 另外有一組針腳用來控制記憶體(RAM),例如給它 0000 0111 這組值,他就會自記憶體中位址 0000 0111 的位置,也就是下圖中變數 a 所在的位址(address)讀取數值, 假設下圖中的動作是要累加(increase)變數 a,那麼這個動作的機械碼就是 0100 1010 0000 0111。
(圖一)
編譯器(Compiler)在做什麼?
編譯器(Compiler)的工作,就是把原始碼(Source code)重新編成與翻譯為機械碼(Object code, 俗稱 Binary code),然而,原始碼和機械碼並不是一行對一行翻譯的, 因為 CPU 並無法處理複雜的動作,它只能夠單純地「載入資料、讓訊號流過電路、輸出結果」,CPU 無法處理有語意的指令,所以如下圖的例子,其中的 [b] 代表變數 b 在記憶體中的位址(address),原始碼裡面一行 a=b+c,這述句的意思是把 b 和 c 相加以後,以此結果賦予 a 變數的值,編譯器所產生的機械碼可能是把 [b] 位置的數值搬移(move)到 CPU 的暫存器,接著把 [c] 位置的數值搬移到 CPU 的另一個暫存器,接著如圖一所示,輸出加法運算的機械碼(設定 CPU 周圍的針腳以選用加法電路),若干 clock 以後,某個暫存器中的值變成方才兩個暫存器相加的值,接著下一行,另一個搬移的機械碼又把結果搬移到記憶體的 [a] 位置。
就像這個例子所做的,編譯器不但把毎一行具有語意的原始碼(source code)另外編成多行 CPU 基礎動作的機械碼,而且把原始碼當中變數都替換為變數的記憶體位址,以 Visual C++ 編譯器為例,每一個 .c 或 .cpp 檔案都會被編譯成一個 .obj 檔案,當中的內容已經是這些原始碼編譯而成的機械碼(object code)。
連結器(Linker)在做什麼?
雖然編譯器已經把所有的原始碼都編譯成了機械碼,但是這樣還是不夠的,以下圖為例,HelloWorld.cpp 這個原始碼檔案所做的事情,是在螢幕上顯現一行 "Hello World1" 文字,HelloWorld.cpp 經過編譯器變成了 HelloWorld.obj 檔案,但是,你不太可能連「輸送控制訊號到顯示卡,並顯示 Hello World! 圖形」這種功能都自己寫,所以下圖的程式裡呼叫了一個叫做 printf( ) 的函式來做這件事情,當然 printf( ) 無論是原始碼或機械碼都不在 HelloWorld.cpp 或 HelloWorld.obj 當中。
如上圖所示,HelloWorld.cpp 中所做的事情就只有呼叫了 printf( ) 函式來顯示 Hello World! 文字,甚麼函式呢?如下圖所示,函式一段程式碼片段,放置在主程式以外的另一個地方,它可能會有要求輸入參數,也可能會傳回一些結果數值。當程式執行到一個函式的時候,如下圖所示,它的流程會跳躍到該函式的程式碼處,執行函式的內容,完畢的時候再返回原流程呼叫該函式的地方,接著執行下去。
一個程式所呼叫的函式可能在另一個原始碼檔案(.c 或 .cpp)當中,也可能在另一個機械碼檔案(.obj)中,也可能在程式庫檔案中(.lib)中,無論如何,當編譯器在編譯 HelloWorld.cpp 的時候它並不知道 printf( ) 的程式碼在哪裡,亦不知道程式的流程該跳往何位址,所以在編譯出的 .obj 檔案中這個「空格」會被保留下來,而由連結器(linker)收集了所有的機械碼檔案(.obj與.lib)以後,才將它們填上,其實程式庫檔案(.lib)只不過是一些機械碼檔案(.obj)的集合體。
承續上面的例子,如下圖所示,連結器所產出的可執行檔(executable file)裡面,它把 HelloWorld.obj 和 printf( ) 的機械碼都合併 HelloWorld.exe 當中了,所以在 HelloWorld.obj 當中呼叫 printf( )函式所造成的程式流程跳躍(jump),就變成了是同一個 .exe 檔案內(作業系統載入後是同一塊記憶體內)的位址跳躍了,所以連結器可以明確地寫上位址數值,總結地來說連結器所做的事情就是收集像這樣互相參考的機械碼、合併為執行檔、並且填寫這些機械碼之間的參考位址,這就是為甚麼通常由原始碼產生的可執行檔都會比原始檔案大很多,因為合併了許多來其他檔案的機械碼。
作業系統載入一個可執行檔的時候,它只會配置該程式的專屬記憶體,並且把可執行檔載入到記憶體當中,並從其中某個位置開始執行程式,所有該程式需要的一切,都必須已經在 .exe 檔案裡面,所以由原始碼產生執行檔的過程中,除了要編譯(compile)還需要經過連結(link)才能得到這樣的可執行檔。
C語言程式開發流程
總結:
1. 建置(build)執行檔的過程包括編譯(Compile)和連結(Link)。
2. 編譯器(Compiler)把原始碼(Source code)變成機械碼(Machine code)。
3. 機械碼本質上就是控制CPU針腳的01訊號。
4. 連結器(Linker)把多份機械碼合併成執行檔(execution files),並更正檔案內的參考位址(address)。
留言列表