C++:Linux下編譯鏈接原理底層演示分析

Linux下編譯鏈接原理演示:

我們一個可執行文件的產生其原理大致如圖所示:
在這裏插入圖片描述
若想更加透徹瞭解編譯鏈接原理,詳情可參考該博客:
C++:一個C/C++源文件從文本變成可執行文件的過程

一、編譯原理底層演示分析

.o文件:其實質是由各種各樣的段組成。,分區域每一塊存了不同的東西。
在這裏插入圖片描述
我們在這裏是爲了通過演示Linux下底層具體實現,更好的瞭解編譯鏈接基本原理。
我們這裏有兩個文件,main.cpp與sum.cpp;
main.cpp:
在這裏插入圖片描述
sum.cpp:
在這裏插入圖片描述
我們來一起瞧一瞧:
①將main.cpp,sum.cpp編譯生成.o文件
在這裏插入圖片描述
②那麼.o文件組成格式是什麼?我們無論什麼程序,最終產生的結果爲:指令或數據。因此,我們使用 objdump 命令可以查看.o文件和一些可執行文件詳細信息。
查看main.o與sum.o的符號表:符號表是彙編器把彙編文件轉成.o文件時候,會給文件生成符號表與各種段。可以使用readelf -h 文件名來查看文件頭。

爲了更好的對比,我們將代碼與符號表一起來看。
mian.cpp: 定義了data,main函數以及引用了gdata與sum函數。
在這裏插入圖片描述
sum.cpp: 定義了一個全局變量gdata與全局函數sum。
在這裏插入圖片描述

問題一:如果當前文件引用外部文件函數或全局變量的符號時候,再當前main.cpp編譯成的main.o文件中,會產生sum的符號與gdata的符號呢?
符號定義與符號引用: 這裏會產生符號的,我們觀察上圖,main函數在main.cpp中定義,最後會放在.text段;data也是main.cpp定義的,爲初始值不爲0的全局變量,最終放在.data段,這是符號已經定義好的;那麼gdata與sum呢?仔細觀察上圖,都產生符號了! 但是它們都在 * UND * 。 這是什麼意思呢?這是未定義段,我們這個符號在代碼中用到它們了,但暫時並不知道它們是如何定義的,因此在當前的符號表中只能將它們放入未定義;這段過程是符號的引用。

       local與global: l是locall局部符號,只能在當前文件中看見;g爲global,在其他文件中也可以看到。鏈接時,所有obj文件在一起鏈接的,對於鏈接器來說它只能看見.o文件中的global符號,local符號看不見。例如:我們定義了一個靜態全局變量或者靜態函數,只能當前文件可見,其他文件不可見,因此可以在多個文件中定義名字相同的靜態全局變量,因爲它們只能在當前文件可見。
       我們來看看sum.cpp中gdata與sum函數,分別在.data段與.text段,說明這是它們定義的地方,它們都爲global符號。
最終我們main.cpp中生成的符號爲:
在這裏插入圖片描述
最終我們sum.cpp中生成的符號爲:
在這裏插入圖片描述
這裏還可以使用objdump -s 文件名直接查看常見的段:
在這裏插入圖片描述
.o文件由各種各樣的段組成,那如何查看所有段呢?我們可以使用readelf -S main.o將其所有段也打印出來。
在這裏插入圖片描述
從上面我們還得出一個結論:編譯過程中,符號是不分配虛擬地址的,只有在鏈接階段才分配。

問題二:那爲什麼編譯後的文件無法直接運行呢?
首先,我們帶上調試信息執行g++ -c main.o -g,再執行objdump -S main.o;
在這裏插入圖片描述
      我們來看一看,不管是訪問自己文件的.cpp還是其他文件的.cpp地址都是沒有的。main函數從高級源代碼轉爲彙編是在編譯時候做的,這個指令無法執行,因爲符號的地址不可能是零地址。編譯過程中符號還沒有分配地址,指令的產生在編譯階段會產生好。符號的地址不確定,會在指令上面,暫時將符號地址都填爲0;這也是.obj文件無法運行的原因。
 

二、鏈接原理底層演示分析

符號解析: 所有.o文件進行合併,各個段進行合併了,包括段表符號表全都進行合併,進行符號解析:所有對符號的引用,都要找到該符號定義的地方。例如:* UND *就是對符號的引用。對符號的引用可以出現多次,但是定義只能出現一次,符號未定義或重定義都會出錯。最終產生的可執行文件也是各種各
給所有的符號分配虛擬地址: 符號解析完成後,給所有符號分配虛擬地址。
符號的重定向: 符號具體的地址寫到指令上。
我們來具體演示一下:
用ld命令進行相應的鏈接處理:ld -e main *.o進行連接,再使用objdump -t命令查看。
在這裏插入圖片描述
我們可以看到。所有的符號都分配了地址,使用objdump -S再來查看:
在這裏插入圖片描述
所以,符號分配地址是在鏈接過程的第一步,符號解析完成後分配虛擬地址,再將 * UND *改爲符號正確的地址。

這裏也解決了我們一個問題:爲什麼程序執行時會從main函數第一行指令開始運行
我們來看看可執行文件的文件頭:之前爲可重定向文件,此時變爲可執行文件。
在這裏插入圖片描述
文件頭記錄了當前可執行程序的入口地址,所以會從main函數第一行指令開始執行。
在這裏插入圖片描述
 

三、可執行文件與可重定位文件區別

前面瞭解了,那麼可執行文件與可重定位文件有什麼區別?
基本結構都一樣,爲各種各樣的段,但是可執行文件多了一個progrma headers段。progrma headers放了兩個LOAD:加載。運行時候有那麼多段,運行時不一定全都加載,系統查看progrma headers的兩個LOAD,告訴系統運行這個程序的時候將哪些內容加載到內存當中。 需要加載的只有代碼段和數據段。
在這裏插入圖片描述
可執行文件加載的大致過程:a.out在磁盤上,裏面有各種各樣的段,elf header裏面存放了程序的入口地址,program headers將.text段,.data段加載到內存中,映射到到進程的虛擬地址空間中。
在這裏插入圖片描述