---
# System prepended metadata

title: 動態連結器
tags: [C 語言筆記]

---

---
title: 動態連結器
tags: C 語言筆記
---
## 動態連結器
### LD_PRELOAD 替換動態連結的函式庫
* random_num.c: 印出隨機的數字(0~99)10次
```c=
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main(){
  srand(time(NULL));
  int i = 10;
  while(i--) printf("%d\n",rand()%100);
  return 0;
}
```
* unrandom.c: 自己編寫的錯誤的 rand 程式，並用 ```gcc -shared -fPIC unrandom.c -o unrandom.so``` 生成 shared object 檔案，其中 -shared 代表生成 so 檔，而 -fPIC 代表產生出的 code 是  position-independent code(PIC)
```c=
int rand(){
    return 42;
}
```
* ```$gcc -o random_num.c random_num``` 後可以先用 ```$ldd random_num``` 來看看這個程式的動態函示庫
```c=
linux-vdso.so.1 (0x00007ffeee9f1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc3116b4000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc3118be000)
```
其中第二行代表引用的C函式庫的連結，可以用```$nm -D /lib/x86_64-linux-gnu/libc.so.6``` 來看有哪些函式。
random_num.c 裡面的 rand() 就是從這邊來的
* 接下來就可以用 ```$LD_PRELOAD=./unrandom.so ./random_num``` 來執行程式
* 用 ```$LD_PRELOAD=./unrandom.so ldd random_num ``` 來觀察 LD_PRELOAD 做了甚麼，可以發現第二行多了我們之前打的 unrandom 的 so
```c=
linux-vdso.so.1 (0x00007ffc62bdd000)
./unrandom.so (0x00007f85a58ef000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f85a56ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f85a58fb000)
```
* 而在動態連結中是先看到先採用，所以放在第二行的 rand() 會比放在第三行的 rand() 先被採用
* 利用 dlsym() 在不影響原始程式碼的情況下修改
* 新的 unrandom.c，其中 dlsym(RTLD_NEXT, "rand") 表示在下一個 object 找名字為 rand 的函式，並回傳它的地址
```c=
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stddef.h>

int rand(){
   static int (*real_rand)() = NULL;
   if (real_rand == NULL){
      real_rand = dlsym(RTLD_NEXT, "rand");  
   }
   printf("1");
   return real_rand();
}
```
之後用 ```$gcc -shared -fPIC unrandom.c -o unrandom.so -ldl``` 編譯(```-ldl``` 表示允許用 dlfcn 函式庫)
最後一樣用 ```$LD_PRELOAD=./unrandom.so ./random_num```來執行
* 其他範例: malloc_count.c
```c=
#define _GNU_SOURCE
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <dlfcn.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);
}
```
執行: ```$ LD_PRELOAD=./libmcount.so ls```

### ELF interpreter
* 可以用 ```$readelf -a hello | less ``` 查看 hello 執行檔的格式，可以看到第一行就是代表 ELF interpreter 被設成 /lib64/ld-linux-x86-64.so.2(有 loader 和 dynamic linker 的作用)
```c=
    [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000005f8 0x00000000000005f8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001f5 0x00000000000001f5  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x0000000000000158 0x0000000000000158  R      0x1000
  LOAD           0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000258 0x0000000000000260  RW     0x1000
  DYNAMIC        0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
                 0x00000000000001f0 0x00000000000001f0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x0000000000000358 0x0000000000000358 0x0000000000000358
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_EH_FRAME   0x000000000000200c 0x000000000000200c 0x000000000000200c
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
                 0x0000000000000248 0x0000000000000248  R      0x1
```
### Compilation Units
* 每個 .c 檔在編譯時在 linking 之前都是獨立的單位(Compilation Unit)雖然優點是可以同時平行編譯多個檔案，也因此造成編譯器很難最佳化因為不知道其他檔案有甚麼東西。所以要把 local function 要宣告成 static，也可以透過 Link Time Optimization(LTO，連結時期做最佳化) 解決
* 複習 static & extern
    * 記憶體方面: static c 代表著就算函式執行完畢，變數也不會消失
    * 範例一: 由於範圍限於函式之中，函式外仍無法存取該變數
      ```c=
      void count() { 
          static int c = 1; 
          printf("%d\n", c); 
          c++; 
      }
      ```
    *  連結程式方面: 定義在另一個 .c 檔案範圍中的變數或函式，可以透過 extern 在其他地方存取
    * 範例二
      ```c=
      int main() { 
          extern double v;
          printf("%f\n", v);

          return 0; 
      }
      ```
