最近花了一些时间研究微信的协程库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
的具体实现后续再介绍。