Libco Hook 機制淺析

路過的摸魚俠發表於2022-04-29

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選項,那麼它的符號也會出現在全域性符號表裡
  • RTLD_NEXT
    • 從當前可執行檔案或動態庫開始,搜尋下一次出現的 symbol
      • 搜尋順序依賴於動態連結庫的載入順序
      • 假設動態連結庫的載入順序是 A -> B -> C -> D,在動態庫 B 裡調這個介面搜尋 symbol1,就會依次去 C 和 D 中搜尋 symbol1,返回先找到的 symbol1 地址

利用 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");

相關文章