《深度探索Linux系統:系統構建和原理解析》筆記——2.工具鏈
1. 工具鏈的工作過程
如下是從原始檔到二進位制檔案的構建過程,需要注意的是 連結階段,連結和包含起始程式的目標檔案(crti.o ...)
實際情況我們只需要 gcc main.c即可完成編譯連結,實際上,gcc只是驅動程式,控制 編譯器cc1,彙編器as,聯結器ld 工作。
使用 gcc -v main.c 可以看到工作的具體流程。
2. 構建階段
2.1 預編譯
gcc -E main.c -o main.i
有預編譯器cpp,但編譯器通常實現了預編譯功能,所以gcc直接使用cc1進行預編譯
主要完成:
- 檔案包含:將包含的檔案的內容複製到原始檔中
- 巨集展開
- 條件編譯
2.2 編譯
gcc -S main.c
通過詞法分析,語法分析,語義分析,並生成中間程式碼,並對中間程式碼進行優化,以生成效率更高,更小的可執行程式碼,
最終生成彙編程式碼,實際生成的彙編程式碼中會很多彙編偽指令,目的是為接下來的連結做準備和避免程式碼優化導致的除錯資訊丟失。
2.3 彙編
將彙編指令翻譯成機器指令,
建立連結時需要的資訊,包括符號表和重定位表等。
和其他階段不同,彙編的輸出為二進位制格式檔案,在Linux中為 ELF格式檔案。包括目標檔案,靜態庫,動態庫,可執行檔案都是ELF格式。
2.3.1 ELF格式
檔案頭部資訊:描述整個檔案的基本屬性,包括:本檔案執行在什麼作業系統,什麼硬體體系,程式入口地址等。最重要的兩個表的資訊,表的位置和條目數。
一張表是 Section Header Table, 連結時使用,記錄各段的位置,長度等。
一張表是 Program Header Table, 供核心和動態載入器載入ELF檔案到記憶體時使用。
所以,對於目標檔案,只需要連結,不需要執行,所以只有Section Header Table,沒有 Program Header Table
檢視 檔案頭資訊
readelf -h main.o
檢視 Section Header Table資訊
readelf -S main.o
在檔案頭資訊後就是各個段:
.text 程式碼
.data 初始化的資料
.bss 未初始化的資料,由於未初始化的資料預設為0,所以不需要在elf檔案中佔用空間,只需要告訴系統分配對應大小的空間,並初始化為0即可。
一個目標檔案的elf如下
.symtab :符號表
read -s main.o 檢視符號表
2.3.2 重定位表
在彙編一個模組時,由於無法知道匯入符號的執行地址,所以給這些符號虛擬一個地址(通常為空),在連結時,確定了這些符號的執行地址,再修訂這些符號的位置,這個過程稱為重定位。
除了編譯時重定位還有載入時重定位和執行時重定位,這裡只討論前者。
連結其會為目標檔案找到其外部符號的定義,為了方便連結器工作,目標檔案需要建立一個表,每個表項為外部符號,這個表稱為重定位表。
重定位表的表項為如下兩種格式:
r_offset 符號值在目標檔案中的偏移值
r_info 符號名稱在符號表的索引
r_addend 用於輔助計算修訂值
檢視重定位表
readelf -r hello.o
下面說明了哪些符號需要重定位
2.3.3 符號表
因為連結器需要重定位符號,所以需要知道這些符號定義在哪裡,所以目標檔案需要建立一個表,記錄自己的匯出符號。
value:符號執行地址,連結時才分配執行時地址,所以這裡值都為空
size:符號對應實體佔用記憶體大小
type:符號型別,object指變數型別
bind:符號繫結資訊,local為內部符號,global為全域性符號
ndx:符號在哪個段,對於main在1即.text段,對於foo2,foo2_func是匯入符號,未定義,所以是UND
在連結時會用符號表進行重定位,若刪除符號表,連結會找不到符號定義而報錯
2.4 連結
連結將多個目標檔案,靜態庫,動態庫,合成一個檔案
連結分為兩個階段:
- 將多個檔案合成一個檔案,若輸出可執行檔案,還需要為指令和符號分配執行時地址
- 進行符號重定位
2.4.1 驗證合併
合成多個檔案就是將多個目標檔案中的相同型別段合併到一個檔案
下面的示例可以看出,可執行檔案被分配了執行時地址,
多個目標檔案的.text的大小和,遠小於可執行檔案的.text大小,因為連結器還連結了包含啟動程式的目標檔案(crt1.o...)
手動連結,-e指定入口地址為main,發現大小為 0x48,但我們希望為0x46。
更換檔案順序,大小成功為0x46,原因是,32機上鍊接需要4位元組對齊,hello.o大小為26,需要補2個位元組,foo.o,foo2.o都是4位元組對齊的,所以若hello.o做首位需要對齊,若hello.o做末尾,不需要對齊
2.4.2驗證重定位
重定位公式
重定位型別為 R_386_32,公式為 S+A
重定位型別為 R_386_PC32, 公式為 S+A+P
S:執行時地址
A:Addend
P:修訂處執行時地址或偏移,對於目標檔案,P為修訂處在段內偏移,對於可執行檔案或動態庫,P為修訂處的執行時地址。
首先檢視hello.o需要重定位的符號,記住其在.text段中的偏移值為0x0b, 0x1b
執行時地址在連結後確定,檢視hello的符號段,獲得foo2, foo2_func 地址為0x0804a020, 0x08048414
根據偏移值,確定Addend,分別是0, -4
可以計算foo2的重定位地址為:
S + A = 0x0804a020 + 0 = 0x0804a020
計算foo2_func還需要確定P,即引用符號的執行時地址,
0x080483f6 + 1 = 0x080483f7
計算為:
S + A + P = 0x08048414 -4 + 0x080483f7 = 0x19
連結器合併段,確定了各個符號的執行地址,並在重定位時,直接修改了需要重定位的符號的相關引用部分的程式碼(修改地址對應符號的執行地址),所以連結器也稱為 link editor
2.4.3 連結靜態庫
靜態庫為目標檔案的打包,連結靜態庫和連結目標檔案一樣,不過合併的段只有真正需要的部分。
構建靜態庫,並使用
檢視靜態庫
檢視可執行檔案,發現的確只有真正使用的符號被連結了
2.4.5 連結
連結動態庫,不會將動態庫程式碼合併,但是需要在被連結檔案中建立動態庫資訊,載入器才能確定其依賴的動態庫是什麼
動態庫資訊在可執行檔案的 dynamic段
動態連結時還要建立 重定位表,這樣載入時,載入器才能根據重定位表重定位外部引用符號,重定位表記錄在ELF檔案的重定位段中,可能包含多個重定位段。
.rel.dyn:需要重定位的變數
.rel.plt:需要重定位的函式
總結:
雖然連結時不會進行重定位,但是 需要記錄依賴的動態庫,和記錄需要重定位的條目。載入時,載入器根據這些資訊完成重定位。