微信協程庫libco研究:hook系統函數

最近花了一些時間研究微信的協程庫libco,libco是微信後臺大規模使用的c/c++協程庫。庫裏面提供了socket族函數的hook,使得後臺邏輯服務幾乎不用修改邏輯代碼就能夠完成異步化改造,號稱單機能夠達到千萬鏈接。html

有關libco庫的具體實現原理後續有時間再討論,本文先討論微信團隊實現對socket族函數的hook的技術細節。linux

首先,咱們先回顧一下程序連接相關的知識。c++

靜態連接

在linux系統中,使用如下命令將源代碼編譯成可執行文件,源代碼通過 預處理,編譯,彙編,連接的過程最終生成可執行文件。一個簡單的編譯命令以下:
gcc -o hello hello.c main.c -lcolib
clipboard.pnggit

其中連接過程分爲靜態連接和動態連接。github

連接器能夠將多個目標文件打包成一個單獨的文件,稱爲庫文件(有靜態庫和動態庫)。
靜態連接是指在連接過程,將靜態庫文件中被引用的目標文件直接拷貝連接到可執行文件中。
使用靜態庫有許多的缺點:微信

  1. 可執行文件大小過大,形成硬盤的浪費
  2. 若是庫文件有更新,則依賴該庫文件的可執行文件必須從新編譯後,才能應用該更新
  3. 假設有多個可執行文件都依賴於該庫文件,那麼每一個可執行文件的.code段都會包含相同的機器碼,形成內存的浪費

而使用靜態庫的優勢爲 編譯簡單,且只連接使用到的目標文件。網絡

動態連接

爲了解決靜態連接的缺點,就出現了動態連接的概念。動態庫這個你們都不會陌生,好比Windowsdll文件,Linuxso文件。動態庫加載後在系統中只會存有一份,全部依賴它的可執行文件都會共享動態庫的code段,data段私有。
動態連接的命令以下:
gcc -o main main.o -L${libcolib.so path} -lcolib
clipboard.pngoracle

運行時動態連接

系統爲咱們提供了 dlopen,dlsym工具,用於運行時加載動態庫。可執行文件在運行時能夠加載不一樣的動態庫,這就爲hook系統函數提供了基礎。
下面用一個小小的例子來講明如何利用dlsym工具hook系統函數。異步

假設如今咱們須要統計程序中malloc的調用次數,可是不能修改原有程序。最簡單的思路相似於Java中動態代理Proxy的作法,先找到系統的malloc函數,而後將其替換爲自定義的函數,在自定義函數中增長調用次數,並回調系統的原有malloc函數。socket

例如咱們要統計如下main.c中調用malloc的次數:

// main.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int index;
    for (index=0; index < 10; index++) {
        char* p = (char*)malloc(4);
        printf("index:%d, p[0]=%d\n", index, *p);
        free(p);
    }
    printf("hello world\n");
    return 0;
}

爲了能讓本身的malloc函數回調系統的malloc函數,咱們須要利用dlsym獲取系統的malloc函數。

// myhook.c
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>

int count = 0;

void *malloc(size_t size) {
    void *(*myMalloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
    count++;
    // 這裏使用第一個字節爲count數來表示程序進入了這個malloc函數
    char* data = (char*)myMalloc(size);
    *data = count;
    return (void*)data;
}

RTLD_NEXT容許從調用方連接映射列表中的下一個關聯目標文件獲取符號,即找到glibc.so中的malloc函數。

下一步則是要讓可執行文件main找到自定義的malloc函數。

在linux操做系統的動態連接庫的世界中,LD_PRELOAD就是這樣一個環境變量,它能夠影響程序的運行時的連接(Runtime linker),它容許你定義在程序運行前優先加載的動態連接庫。loader在進行動態連接的時候,會將有相同符號名的符號覆蓋成LD_PRELOAD指定的so文件中的符號。換句話說,能夠用咱們本身的so庫中的函數替換原來庫裏有的函數,從而達到hook的目的。

編譯:

$ gcc -o main main.c
$ gcc -o libmymalloc.so -fPIC -shared -D_GNU_SOURCE myhook.c -ldl

運行:

$ LD_PRELOAD=./libmymalloc.so ./main

index:0, p[0]=1
index:1, p[0]=3
index:2, p[0]=4
index:3, p[0]=5
index:4, p[0]=6
index:5, p[0]=7
index:6, p[0]=8
index:7, p[0]=9
index:8, p[0]=10
index:9, p[0]=11
hello world

至此,malloc函數的hook已經完成。

不使用LD_PRELOAD的Hook

這樣就結束了嗎?咱們看看libco庫是如何實現hook的呢,它的makefile中並無LD_PRELOAD相關的信息。其祕密在於co_hook_sys_call.cpp,其將 co_enable_hook_sys()的定義在該cpp文件內,這樣就把該文件的全部函數都導出了(即導出符號表)。

//co_hook_sys_call.cpp
ssize_t read(int fd, void* buf, size_t bytes) 
{
...
}

...

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

咱們仍然以上面malloc的例子來講明:

// main.c
#include <stdio.h>
#include <stdlib.h>
#include "myhook.h"

int main() {
    int index = 0;
    for (index=0; index < 10; index++) {
         printf("index:%d, hook res:%d\n", index, enable_hook());
         void *p = malloc(4);
         free(p);
    }
    printf("hello world\n");
    return 0;
}
// myhook.h
int enable_hook();
// myhook.c
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>
#include "myhook.h"

static int count = 0;

void *malloc(size_t size) {
    void *(*myMalloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
    count++;
    return myMalloc(size);
}

int enable_hook() {
    return count;
}

編譯和運行:

$ gcc -o libmymalloc.so -fPIC -shared -D_GNU_SOURCE myhook.c -ldl
$ gcc main.c  -L . -Wl,-rpath . -lmymalloc -o main
$ ./main
index:0, hook res:0
index:1, hook res:2
index:2, hook res:3
index:3, hook res:4
index:4, hook res:5
index:5, hook res:6
index:6, hook res:7
index:7, hook res:8
index:8, hook res:9
index:9, hook res:10
hello world

這種方式算是對源代碼進行了侵入,必須調用特定的函數(即本例中的enable_hook()),才能將hook的函數導出,並連接到現有的可執行文件的內存空間中。

總結

libco庫經過非LD_PRELOAD的方法,將網絡相關的readwrite...等方法進行hook後,將其改形成異步操做,即相關調用阻塞後讓出cpu,讓其餘協程繼續處理,從而達到異步化的效果。libco的具體實現後續再介紹。

參考

高級語言的編譯:連接及裝載過程介紹
hook姿式總結
dlsym獲取新符號

相關文章
相關標籤/搜索