---
# System prepended metadata

title: '你所不知道的 C 語言: 動態連結器篇'
tags: [你所不知道的 C 語言]

---

# 動態連結器

## 簡易運作流程觀察

以下為一個在認識動態連結器的功用前，讓我們觀察動態連結器其中一個功能的例子:

```C=
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>

void *malloc(size_t size) {
    char buf[32];
    static void *(*real_malloc)(size_t) = NULL;
    if (real_malloc == NULL) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    sprintf(buf, "malloc called, size = %zu\n", size);
    write(2, buf, strlen(buf));
    return real_malloc(size);
}
```

透過以下指令編譯程式

```
gcc -D_GNU_SOURCE -shared -ldl -fPIC -o /tmp/libmcount.so malloc_count.c
```

並搭配環境變數執行 `ls` 指令後

```
LD_PRELOAD=/tmp/libmcount.so ls
```

可以發現 `ls` 會呼叫我們定義的 `malloc()`

* 透過設定 `LD_PRELOAD` 環境變數，glibc 的 dynamic linker (`ld.so`) 會在載入和重定位 (relocation) `libc.so` 之前，載入我們撰寫的 `/tmp/libmcount.so` 動態連結函式庫，如此一來，我們實作的 malloc 就會在 `libc.so` 提供的 malloc 函式之前被載入。

以上實驗可以讓我們對於程式被連結的流程有以下概念:

* 程式呼叫的函式有可能不是在經過編譯與連結後就確定的，也有可能會在執行時才被連結。

## 相關背景知識

### Compilation Units

為了易於開發與管理專案，會將許多程式分開撰寫 (將不同功能的程式放置於不同 .h 與 .c 檔)。這些不同的 .c 與 .h 檔便是一個 compilation unit。

* 這樣做對開發與管理上來說是好事，但容易使編譯器無法偵測到一些潛在能做最佳化的程式。
    * 當將許多從不同 compilation units 產生的 .o 檔在透過 linker 連結產生執行檔前，是無法確認其他 compilation unit 的資訊的 (像是不知道用 extern 宣告的變數實際上是什麼)

## 動機

當我們要做 relocation 的 symbols 很多的時候，在開始執行 main 函式之前會需要耗費大量時間去完成所有 symbols 的 relocation。因此為了減少程式開始執行所需的時間，會透過 lazy binding 的方式使程式在需要的時候才對 symbol 做 relocation。

## 運作原理

透過 procedure linkage table (PLT) 與 global offset table (GOT) 達到 lazy binding。

### Procedure Linkage Table (PLT) & Global Offset Table (GOT)

以下為 PLT 與 GOT 在動態連結中解析需動態連結的 symbols 的位址的過程:

1. 編譯時程式碼中各個不確定的 symbols 的位址會填入 0。
2. 多個 compilation units 做 linking 時，部分位址才會被解析
    * 靜態連結: 其他 compilation units 中能看見的 symbols 的位址會填到指令 (在 .code section 裡) 中。
    * 動態連結: 要動態載入的 symbols 會新增一個 PLT 表格欄位 (同樣存放於 read only 的 .code section 中)，欄位中存放著 GOT 的位址 (GOT 存放在可修改的 .data section 中)。
        * GOT 的初始值為幫忙查找 symbol 實際位址的動態連結器的位址。
        * 動態連結器找到的位址會更新到 PLT 指向的 GOT。
3. 程式執行時，當第一次用到需要動態連結的 symbol 時，會先透過該 symbol 對應的 PLT 去跳到存放初始值的 GOT，也就是先執行解析 symbol 實際位址的動態連結器查詢 symbol 對應的程式碼的存放位址，將查詢結果更新到 GOT 後再去執行 symbol 實際上對應到的程式。

## 相關應用場景與工具

### Link Time Optimization (LTO)

為了能讓編譯器有辦法對多個 compilation units 做最佳化，因此產生了在連結階段做最佳化的概念與技術。

* 這是一個簡便，且能不更動程式碼，維持程式執行結果的優化方式。

#### LTO 的功能

LTO 能做到的事包含但不僅限以下事項: 

* 縮減程式大小，使程式在記憶體佔據的空間較少。
    * 應用場景: 當硬體資源 (記憶體大小) 很少
    * 實作方式: 折疊函式 (function folding)，將兩個很類似的函式濃縮 (兩份類似的程式碼變為一份)，把不同的部分獨立出來
* 透過展開函式 (function inlining) 最佳化執行效率

### Symbol Visibility

有時候會想控制 symbol 的可視範圍，像是只在沒有其他 compilation units 定義特定 symbol 時才使用自己的定義，便能使用 gcc 的 visibility 屬性 (`__attribute__((visibility()))`) 與 `-fvisibility` 編譯參數來做到這件事情。