微信协程库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获取新符号

相关文章
相关标签/搜索