編譯和鏈結那點事《上》

2021-09-23 22:58:25 字數 3826 閱讀 8327

有位學弟想讓我說說編譯和鏈結的簡單過程,我覺得幾句話簡單說的話也沒什麼意思,索性寫篇博文稍微詳細的解釋一下吧。其實詳細的流程在經典的《linkers and loaders》和《深入理解計算機系統》中均有描述,也有國產的諸如《程式設計師的自我修養——鏈結、裝載與庫》等大牛著作。不過,我想大家恐怕很難有足夠的時間去研讀這些厚如詞典的書籍。正巧我大致翻閱過其中的部分章節,乾脆也融入這篇文章作為補充吧。

我的環境:fedora 16 i686 kernel-3.6.11-4 gcc 4.6.3

閒話不多說了,我們進入正題。在正式開始我們的描述前,我們先來引出幾個問題:

c語言**為什麼要編譯後才能執行?整個過程中編譯器都做了什麼?

c**中經常會包含標頭檔案,那標頭檔案是什麼?c語言庫又是什麼?

有人說main函式是c語言程式的入口,是這樣嗎?難道就不能把其它函式當入口?

不同的作業系統上編譯好的程式可以直接拷貝過去執行嗎?

如果上面的問題你都能回答的話,那麼後文就不用再看下去了。因為本文是純粹的面向新手,所以注定了不會寫的多麼詳細和深刻。如果你不知道或者不是很清楚,那麼我們就一起繼續研究吧。

我們就以最經典的helloworld程式為例開始吧。我們先使用vim等文字編輯器寫好**,接著在終端執行命令 gcc helloworld.c -o helloworld 輸出了可執行檔案helloworld,最後我們在終端執行 ./helloworld,順利地顯示了輸出結果。

可是,簡單的命令背後經過了什麼樣的處理過程呢?gcc真的就「直接」生成了最後的可執行檔案了嗎?當然不是,我們在gcc編譯命令列加上引數 –verbose要求gcc輸出完整的處理過程(命令列加上 -v 也行),我們看到了一段較長的過程輸出。

輸出結果我們就不完整截圖了,大家有興趣可以自己試驗然後試著分析整個流程。

從圖中我們大致可以看出gcc處理helloworld.c的大致過程:

預處理(prepressing)—>編譯(compilation)—>彙編(assembly)—>鏈結(linking)

我們一步一步來看,首先是預處理,我們看看預處理階段對**進行了哪些處理。

我們在終端輸入指令 gcc -e helloworld.c -o helloworld.i,然後我們開啟輸出檔案。

首先是大段大段的變數和函式的宣告,汗..我們的****去了?我們在vim的普通模式中按下shift+g(大寫g)來到最後,終於在幾千行以後看到了我們可憐兮兮的幾行**。

前面幾千行是什麼呢?其實它就是 /usr/include/stdio.h 檔案的所有內容,預處理器把所有的#include替換為實際檔案的內容了。這個過程是遞迴進行的,所以stdio.h裡面的#include也被實際內容所替換了。

而且我在helloworld.c裡面的所有注釋被預處理器全部刪除了。就連printf語句前的tab縮排也被替換為乙個空格了,顯得**都不美觀了。

時間關係,我們就不一一試驗處理的內容了,我直接給出預處理器處理的大致範圍吧。

基本上就是這些了。在這裡我順便插播乙個小技巧,在**中有時候巨集定義比較複雜的時候我們很難判斷其處理後的結構是否正確。這個時候我們呢就可以使用gcc的-e引數輸出處理結果來判斷了。

前文中我們提到了標頭檔案中放置的是變數定義和函式宣告等等內容。這些到底是什麼東西呢?其實在比較早的時候呼叫函式並不需要宣告,後來因為「筆誤」之類的錯誤實在太多,造成了鏈結期間的錯誤過多,所有編譯器開始要求對所有使用的變數或者函式給出宣告,以支援編譯器進行引數檢查和型別匹配。標頭檔案包含的基本上就是這些東西和一些預先的巨集定義來方便程式設計師程式設計。其實對於我們的helloworld.c程式來說不需要這個龐大的標頭檔案,只需要在main函式前宣告printf函式,不需要#include即可通過編譯。.h>

宣告如下:

