代碼是如何編譯成程序的?

這段程序應該是碼農的入門曲:javascript

#include<stdio.h>int main(void){printf("Hello,World!\n");return 0;}


我想大部分人都能閉着眼睛敲出來,連鼠標都不用移動。編譯連接,運行結果以下:css

$gcc -o hello hello.c$./helloHello World!


很好,C語言基礎很紮實😄。咱們這裏來分析一下這幾行敲下gcc這一行命令以後到底發生了什麼(Shell的運行機制下回探討),來看看C語言翻譯爲機器碼,看看可執行文件裏都有哪些東東。java

上面GCC的構建過程分爲4個步驟,分別是預處理、編譯、彙編和連接,以下圖所示:shell

             

             

預編譯swift

首先是對源文件hello.c中的預處理指令即#開頭的指令,如#include、#define等進行展開替換刪除等處理,被預編譯成一個.i文件。預處理過程至關於以下編譯命令:bash

$gcc -E -o hello.i hello.c


預處理完成以後,註釋內容被刪除,宏定義會被展開。微信

編譯函數

預處理以後就須要對生成的預處理文件進行詞法分析,語法分析,語義分析及優化後生成相應的彙編代碼文件,也就是將高級語言翻譯成機器碼的最核心的部分。咱們能夠經過命令:工具

gcc -S -o hello.s hello.c


將源文件編譯成彙編代碼。優化

彙編

彙編是將彙編代碼翻譯成可執行的指令,每一條彙編語句基本對應一條機器指令,所以彙編器相對編譯器較爲簡單,只須要按照彙編指令和機器指令的對照表進行一一翻譯,這也是「彙編」一詞的由來。能夠用下面的命令得到彙編後的目標文件:

gcc -o hello.o -c hello.c


上述命令至關於:

as hello.s -o hello.o


目標文件格式跟可執行文件同樣,都屬於ELF文件。Linux系統下ELF類型文件還包括核心轉儲文件(core dump)、動態連接庫(.so文件)。ELF文件包括文件頭、代碼段、數據段和.bss段(未初始化的全局變量),使用命令:objdump -h hello.o 能夠查看目標文件的主要段,能夠看到hello.o的代碼段和數據段。

使用objdump -d hello.o命令查看目標文件hello.o的內容以下:

其中:

18: e8 00 00 00 00 callq 0 <_main+0x1d> 表示對函數printf的引用


能夠看到,編譯階段,printf函數在外部定義,未定義函數printf的調用地址爲0。這裏囉嗦下,objdump是個很好用的工具,對於初學編譯原理頗有用。

連接

連接是將各個目標文件所須要的代碼塊收集在一塊兒,生成最終的可執行文件。咱們的helloworld裏面調用了printf函數,可是並無它的實現,其實如今libc.so(動態庫)或者libc.a(靜態庫)中。所謂的庫就是將一些比較經常使用的函數實現編譯成目標文件並打包,所以咱們使用ar命令就能夠將庫拆分紅目標文件:

$ar -t libc.ainit-first.olibc-start.osysdep.oversion.ocheck_fds.olibc-tls.oelf-init.odso_handle.oerrno.oinit-arch.oerrno-loc.ohp-timing.oiconv_open.oiconv.oiconv_close.ogconv_db.o…………


查看連接後可執行文件hello的內容:

其中:

100000f78: e8 0d 00 00 00 callq 13 <dyld_stub_binder+0x100000f8a>


能夠看到代碼段調用地址已被賦值。我這裏使用的iOS系統,實現與Linux略有不一樣,dyld_stub_binder 會在目標符號(例如 printf)被調用時,將其連接到指定的動態連接庫 libSystem,再調用printf函數,printf符號位於在data段的lazy符號表中可獲取。

 

靜態連接過程包括:

  • 空間與地址分配符號解析和重定位靜態庫連接

下面一一講解。

空間地址的分配

剛纔講了,連接過程就是將多個目標加工後合併成一個可執行文件,對於有多個目標文件的連接狀況,存在兩種地址空間分配策略:按序疊加和類似段合併。

按序疊加很好理解,就是直接合並:

直接合並會形成一個問題,就是可執行文件會有不少零散的段,而每一個段都須要地址和空間對齊,如x86硬件下對齊單位是頁,也就是4096字節,零散段會形成空間浪費。

       類似段合併就是將相同性質的段合併到一塊兒:

這裏.bss段存放的是未初始化的全局變量,由於沒有內容,所以不佔用文件空間只佔用虛擬地址空間,即進程空間,參見:

進程是如何使用內存的?

符號解析和指令的修正

       ELF文件中定義了一個重定位表段,裏面定義了須要在連接階段進行重定位的符號。hello.c編譯成hello.o文件後,裏面的printf函數並無在hello.o中實現,所以會放在重定位段中。連接的時候,會在全部的.o文件中查找未定義符號表,並將符號定義的首地址相對引用地址求得偏移值後填入引用處。好比咱們在main函數中引用的printf函數,編譯階段地址爲0,連接階段會填上0x2004。

若是存在未找到的符號,鏈接失敗編譯器報錯,就是咱們常常見到的:

undefined reference to "XXXX"


剛纔咱們看到libc.a文件打散以後是一堆.o文件,就包括printf.o文件,裏面定義了printf函數的實現。通過迭代查找,設置好程序入口,連接工做就完成了。

       連接過程比較複雜,包括絕對地址重定位和C++中重複代碼處理等等,須要在項目中試錯理解,後續有空再續寫。


本文分享自微信公衆號 - 機械猿(on_ourway)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索