Libco Hook 機制淺析
之前的文章裡我們提到過 Libco 有一套 Hook 機制,可以通過協程的讓出(yield)原語將系統的阻塞系統呼叫改造為非阻塞的,這篇文章我們將深入解析 Hook 機制到底是怎麼運作的
Hook 機制的核心有兩點
- 提供自己的實現覆蓋標準庫(
libc.so
)的實現 - 在自己實現的程式碼裡要有辦法能夠呼叫標準庫的實現
也就是說,我們提供的實現其實是標準庫實現的 wrapper
為了搞明白 Hook 機制,我們首先要了解 Linux 動態庫究竟是怎麼運作的
動態庫的載入和符號表
動態庫是在執行時連結的,這個工作是由動態連結器來完成的(Linux 下是 /lib/ld-linux.so.2
),主要涉及到的步驟有
- 搜尋可執行檔案依賴的所有動態庫,並將它們載入到程式的虛擬地址空間中
- 做符號解析和重定位
- 動態庫的符號會被加到全域性符號表裡
- 執行共享物件的初始化程式碼
如果可執行檔案依賴的多個動態庫定義了同一個符號時,以先載入的動態庫為準,那麼如果想要覆蓋掉動態庫 A 裡的符號,最簡單的做法就是讓我們的庫在動態庫 A 之前載入,通常使用環境變數 LD_PRELOAD
來實現這點
LD_PRELOAD
中列出的動態庫會在所有其他動態庫之前載入,包括libc.so
- 不管程式是否依賴於它們,這些庫都會被載入
- 如果我們想要提供自己的 malloc 實現,只需要在自己的動態庫裡實現 malloc,然後將它加入
LD_PRELOAD
中,這樣就會覆蓋掉標準庫的 malloc 實現
命令
LD_DEBUG=files ./a.out
可以檢視動態庫的載入順序和初始化順序,這兩個順序不一定相同
dlsym
解決了第一個問題,那麼剩下的問題就是如何在我們的實現裡呼叫標準庫實現了,直接用函式名呼叫肯定是不行的,那麼我們能想到的辦法就是能否給標準庫的實現改一個名字呢?
為了實現這點,我們需要用到 dlsym 函式, 它的函式原型為
void *dlsym(void *restrict handle, const char *restrict symbol);
dlsym 的原意是用來獲得動態載入進來的動態庫中的介面(Linux 中的動態庫不僅可以在程式啟動時載入,還可以在程式執行過程中載入和解除安裝),其中 handle 是動態庫的控制程式碼,symbol 是要搜尋的符號
此外,dlsym 還支援兩個偽 handle
RTLD_DEFAULT
- 按預設搜尋順序搜尋第一次出現的 symbol,搜尋範圍包含全域性符號表裡的所有符號
- 程式可執行檔案本身的符號
- 動態連結器載入的動態庫中的符號
- 如果用 dlopen 載入動態庫時,指定了
RTLD_GLOBAL
選項,那麼它的符號也會出現在全域性符號表裡
- 按預設搜尋順序搜尋第一次出現的 symbol,搜尋範圍包含全域性符號表裡的所有符號
RTLD_NEXT
- 從當前可執行檔案或動態庫開始,搜尋下一次出現的 symbol
- 搜尋順序依賴於動態連結庫的載入順序
- 假設動態連結庫的載入順序是 A -> B -> C -> D,在動態庫 B 裡調這個介面搜尋 symbol1,就會依次去 C 和 D 中搜尋 symbol1,返回先找到的 symbol1 地址
- 從當前可執行檔案或動態庫開始,搜尋下一次出現的 symbol
利用 RTLD_NEXT
就可以實現我們想要的功能,假設說我們用 LD_PRELOAD
覆蓋了標準庫 malloc 實現,就可以通過 dlsym 拿到標準庫的 malloc 地址(前提是在 libc.so 之前沒有其他庫定義了 malloc),給它的函式指標起一個其他的名字,就可以在我們的實現裡呼叫標準庫 malloc 了
可以看到 Libco 裡就是用這種方法拿到所有標準庫實現的函式指標
typedef ssize_t (*read_pfn_t)(int fildes, void *buf, size_t nbyte);
static read_pfn_t g_sys_read_func = (read_pfn_t)dlsym(RTLD_NEXT, "read");