int printf(const char *format, ...);
這個大家就自行測試吧。另外再補充一點,gcc其實並不要求函式一定要在被呼叫之前定義或者宣告(msvc不允許),因為gcc在處理到某個未知型別的函式時,會為其建立乙個隱式宣告,並假設該函式返回值型別為int。但gcc此時無法檢查傳遞給該函式的實參型別和個數是否正確,不利於編譯器為我們排除錯誤(而且如果該函式的返回值不是int的話也會出錯)。所以還是建議大家在函式呼叫前,先對其定義或宣告。

預處理部分說完了,我們接著看編譯和彙編。那麼什麼是編譯?一句話描述:編譯就是把預處理之後的檔案進行一系列詞法分析、語法分析、語義分析以及優化後生成的相應彙編**檔案。這一部分我們不能展開說了,一來我沒有系統學習過編譯原理的內容不敢信口開河,二來這部分要是展開去說需要很厚很厚的一本書了,細節大家就自己學習《編譯原理》吧,相關的資料自然就是經典的龍書、虎書和鯨書了。

gcc怎麼檢視編譯後的彙編**呢?命令是 gcc -s helloworld.c -o helloworld.s,這樣輸出了彙編**檔案helloworld.s,其實輸出的檔名可以隨意,我是習慣使然。順便說一句,這裡生成的彙編是at&t風格的彙編**,如果大家更熟悉intel風格,可以在命令列加上引數 -masm=intel ,這樣gcc就會生成intel風格的彙編**了(如圖,這個好多人不知道哦)。不過gcc的內聯彙編只支援at&t風格,大家還是找找資料學學at&t風格吧。

再下來是彙編步驟,我們繼續用一句話來描述:彙編就是將編譯後的彙編**翻譯為機器碼,幾乎每一條彙編指令對應一句機器碼。

這裡其實也沒有什麼好說的了,命令列 gcc -c helloworld.c 可以讓編譯器只進行到生成目標檔案這一步,這樣我們就能在目錄下看到helloworld.o檔案了。

linux下的可執行檔案以及目標檔案的格式叫作elf(executable linkable format)。其實windows下的pe(portable executable)也好,elf也罷,都是coff(common file format)格式的一種變種,甚至windows下的目標檔案就是以coff格式去儲存的。不同的作業系統之間的可執行檔案的格式通常是不一樣的,所以造成了編譯好的helloworld沒有辦法直接複製執行,而需要在相關平台上重新編譯。當然了,不能執行的原因自然不是這一點點,不同的作業系統介面(windows api和linux的system call)以及相關的類庫不同也是原因之一。

我們接下來看最後的鏈結過程。這一步是將彙編產生的目標檔案和所使用的庫函式的目標檔案鏈結生成乙個可執行檔案的過程。我想在這裡稍微的擴充套件一下篇幅,稍微詳細的說一說鏈結,一來這裡造成的錯誤通常難以理解和處理,二來使用第三方庫在開發中越來越常見了,想著大家可能更需要稍微了解一些細節了。

我們先介紹gnu binutils工具包,這是一整套的二進位制分析處理工具包。詳細介紹請大家參考餵雞百科:

我的fedora已經自帶了這套工具包,如果你的發行版沒有,請自行搜尋安裝方法。

另外,上文部分內容因為考慮到讀者基礎,所以行文力求簡明易懂,部分描述並不嚴密且有部分刻意的簡化和保留。

預編譯那點事

js在執行時會依次進行三步工作。首先看一段 var a 3 console.log a 假設 只有這兩行,那麼他是怎麼進行解析的呢?首先建立全域性物件go global object 將變數的值以屬性值掛載到物件上,但是此時值為undeifind 此動作發生在 執行前一刻 編譯完成開始執行 賦值 此...

關於友情鏈結連坐那點事

友情鏈結是高質量外鏈的一種,交換友情鏈結是基本所有站長都幹過的事,但友情鏈結容易發生連坐的悲劇,所以很多站長都很小心,會定期對友情鏈結進行檢查,一旦發現對方排名 快照不對就馬上撤掉該鏈結。其實這是一種不明智的行為,不是所有的友情鏈結出問題都會連坐。那麼 什麼時候會連坐?為什麼 沒被連坐排名卻下降?接...

Canvas和Paint那點事(2)

最近在研究乙個音訊圖的繪製,用到了canvas畫圖方法,乙個奇怪的問題困擾了我好久,最後終於解決了。本來是想得到這種不斷跳動的音訊頻譜柱狀圖的 誰能想到,本想要個格格,誰知道來了個嬤嬤。得到了這樣的效果 方法一 canvas.drawcolor color.transparent,porterduf...