---
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 主要就是在做這件事

* GOT(Global Offset Table): GOT 記錄在 ELF 文件中所用到的共享庫中符號的絕對地址。剛開始第一次存取時 GOT 是空的,會呼叫動態解析(_dl_runtime_resolve)來取得絕對位置並記錄,之後第二次便可以直接查表
GOT 格式範例:

* PLT(Procedure Linkage Table): PLT 的作用是將程式中位置無關的 Symbol 轉移成絕對地址,存放的是對應的 GOT 位址。當一個 Symol 被呼叫時,PLT 會去 GOT 中找對應的絕對地址。其中第一個元素 plt[0] 是公共plt,負責呼叫動態鏈接器
* 流程:

* 左圖的第一步,程式遇到 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)