### Symbol Visibility
* 所有「不是 static」的 symbol 都可會開放給其他 compilation unit 去存取，這樣的行為我們稱為 "export"，一個 symbol 一旦 export，就可能遇到前述的 interpositioning(上面的 LD_PRELOAD 操作)
* 解法: symbol visibility，用 -fvisibility 編譯命令，以便對每個 object file 來進行全域設定
* [visibility ](https://gcc.gnu.org/onlinedocs/gcc-4.0.0/gcc/Function-Attributes.html)
    * ```int i __attribute__ ((visibility ("default")))```: 不修改 visibility 
    * ```int i __attribute__ ((visibility ("hidden")))```: i 將不被匯出到 dynamic symbol table，不能被其它 object 進行使用，代表 local
    * 範例: 定義 CHEWING_API 巨集代表設定 visibility 為 default，而 CHEWING_PRIVATE 代表 hidden
    ```c=
    if (__GNUC__ > 4) && (defined(__ELF__) || defined(__PIC__))
    #    define CHEWING_API __attribute__((__visibility__("default")))
    #    define CHEWING_PRIVATE __attribute__((__visibility__("hidden")))
    #else
    #    define CHEWING_API
    #    define CHEWING_PRIVATE
    #endif
    ```
* 測試程式碼: 用 ``` $readelf -a test | less ``` 觀察 symbol table 的變數的 BIND 結果是否跟設定的一樣
    ```c=
    static int local(void) { }
    int global(void) { }
    int  __attribute__((weak)) weak(void) { }
    ```
* 把前面的 unrandom.c 改成如下，可以使 LD_PRELOAD 沒有效果
    ```c=
    static 或 __attribute__((visibility("hidden"))) rand() {...}
    ```
### 動態連結支援
* 執行時在定位，不需要一開始就全部定位完(費時)
* [LINKING 心得](https://www.bottomupcs.com/chapter08.xhtml)
    * Code Sharing
        * dynamic linking: 當某個程式在其他程式執行時被呼叫，系統會去檢查它有沒有已經被其他程式載入到記憶體，有的話就可以直接 mapping pages 到執行檔裡面，反之則載入到記憶體
        * ELF header 有兩個互斥的 flags，ET_EXEC 和 ET_DYN 表示這個執行檔是否為 executable 或 shared object file
    * The Dynamic Linker
        * dynamic linker 是一個管理在 executable 上的  shared dynamic libraries 的程式，他負責載入 libraries 到記憶體和修改程式使其可在執行時期呼叫 functions 
        * ELF interpreter: 在 linux 中負責動態連結的檔案，可以用 ```$ ldd /bin/ls``` 來發現就是 ```ld-linux.so.x```(x 可依版本分為不同數字)，它會告訴程式需要的函式的位置
        * Relocations:  dynamically linked executables 裡面使用到的 shared library 的 function 都是用相對位址，必須在執行時期 shared library 被載入記憶體後才會變成絕對位址
        * 觀察 Relocation: 
            ```
            $ cat addendtest.c
            extern int i[4];
            int *j = i + 2;
            int *k = i + 3;
            ```
            ```
            $ cat addendtest2.c
            int i[4];
            ```
            * 可以看到 j 的 總offset 為 0x2000+8，k 則是 0x2008+c
            ```
            $ gcc -nostdlib -shared -fpic -s -o addendtest2.so addendtest2.c
            $ gcc -nostdlib -shared -fpic -o addendtest.so addendtest.c addendtest2.so
            $ readelf -r ./addendtest.so
            Relocation section '.rela.dyn' at offset 0x2e8 contains 2 entries:
            Offset          Info           Type           Sym. Value    Sym. Name + Addend
            000000002000  000100000001 R_X86_64_64       0000000000000000 i + 8
            000000002008  000100000001 R_X86_64_64       0000000000000000 i + c
            ```
        * Position Independence: 幾乎所有的 libraries 都是 position independent code，可在 memroy 中任意位置正確地運行，而不受其絕對地址影響的一種機器碼
    * GOT&PLT
        * 如果有一個程式或多個程式執行時需要多次使用 printf() ，比較有效率的方式是讓系統的主記憶體只存著一份 printf 函數，其他所有程式要輸出字串時再跳到唯一存放printf的地方執行，結束再跳回原來的程式就行了，GOT&PLT 主要就是在做這件事
        ![](https://i.imgur.com/n1aDsSw.png)
        * GOT（Global Offset Table）: GOT 記錄在 ELF 文件中所用到的共享庫中符號的絕對地址。剛開始第一次存取時 GOT 是空的，會呼叫動態解析(_dl_runtime_resolve)來取得絕對位置並記錄，之後第二次便可以直接查表
        GOT 格式範例:
        ![](https://i.imgur.com/r9Xx096.png)
        * PLT（Procedure Linkage Table）: PLT 的作用是將程式中位置無關的 Symbol 轉移成絕對地址，存放的是對應的 GOT 位址。當一個 Symol 被呼叫時，PLT 會去 GOT 中找對應的絕對地址。其中第一個元素 plt[0] 是公共plt，負責呼叫動態鏈接器
        * 流程: 
        ![](https://i.imgur.com/Hl00IjJ.jpg)
            * 左圖的第一步，程式遇到 printf 這個 symbol，跳到 對應的PLT 找值
            * 再從 PLT 跳到對應的 GOT 求值，但是也沒有所以跳回來原本的 PLT 的下個指令，把該函數的 ID(0x1) 推到 stack 上，再跳到 PLT[0] 呼叫動態連結
            * 左圖第四步，主要是在跳到 dynamic linker，dynamic linker 從 stack 觀察主程式想使用哪個函數，更改對應的 GOT 的內容，使其指向函數的真正位置
            * 右圖就是程式再次呼叫 printf 時，直接查表回傳絕對位址
            * 有需要時再呼叫這個動態鏈接的方式叫做 Lazy Linking
        * .got 和 .got.plt 是 ELF檔案 把 GOT 拆成變數用的部分(.got)以及函數用的部分(.got.plt)
[參考](https://hackmd.io/@rhythm/ry5pxN6NI)