iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Arm Platforms

30天從0開始探索嵌入式世界系列 第 7

Day.7 深入理解動態連結

為甚麼會出現動態連結?

動態連結出現的原因就是為了解決靜態連結中提到的兩個問題:

  • 浪費空間,因為每個可執行程序中對所有需要的Obj file都要有一份副本,所以如果多個程序對同一個目標文件都有依賴,如多個程序中都調用了printf()函數,則這多個程序中都含有printf.o,所以同一個Obj file 都在記憶體中存在多個副本。
  • 每當函數庫的程式碼修改了,這個時候就需要重新進行編譯連結形成可執行程序。

如何使用動態連結 ?

// main.c
void func1();
int main() {
    func1();
    return 0;
}

#include <stdio.h>


void func() {
    printf("func \n");
}

void func1() {
    printf("func 1\n");
}
  1. 生成地址無關可執行文件 (position-independent executable)
    $gcc -shared -fPIC -o func.so func.o
    -fPIC 作用於編譯階段,告訴編譯器產生與位置無關代碼(Position-Independent Code)

    -shared 告訴連結器創建一個共享目標文件

  2. 生成可重定位目標文件
    $gcc -c main.c

  3. 與動態連結庫(.so)產生可執行文件
    $gcc -o main main.o ./func.o

Lazy-binding

如果一個程式是動態連結,那麼他的function位置會在執行時才會固定,而引入一個library有好幾個函式,我們不見得每個都會用到,所以當真正調用時,才會去載入它。這就是Lazy-binding的機制。

怎麼看一個程式有沒有使用Lazy-binding呢?我們常常在做objdump -d elf 時,會看到call puts@plt這樣的調用方式,這便是Lazy-binding調用函式的方式了

地址無關可執行文件(PIC)

無論我們記憶體載入任何一個目標模組,資料段 和 程式碼段的距離都是保持不變的,因此,程式碼段 中 任何指令與 資料段 任何變數的距離都是一個常數,與程式碼段 和 資料段 記憶體位址是無關的。

GOT/PLT

然而現代作業系統不允許修改程式碼段,只能修改資料段,那麼 GOT(Global Offset Table)和 PLT(Procedure Linkage Table) 就為此而生

  • GOT(Global Offset Table):全局偏移表用於記錄在 ELF 文件中所用到的共享庫中符號的絕對地址。在程序剛開始運行時,GOT 表項是空的,當符號第一次被調用時會動態解析符號的絕對地址然後轉去執行,並將被解析符號的絕對地址記錄在 GOT 中,第二次調用同一符號時,由於 GOT 中已經記錄了其絕對地址,直接轉去執行即可(不用重新解析)。

  • PLT(Procedure Linkage Table):過程鏈接表的作用是將位置無關的符號轉移到絕對地址。當一個外部符號被調用時,PLT 去引用 GOT 中的其符號對應的絕對地址,然後轉入並執行。

流程

  • 當程式碼中 Call Printf(),在對應的組語會看見 看到call printf@plt。
  • printf@GOT會回去向.got.plt取值
    • 由於是第一次使用函數,在GOT 表中並不會找到該函數地址,因此必須透過 PLT 將 GOT 重定位
  • 將找到的函數位置所需的參數 推入 stack中
  • 透過執行dl_runtime_resolve 找出函式位址
  • 這時系統就會把 func 的位址寫進 .got.plt 當中。

那麼在下次呼叫時就避免再重定位,直接跳到 printf 地址了

透過GDB可以觀看詳細流程
這邊可以參考

參考資料

深入理解計算機系統
組譯器與連結器 (下)
動態連結的PLT與GOT


上一篇
Day.6 深入理解連結之重定址
下一篇
Day.8 Cache 的基本原理
系列文
30天從0開始探索嵌入式世界15

尚未有邦友留言

立即登入留言