2.深入一點理解C源程式的編譯過程

菲拉拉發表於2021-01-03

2.深入一點理解C源程式的編譯過程

本文章的大多數靈感及知識來源於南京大學的計算機系統基礎教材,如果希望更加深入地對相關只是做進一步瞭解,可以移步上述相關資源。在網上可以輕易獲得上述資源,mooc也有相關課程。也可閱讀《深入理解計算機系統》。

 

本文章中所有圖片均來自網際網路,如有侵權,勞請聯絡。

 

另外,轉載請標明出處哦,語雀:https://www.yuque.com/yifeideshijie

 

image

1.直接點!一個.c檔案到一個可執行檔案到底經歷了什麼?

 

我們以在Linux下采用gcc進行編譯為例,一個C源程式首先會經歷如下幾個階段:

 

image.png

 

1.預處理

預處理程式(cpp)完成,這一階段的任務較為簡單,你只需要把以#開頭的命令執行即可,正如第1篇中講的那樣,#include所包含的檔案中的內容會被直接複製到檔案中,#define命令所指示替換的內容也會被替換,現在,我們得到了一個.i檔案,即預處理之後得到的檔案,該檔案與.c檔案一樣,依然是一個文字檔案

 

2.編譯階段

整個過程中最燒腦最複雜的過程出現了,沒錯,這就是編譯,整個編譯階段有編譯程式(cc1)完成,得到彙編源程式(.s)檔案這個過程最為複雜,還包含詞法分析,語法分析,語義分析等階段,由於這些過程很複雜,這裡就不展開了,在這一過程中也是編譯報錯的主要來源。值得注意的是,這裡的編譯階段與我們經常所說的將源程式“編譯”為可執行檔案中的編譯是不一樣的,由於整個過程中編譯階段最為複雜和重要,所以我們就直接稱之為編譯,實際上整個過程遠非僅僅如此。

 

需要注意的是,編譯階段最後得到的.s系統與計算機採用的ISA(指令集體系結構)有關,這是由於每一個ISA對應其特定的彙編指令集,自然其翻譯得到的彙編程式碼也就不一樣。

ISA直接決定了物理機的設計,通常情況下,你的機器一般為x86_64/x64/amd64(三者指的是一種ISA,是由Intel的32位指令集發展而來,但是是由amd公司完成的,注意Intel公司自己開發的是IA-64,但用的不多,不是主流,商業上可稱之為失敗,與前述的ISA並非一物),也就是intel的機器,當然,我們可能學習的主要是IA-32/x86(兩者均指的是Intel公司的32位指令集,IA指的是Intel Architecture,這個指令集由Intel公司發明,商業上獲得了巨大成功,直接促成了amd64在相容IA-32的情況下獲得了成功,成為現在PC端的主流ISA)。當然還存在RISV32/64和MIPS32/64等指令集架構。

 

也就是說,在同一臺機器上,所有的高階語言源程式最終都會翻譯成相同組合語言表示的彙編源程式,但是對於採用不同ISA的機器來說,同一個高階語言源程式最終也會翻譯成不同組合語言表示的彙編源程式。

 

這也就是為什麼,我們在下載軟體的時候,需要先明確自己的機器採用的指令集,如果你用的是主流IntelCPU機器的話,大多數情況下你可能下載的是x86-64或者amd64版本(假設你的機器是64位機)的執行程式。

 

3.彙編階段

得到了.s的彙編原始碼,再把相應彙編源程式通過對應的彙編程式(as)進行彙編生成可重定向目標程式(.o)檔案就容易了。

這裡解釋為什麼我們得到的檔案是可重定向檔案且不能直接執行的原因。

重定向是說,生成的目標檔案中有一些符號的入口地址或者其對應的指向還沒有明確,例如我們在main.c檔案中還需要呼叫來自a.c檔案中的help()函式,在得到的可重定向目標檔案中,自然不知道help()函式的入口地址到底在哪裡了。

 

4.連結階段

