--- 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)