動態連結出現的原因就是為了解決靜態連結中提到的兩個問題:
// main.c
void func1();
int main() {
func1();
return 0;
}
#include <stdio.h>
void func() {
printf("func \n");
}
void func1() {
printf("func 1\n");
}
生成地址無關可執行文件 (position-independent executable)
$gcc -shared -fPIC -o func.so func.o-fPIC 作用於編譯階段,告訴編譯器產生與位置無關代碼(Position-Independent Code)
-shared 告訴連結器創建一個共享目標文件
生成可重定位目標文件
$gcc -c main.c
與動態連結庫(.so)產生可執行文件
$gcc -o main main.o ./func.o
如果一個程式是動態連結,那麼他的function位置會在執行時才會固定,而引入一個library有好幾個函式,我們不見得每個都會用到,所以當真正調用時,才會去載入它。這就是Lazy-binding的機制。
怎麼看一個程式有沒有使用Lazy-binding呢?我們常常在做objdump -d elf 時,會看到call puts@plt這樣的調用方式,這便是Lazy-binding調用函式的方式了
無論我們記憶體載入任何一個目標模組,資料段 和 程式碼段的距離都是保持不變的,因此,程式碼段 中 任何指令與 資料段 任何變數的距離都是一個常數,與程式碼段 和 資料段 記憶體位址是無關的。
然而現代作業系統不允許修改程式碼段,只能修改資料段,那麼 GOT(Global Offset Table)和 PLT(Procedure Linkage Table) 就為此而生
GOT(Global Offset Table):全局偏移表用於記錄在 ELF 文件中所用到的共享庫中符號的絕對地址。在程序剛開始運行時,GOT 表項是空的,當符號第一次被調用時會動態解析符號的絕對地址然後轉去執行,並將被解析符號的絕對地址記錄在 GOT 中,第二次調用同一符號時,由於 GOT 中已經記錄了其絕對地址,直接轉去執行即可(不用重新解析)。
PLT(Procedure Linkage Table):過程鏈接表的作用是將位置無關的符號轉移到絕對地址。當一個外部符號被調用時,PLT 去引用 GOT 中的其符號對應的絕對地址,然後轉入並執行。
那麼在下次呼叫時就避免再重定位,直接跳到 printf 地址了
透過GDB可以觀看詳細流程
這邊可以參考
深入理解計算機系統
組譯器與連結器 (下)
動態連結的PLT與GOT