1. 程式人生 > >libco hook原理簡析

libco hook原理簡析

mysq 協程 調用 blog type 全部 同名 自己的 動態

我們先看一下libco協程庫的特性描述

libco的特性
    無需侵入業務邏輯,把多進程、多線程服務改造成協程服務,並發能力得到百倍提升;
    支持CGI框架,輕松構建web服務(New);
    支持gethostbyname、mysqlclient、ssl等常用第三庫(New);
    可選的共享棧模式,單機輕松接入千萬連接(New);

對於其第三點特性,支持gethostbyname、mysqlclient、ssl等常用第三庫。這說明什麽?說明它們的網絡IO函數,使用的是libco中的網絡IO函數,不然進入不了協程調度。那麽lobco是如何實現的呢?如果你善於運用搜索引擎,一定會找到一些文章這樣解釋:因為libco協程庫hook了系統的socket相關函數。

上面那句話,其實說了等於沒說... 這是果,而不是因,而這也是我打算寫這篇文章的原因,此文的相關版本最早發布在公司內部論壇,時間在17年初,由於x*&#%#(一堆敏感詞),此文為裁剪版。

答案之一在co_sys_hook_call.cpp文件中,在該文件中實現了hook 系統socket相關函數的第一步(註意是第一步,還有後續條件),在該文件中,可看到其定義的大量與系統socket相關函數同名,同參數的函數,如下文的socket函數所示

//co_sys_hook_call.cpp
...
int socket(int domain, int type, int protocol)
{
    HOOK_SYS_FUNC( socket );
    if( !co_is_enable_sys_hook() )
    {
        return g_sys_socket_func( domain,type,protocol );
    }
    int fd = g_sys_socket_func(domain,type,protocol);
    if( fd < 0 )
    {
        return fd;
    }
    rpchook_t *lp = alloc_by_fd( fd );
    lp->domain = domain;
    fcntl( fd, F_SETFL, g_sys_fcntl_func(fd, F_GETFL,0 ) );
    return fd;
}
...

對於每一個其定義的同步socket相關函數,該文件中也定義了一個對應的函數指針

static socket_pfn_t g_sys_socket_func   = (socket_pfn_t)dlsym(RTLD_NEXT,"socket");

細心的讀者想必註意到了上面的socket函數,其第一行的

HOOK_SYS_FUNC( socket );

我們看一下HOOK_SYS_FUNC是幹嘛的。

#define HOOK_SYS_FUNC(name) if( !g_sys_##name##_func ) { g_sys_##name##_func = (name##_pfn_t)dlsym(RTLD_NEXT,#name); }

結合上面的函數指針定義,我們可以知道該宏用於初始化對應的函數指針。對於dlsym,它是用於從加載進內存的動態庫中(通過傳入其句柄)尋找指定符號的函數或者變量,這裏的關鍵點是傳入的句柄參數是RTLD_NEXT,該參數表示從當前庫之後的load進來的動態庫中尋找該符號。比如對於socket函數,如果沒有其它庫也定義了該函數的話,在這裏找到的會是glibc中相關socket函數的地址。

我們可以看到libco在自己的socket函數中加入了一些額外的邏輯,然後最終調用的還是系統庫中的socket函數。這成功的實現了hook的第一步。

接下來分析hook的第二步,也就是如何保證第三方庫調用的是libco自身實現的相關socket函數呢?
簡單而言,就是通過調整最終生成的可執行文件的鏈接順序,使其全局符號表中的跟socket相關函數的符號為libco協程庫中的符號。
這裏需要簡述一下目標文件生成最終的可執行文件的鏈接過程。
我們先簡單看一下鏈接過程中會發生什麽。在鏈接過程中,鏈接器會按順序掃描輸入的目標文件,將其中的符號加入全局符號表,再計算出合並後的各個段的長度和位置。之後將進行符號解析和重定位。

另外對於動態鏈接來說,其還將遵循如下一條規則

全局符號介入
    linux下的動態鏈接器存在以下原則:當共享對象被load進來的時候,它的符號表會被合並到進程的全局符號表中(這裏說的全局符號表並不是指裏面的符號全部是全局符號,而是指這是一個匯總的符號表),當一個符號需要加入全局符號表時,如果相同的符號名已經存在,則後面加入的符號被忽略。

由於glibc是c/cpp程序的運行庫,因此它是最後以動態鏈接的形式鏈接進來的,我們可以保證其肯定是最後加入全局符號表的,由於全局符號介入機制,glibc中的相關socket函數符號被忽略了(但是libco中巧妙的運用RTLD_NEXT參數獲取到了其地址),也因此只要最終的可執行文件鏈接了libco協程庫,就可以基本保證相關的socket函數被hook掉了。

為什麽是基本保證?根據我的使用經驗,有socket相關函數定義的動態庫,不止glibc,pthread庫中也有(不確定其它庫是否也有)!不過也沒有關系,只需要保證libco庫位於pthread庫之前鏈接即可。如下所示

gcc main.c -o test -LSOME_PATH -llibco -lpthread

另外我們可以看到在co_hook_sys_call.cpp文件的末尾有一個很有意思的函數

void co_enable_hook_sys() //這函數必須在這裏,否則本文件會被忽略!!!
{
    stCoRoutine_t *co = GetCurrThreadCo();
    if( co )
    {
        co->cEnableSysHook = 1;
    }
}

這裏放這個函數其實是針對靜態鏈接的情況,因為如果該文件生成的目標文件中的符號如果最終全部都沒有被強引用到的話(比如在一個.h文件中對某個函數進行聲明,那麽是對這個函數符號的弱引用,而對這個函數進行調用,則是對這個函數符號的強引用),那麽該目標文件在靜態鏈接的過程中會被忽略掉。早期的計算機內存都非常小,因此能省一點是一點。

到這裏基本就分析完畢了。另外假如你使用的是libco的動態庫的話,可以通過readelf -d | grep ‘NEEDED‘ 目標文件命令或者ldd 目標文件,查看當前動態庫的鏈接順序,確保hook掉了socket相關函數。

libco hook原理簡析