這一階段的目標很明確,即需要將檔案中那些只是宣告但沒有定義的符號通過與其他的.o檔案連結,從而明確這些函式或者變數的具體指向,這樣,在相應的連結程式(ld)執行完相關操作後,我們就可以得到一個可執行目標程式了。

 

 

 

2.你需要了解一些在彙編和連結過程中的彩蛋

通過上面的閱讀你可能發現了一個bug,因為你在實際中遇到了這樣一種情況,同樣使用IntelCPU的裝了Windows的聯想機器和裝了Mac作業系統的蘋果機器,在下載軟體的時候為什麼需要下載不同的檔案呢?他們的指令集不是一樣的嗎?

image

 

這是因為,彙編程式將彙編源程式翻譯為可重定向目標檔案時,作業系統也在其中插了一腳,沒錯,生成的目標檔案不僅與ISA有關,還與作業系統有關。

 

使用者直接與作業系統打交道,給作業系統的程式必須得先讓作業系統理解,才能由作業系統轉交給機器執行,不同的作業系統能夠理解的目標檔案格式是不同的,由實現的廠家自行決定,這也就是說,同樣的應用程式還要為不同的作業系統給出不同的可執行檔案。Windows使用的是可移植可執行格式(COFF),DOS採用的是COM檔案格式。

image

 

下面我們以現代UNIX系統採用的目標檔案格式為例(如Linux),這一格式叫做可執行可連結格式,我們稱之為ELF格式,由彙編程式(沒錯,如果作業系統理解的目標檔案格式不同,自然其對應的彙編程式和連結程式都不一樣)彙編得到的ELF格式的可重定位目標檔案格式如下:

image.png

 

 

在該格式之下,程式中的相關資料被有組織地放在了每一個節中,不同的節描述不同型別的資訊的特徵,上面的圖中有所描述,主要注意.text和.rodata節,這裡面主要儲存了相關的執行程式碼;以及.data和.bss節,這裡主要儲存全域性變數資料。如若希望就此由更為深入的理解,你或許應該詳細閱讀南大的計算機系統基礎這本書或者其他教材。

 

當然,這還只是可重定向的ELF檔案格式,還要經過連結器得到可執行的目標檔案。

 

首先,我們得知道連結器幹了什麼,無非兩件事,符號解析和重定向,至於二者詳細內容,那就需要詳細閱讀教材了,不過大體上將,符號解析就是你得知道該可重定向目標檔案中的符號都指向了哪裡,是指向本目標檔案中定義的符號還是其他模組中定義的符號;重定向的目的是為最終的執行服務的,最終執行只要在記憶體上的,重定向就是為了解決最後這個目標程式到底在哪裡執行,但是遺憾的是,重定向功能並非真正將程式指向一片指定的空間,而是講其對映到虛擬儲存空間。

 

那?什麼又叫虛擬儲存空間呢,虛擬的意思就是假的,並非真的,例如所有可執行目標檔案的只讀程式碼段都對映到了0x08048000開始的一段區域,可實際執行中顯然不可能所有執行程式都放在這一段位置,但這樣的工作並非沒有意義,這樣的工作完成了一個很重要的工作,即這些符號的相對位置,連結器將所有的可重定向目標檔案的相同的有必要留下的節合併,並通過.rel.text節和.rel.data節中的資訊確定每個符號的虛擬地址。

 

這樣,我們最終就得到了可執行目標檔案(ELF檔案格式)的最終的狀態:

image.png

得到的這個可執行檔案應該是可以直接對映到儲存上的,因為作業系統要直接那這個檔案到物理機器上執行了,所以,該檔案中的內容是可以直接對映到儲存上的,如下,我們以另一種視角看這個目標檔案:

image.png

正如上圖所示,執行的時候檔案中一部分內容直接對映到實體地址中,對應的部分我們稱之為只讀段(程式碼段)和讀寫段(資料段),其餘的在執行時,還包括堆區和棧區。當然,右圖的只是虛擬映像,可能並非實際中的真正的儲存映像,這取決於重定位型別,IA-32處理器有基本的兩種重定位型別:

R_386_PC32 : 相對定址,有效地址為PC加上重定位後地址

