最近花了一些時間研究微信的協程庫libco,libco是微信後臺大規模使用的c/c++協程庫。庫裏面提供了socket族函數的hook,使得後臺邏輯服務幾乎不用修改邏輯代碼就能夠完成異步化改造,號稱單機能夠達到千萬鏈接。html
有關libco庫的具體實現原理後續有時間再討論,本文先討論微信團隊實現對socket族函數的hook的技術細節。linux
首先,咱們先回顧一下程序連接相關的知識。c++
在linux系統中,使用如下命令將源代碼編譯成可執行文件,源代碼通過 預處理,編譯,彙編,連接的過程最終生成可執行文件。一個簡單的編譯命令以下:gcc -o hello hello.c main.c -lcolib
git
其中連接過程分爲靜態連接和動態連接。github
連接器能夠將多個目標文件打包成一個單獨的文件,稱爲庫文件(有靜態庫和動態庫)。
靜態連接是指在連接過程,將靜態庫文件中被引用的目標文件直接拷貝連接到可執行文件中。
使用靜態庫有許多的缺點:微信
.code
段都會包含相同的機器碼,形成內存的浪費而使用靜態庫的優勢爲 編譯簡單,且只連接使用到的目標文件。網絡
爲了解決靜態連接的缺點,就出現了動態連接的概念。動態庫這個你們都不會陌生,好比Windows
的dll
文件,Linux
的so
文件。動態庫加載後在系統中只會存有一份,全部依賴它的可執行文件都會共享動態庫的code
段,data
段私有。
動態連接的命令以下:gcc -o main main.o -L${libcolib.so path} -lcolib
oracle
系統爲咱們提供了 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已經完成。
這樣就結束了嗎?咱們看看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
的方法,將網絡相關的read
,write
...等方法進行hook
後,將其改形成異步操做,即相關調用阻塞後讓出cpu,讓其餘協程繼續處理,從而達到異步化的效果。libco
的具體實現後續再介紹。