動態連結的應用
顯示執行時連結
支援動態連結的系統一般都支援一種更加靈活的模組載入方式,叫做顯示執行時連結
,有時也叫做執行時載入
。
這種執行時載入使得程式的模組組織變得很靈活,可以用來實現一些諸如外掛、驅動等功能。 當程式需要用到某個外掛或者驅動的時候,才將相應的模組裝載進來,而不需要從一開始就將他們全部裝載進來,從而減少了程式的啟動時間和記憶體使用。 並且程式可以在執行的時候重新載入某個模組,這樣使得程式本身不必重新啟動而實現模組的增加、刪除、更新等。
動態庫的裝載是通過一系列由動態連結器提供的API來完成的:開啟動態庫(dlopen)、查詢符號(dlsym)、錯誤處理(dlerror)以及關閉動態庫(dlclose),
程式可以通過這幾個API對動態庫進行操作。這幾個API的實現是在/lib/libdl.so.2裡面,他們的宣告和相關常量被定義在系統標準標頭檔案<dlfcn.h>
中。
dlopen()
dlopen()函式用來開啟一個動態庫,並將其載入到程序的地址空間,完成初始化過程,他的C原型定義為:
void *dlopen(const char *filename, int flag);
第一個引數是被載入動態庫的路徑,如果這個路徑是絕對路徑則該函式將會嘗試直接開啟該動態庫;如果我們將filename這個引數設定為0,
那麼dlopen返回的將是全域性符號表的控制代碼,也就是說我們可以在執行時找到全域性符號表裡面的任何一個符號,並且可以執行他們,這有些類似高階語言反射的特性。
全域性符號表包括了程式的可執行檔案本身、被動態連結器載入到程序中的所有共享模組以及在執行時通過dlopen開啟並且使用了RTLD_GLOBAL
方式的模組中的符號。
第二個引數flag表示函式符號的解析方式,常量RTLD_LAZY
表示使用延遲繫結,當函式第一次被用到時才進行繫結(繫結的意思是在動態庫載入時進行符號重定位),
即PLT機制;而RTLD_NOW
表示當模組被載入時即完成所有的函式繫結工作,如果有任何未定義的符號引用的繫結工作沒法完成,那麼dloopen()就會返回錯誤。
另外還有一個常量RTLD_GLOBAL
可以跟上面的兩者中任意一個一起使用(通過常量的“或”操作)。他表示將被載入的模組的全域性符號合併到程序的全域性符號表中,
使得以後載入的模組可以使用這些符號。
dlopen的返回值是被載入的模組的控制代碼,這個控制代碼在後面使用dlsym和dlclose時會用到。在完成裝載、對映和重定位以後,dlopen會執行.init
段的程式碼來初始化模組然後返回。
dlsym()
dlsym函式基本上是執行時裝載的核心部分,我們可以通過這個函式找到所需要的符號。他的定義如下:
void *dlsym(void *handle, char *symbol);
第一個引數是有dlopen()返回的動態庫的控制代碼;第二個引數即所要查詢的符號的名字,一個以\0
結尾的C字串。
如果dlsym()找到了相應的符號則返回該符號的值;沒有找到則返回NULL。dlsym()返回的值對於不同型別的符號,意義是不同的。
如果查詢的符號是函式則返回的是函式地址;如果是變數則返回的是變數的地址;如果這個符號是常量則返回的是該常量的值。
這裡有一個問題:如果常量的值剛好是NULL或者0呢?我們怎麼判斷dlsym()是否找到了改符號了?這就要用到dlerror()函數了,
如果符號找到了,那麼dlerror()返回NULL,如果沒有找到,dlerror()返回相應的錯誤資訊。
符號優先順序
當多個共享模組中有符號名衝突時,先裝入的符號優先,我們把這種優先順序方式稱為裝載序列。那麼當我們的程序中有模組是通過dlopen()裝入的共享物件是, 這些後裝入的模組中的符號可能會跟先前已經裝入的模組之間的符號重複。這種情況下動態聯結器在進行符號解析以及重定位時,都是採用裝載序列。
dlsym()對符號的查詢優先順序分兩種型別。第一種情況是,如果我們是在全域性符號表中進行符號查詢,即dlopen()的filename引數為NULL, 那麼由於全域性符號表使用的裝載序列,所以dlsym()使用的也是裝載序列。第二中情況是如果我們是對某個通過dlopen()開啟的共享物件進行符號查詢的話,那麼採用的是一種叫做依賴序列的優先順序。 什麼叫依賴序列呢?它是以被dlopen()開啟的那個共享物件為根節點,對它所有依賴的共享物件進行廣度優先遍歷,直到找到符號為止。
dlclose()
dlclose()的作用跟dlopen()剛好相反,用於解除安裝已經載入的模組。系統會維持一個載入引用計數器,沒次使用dlopen()載入某模組時,相應的計數器加一;
每次使用dlclose()解除安裝某模組時,相應的計數器減一。只有當計數器值減為0時,模組才被真正的解除安裝掉。解除安裝過程是先執行.finit
段的程式碼,然後將相應的符號從符號表中去除,
取消程序空間跟模組的對映關係,然後關閉模組檔案。
示例程式
這段程式將數學庫模組用執行時載入的方法載入到程序中,然後獲取sin()函式符號地址,呼叫sin()並且返回結果:
#include<stdio.h> #include<dlfcn.h> int main(int argc, char *argv[]) { void *handle; double (*func)(double); char *error; handle = dlopen(argv[1], RTLD_NOW); if(handle == NULL) { printf("Open library %s error: %s\n", argv[1], dlerror()); return -1; } func = dlsym(handle, "sin"); if((error = dlerror()) != NULL) { printf("Symbol sin not found: %s\n", error); dlclose(handle); return -1; } printf(" %f\n", func(3.1415926/2)); dlclose(handle); return 0; }
環境:Ubuntu
編譯:gcc test2.c -o test2 -ldl
執行:./test2 /lib/x86_64-linux-gnu/libm-2.23.so
輸出:1.000000
參考
這篇文章主要是看《程式設計師的自我修養-連結、裝載與庫》這本書的筆記,經常在一些大型專案中看到使用這種方式給程式寫外掛, 而且可以通過這種方式來hook系統中的庫函式來實現一些比較牛逼的特性,比較著名的是微信ofollow,noindex" target="_blank">libco網路庫 或者之前分析過SNG的SPP微執行緒框架;
至於hook的原理網上已經有很多介紹:libco hook原理簡析 。