R_386_32: 絕對地址,即重定位後就是記憶體中地址

 

我們再加一個程式實際執行時的彩蛋:

image

 

 

沒錯,這裡是很繁瑣。

 

耐心一點吧,總會過去的。

 

其實這裡應該是在執行的時候再詳細講的,但這裡直接趁熱打鐵講了吧。

 

在實際執行時,機器也不會就直接把目標檔案中的程式碼段,資料段一股腦地載入到記憶體裡,他會先耍個小聰明,先不載入,什麼時候要這個資料了再去載入,當然,這裡就涉及到了記憶體管理,即記憶體中發生缺頁(資料沒有載入到記憶體,但我現在要用)時,需要怎麼辦,這裡又得扯出一大堆東西了。總之,實際執行時存在很多小技巧,具體的,可以通過作業系統或者組員或者系統結構相關書籍課程學習。

 

這個彩蛋它很香嗎?

image

 

你或許覺得這個彩蛋講的雲裡霧裡,確實,它缺少很多的細節,但這裡我並不想牽扯太多細節,即便如此粗略,它依然涉及了計算機系統中一個重要的思想:虛擬技術。我想表達的是,在涉及到程式最終的執行過程時,你會發現受限於目前物理儲存與匯流排資料傳輸技術,大量的基於虛擬的技術應運而生,永遠記住,IO是最花費時間的,能不IO就不IO,例如上面所講的,為了避免因將磁碟上的資料載入到記憶體中的IO時間消耗,直接先不載入,用的時候再載入,這樣不就不需要一次性大量的IO浪費時間且佔用寶貴的記憶體了嗎?

 

同樣地,在上層應用中,也存在虛擬技術,最直接的就是虛擬機器,通過軟體模擬一臺機器,這樣,在這臺機器上還能再跑一個作業系統,這樣一層疊一層。

 

於是,我們開始理解,計算機是一個典型的層次結構,上一層是下一層的抽象,下一層為上一層提供基礎性功能,上一層將這些功能在抽象簡化後提供給上上一層使用,在整個層次系統的最上層就是我們使用者,而最底層就是機器,是數位電路,是類比電路,是物理規律。這或許是整個計算機系統的大體思想——分層次。

 

在計算機網路中你也會看到這樣的典型的分層思想。

 

 

總之,目前為止,我們終於得到了一個可執行檔案了(ELF格式),好了,我們要開始執行它了。

 

3.天殺的,我終於要執行了

好了,作業系統拿到這個可執行檔案了,它要開始執行了,這一部分的內容就涉及到計算機組成原理了,在扯下去就偏題了,因此我不打算在這裡再過多的說(主要是我也忘記差不多了,,,)

 

總之作業系統將可執行目標檔案放在了記憶體上,CPU開始從記憶體中取指令到PC暫存器開始譯碼,取運算元,計算,得到結果寫回運算元等一系列流水操作。並最終得到結果給使用者。

 

至於作業系統又如何實現多程式,CPU又如何實現流水線技術,在缺頁的時候如何產生中斷到磁碟讀資料等等問題就過於繁瑣,不適合細談了,得有待進一步系統性的學習,這裡只是起一個引路的作用。

 

4.好了,終於結束了

你沒看錯,結束了。

 

不知道你是意猶未盡,覺得講的細節太少,還是覺得雲裡霧裡,不知所云。如果你剛剛上大學,剛剛步入大二或者大三,這些東西或許你聽過一些,但不是全都懂,但這並沒有什麼關係,本人能力有限,絕非學霸,只能粗略地就大體框架系統講一下自己的觀點,細枝末節還有待你深入學習了。

 

如果你是大佬,你或許覺得這些簡直就是小兒科,不足一提,你想要的細節性的技術性的完全沒有。沒錯,確實如此,因為我也有很多東西不會,還需要深入瞭解,如果看完文章覺得我對於整個計算機學科的理解有偏差或者存在不足的話,真的請你能夠幫我指出來,必感激不盡。

 

好了,這一篇的內容就此結束了。

 

 

image

 

相關文章