C++編譯和鏈接過程

我們知道,用c/c++語言編寫程序的時候,必須要經過編譯和鏈接過程,才能將我們c/c++的源代碼轉化爲可執行文件(Windows下是.exe程序,Linux下是elf格式的可執行文件)。

那麼編譯和鏈接過程到底做了什麼,這個可執行文件又是被加載到哪裏運行的呢

數據和指令

無論用哪種語言所寫的代碼,歸根結底會產生兩種東西:指令和數據。那哪些代碼是指令,哪些代碼是數據?

所有的全局變量和靜態變量都是數據,除此之外都是指令(包括局部變量)。我們來看一段代碼

很明顯在上面這段代碼中,data1,2,3,4,5,6,7,8,9都是數據,除了這些之外其他的代碼都是指令。我們都知道,程序是被加載到內存上運行的,既然代碼被分成了指令和數據兩種不同的東西,那麼在內存中他們就不可能被無序的,混亂的放在一起,肯定會有一定的劃分規定。

虛擬地址空間

在每個程序運行的時候,我們的操作系統都會給他分配一個固定大小的虛擬地址空間(x86,32bit,Linux內核下默認大小爲4G),那這段內存是怎樣分配的呢?我們來看一下

這就是虛擬地址空間各個區域的分佈圖,從圖中可以看出,整個4G的空間有1G是供操作系統使用的內核空間,用戶無法訪問,還有3G是我們的用戶空間,以供該虛擬地址空間上進程的運行。在這3G的用戶空間中又被分成了很多段,從0地址開始的128M大小是系統的預留空間,用戶也是無法訪問的,接下來是.text段,該段空間中存放的是代碼,然後是.data段和.bss段,這兩段裏面存放的都是數據,但又有不同:.data段中存放的數據是已經初始化並且初始化值不爲0的數據,而.bss段中存放的是未經初始化或者初始化爲0的數據。我們可以看一下Linux下虛擬地址空間的分配狀況

上面是Linux下一塊進程的虛擬地址空間的段表信息,我們可以清楚的看到上面有我們的.text段,.data段,.bss段等信息,說明這些段是真實存在的,而不是我們人爲臆造出來的。但是我們在查看段表信息的時候會發現,.bss段的起始地址和.comment段的起始地址是相同的,這是爲什麼呢?bss三個英文字母的含義是:better save space(更好的節省空間),這裏所節省的空間是誰的空間呢?我們知道程序運行的時候要從文件中加載到內存上,而上面這個段表信息是在生成中間文件.o文件時所調出來的,也就是說此時還沒有生成可執行文件,自然系統也不會爲其分配虛擬地址空間,因此顯而易見,這裏所節約的是文件的空間,

也就是說,在生成的中間文件中,系統並沒有爲.bss段分配空間,那其中的數據是如何保存的呢,其實不難理解。我們先把這個問題保留下來,後面會解決。現在我們來看一下編譯和鏈接過程主要做了什麼。

 

編譯過程

編譯過程中,系統主要會做三件事:預編譯,編譯,彙編。

預編譯: 去掉代碼中的註釋,處理以“#“號開頭的預處理命令,進行宏替換

編譯:生成符號,將源代碼的指令轉化爲彙編指令

彙編:生成二進制可重定位文件

這裏我們比較陌生的就是生成符號了,也是我們的重點。c/c++代碼在編譯時會生成符號,所有的數據都會生成符號,而指令只有函數名會生成符號。我們在返回我們上面那個段表中看一下,我們知道上面的代碼中會有六個數據存放在.bss段,可是.bss段的大小隻有20個字節也就是說其中只有5個數據(16進制的14轉換到十進制是20),那另外的那個數據難道丟了嗎??其實,生成符號的過程中,所有靜態的變量生成的符號是local符號(僅當前文件可見),所有初始化了的非靜態全局變量都會生成一個global(所有文件可見)的強符號,而未被初始化的非靜態全局變量就會生成一個global的弱符號。弱符號是不確定的符號,不確定是否有其他文件中同名的變量會生成強符號,或者其他同名的變量雖然生成弱符號,但是所佔的內存比該弱符號大,以上兩種情況,在鏈接過程中,該弱符號都會被替換掉(強符號若同名會產生編譯錯誤,在編譯時就已經確定,弱符號在鏈接時才確定)。那很明顯上面的代碼中,data3會產生弱符號,因此在編譯時弱符號不會被存儲在.bss段,而是被保存在comment塊中,我們來看一下符號表

我們看到,未被初始化的Data3並未被存放在.bss段,而是在comment塊中,因爲他是一個弱符號。此時我們在另外一個文件中定義一個變量和一個函數,在main.c文件調用這個函數和變量

我們再看一下符號表

可以明顯的看到最下面的兩個*UND*的符號,這是因爲在編譯的過程中,是每個文件單獨編譯的,不會看到其他文件中定義的東西,在main.c中只有變量data和函數fun()的聲明,所以他們會被認爲是未定義的符號。

鏈接過程

編譯完成之後,緊接着會進行鏈接過程,我們先來看一下來鏈接過程到底做了什麼

合併段

在elf文件中字節對齊是以4字節對齊的,但是在可執行程序中對齊方式是以頁的方式對齊的(一個頁的大小爲4k),因此如果我們在鏈接時將各個.o文件各個段單獨的加載到可執行文件中,將會非常浪費空間:如下表

因此我們需要合併段,調整段偏移,將各個文件不同的段合併起來,每個.o文件的.text段合併在一起.data段合併在一起,這樣,在生成的可執行 文件中,各個段都只有一個,如下圖,由於在鏈接時只需要加載代碼段(.text段)和數據段(.data段和.bss段)。因此合併段之後,在系統給我們分配內存時,只需要分配兩個頁面大小就可以,分別存放代碼和數據如圖

調整段偏移

合併段之後,必須進行的一個操作就是調整段偏移和段長度。每個進程都有自己的虛擬地址空間,都是從0地址開始的,將各個文件的各個段加載進來之後,段的大小會有所變化,相對於0地址的偏移量也會不同,因此我們需要調整段偏移和段偏移如圖

彙總所有符號

每個obj文件在編譯時都會生成自己的符號表,所以我們要把這些符號都合併起來進行符號解析

完成符號的重定位

在進行合併段,調整段偏移時,輸入文件的各個段在連接後的虛擬地址就已經確定了,這一步完成後,連接器開始計算各個符號的虛擬地址,因爲各個符號在段內的相對位置是固定的,所以段內各個符號的地址也已經是確定的了,只不過連接器需要給每個符號加上一個偏移量,使他們能夠調整到正確的虛擬地址,這就是符號的重定位過程

在 elf文件中,有一個叫重定位表的結構專門用來保存這些與從定位有關的信息,重定位表在elf文件中往往是一個或多個段運行
————————————————
版權聲明:本文爲CSDN博主「Mr_H9527」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/Mr_H9527/article/details/81156112

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章