Post

协程梳理实践(四) -- sylar协程API hook封装

梳理sylar协程对标准库和系统API的hook封装

协程梳理实践(四) -- sylar协程API hook封装

1. 引言

梳理sylar协程对标准库和系统API的hook封装实现。

相关参考:

说明:本博客作为个人学习实践笔记,可供参考但非系统教程,可能存在错误或遗漏,欢迎指正。若需系统学习,建议参考原链接。

2. hook介绍和示例

hook实际上就是对系统接口的封装,提供和原始接口一样的函数签名,让使用者在调用时跟调用原始系统接口没有什么差别,但实际上是执行hook接口中的逻辑,可以实现自定义操作+原始接口操作。

sylar中的hook,就是为了在不重写代码的情况下,把原有代码中的同步socket操作api都转换为异步操作,以提升性能。

hook使用场景示例:单个线程中,3个协程分别在默认情况和hook系统api情况下的表现

  • 协程1:sleep(2)
  • 协程2:在socket fd1 上send 100k数据;
  • 协程3:在socket fd2 上recv数据直到成功

sylar-coroutine-hook

  • 1、默认情况下,单个线程中的3个协程需要串行。sleep期间其他协程无法resume运行,recv阻塞等待数据发送期间,其他协程也无法运行
  • 2、hook情况下,对sleepsendrecv接口进行hook,分别用上节中的定时器IOManager epoll,在定时器到期或有io事件时才执行相应回调函数,可以避免无意义的阻塞,让CPU时间片运行在有意义的操作上

3. hook实现方式

hook实现有多种方式:动态链接静态链接,还有内核模块的hook。本篇中基于参考链接,也仅说明动态链接的hook方式。

3.1. 动态库hook方式

作为理解hook实现的基础,先看下编译的链接顺序、符号冲突相关的几个小实验,可了解:关于链接与装载的几个测试代码,代码可见:compile。此处只放一下结论。

  • 1、(未定义符号提前解决)无论动态链接还是静态链接,链接时都是从左到右扫描库文件。扫描时如果发现所有未定义符号都解决了,则后面的库就不会再继续扫描
    • 对应上面链接中的 测试1 和 测试4。
    • 只是不用扫描这部分符号,不是说不加载库了,比如用户在链接libc库前链接指定库,库中实现了read/write,而libc中也有这些接口,但因为是基础的运行时库,还是会加载的。
  • 2、(符号冲突)从左到右扫描过程中,如果发现优先级相同的符号出现了2次,则:
    • 2.1、动态链接不会报错,而是以第一次加载的符号为准。因为动态链接器在加载动态库时,会维护一份所有对象共享全局符号表,符号加入全局符号表时若发现相同符号已存在,则会忽略后加入的符号(可称作 全局符号介入)。(测试5)
    • 2.2、静态链接会报重复定义错误,因为静态库中没有全局符号表的介入。(测试2)

另外,关于编译和链接,可参考CSAPP中的章节,可以参考这篇笔记:CSAPP 第七章笔记:链接过程

基于动态链接进行hook,也有2种方式:

  • 1、外挂式hook,也称非侵入式hook,不需要重新编译代码。通过优先加载自定义动态库来实现对后面动态库的hook。(对应上面结论中的2.1,实验demo可见参考链接)
  • 2、侵入式hook,需要改造代码或者重新编译一次,以指定动态库的加载顺序。
    • 改造代码方式:在代码里(比如main.c)直接实现相同签名的函数
    • 重新编译方式:编译时将自定义库放在libc之前链接,如gcc main.c -L. -lhook -Wl,-rpath=.(libc库默认链接顺序总是在最后),以实现全局符号介入

3.2. 获取默认接口的符号

动态链接时,因“全局符号介入”机制覆盖的默认接口还是需要的,除了自定义的操作部分,最终要实现的功能还是需要通过系统的默认接口来实现的。可通过dlsym接口获取默认接口的符号。

  • 配合dlopen,还可以实现插件化的动态库加载,程序库动态升级也可以基于该机制实现。

dlsymdl缩写对应:dynamic linker)接口声明如下,第一个参数是dlopen打开动态库返回的句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
dlsym(3)                      Library Functions Manual                   dlsym(3)

NAME
       dlsym, dlvsym - obtain address of a symbol in a shared object or executable
LIBRARY
       Dynamic linking library (libdl, -ldl)
SYNOPSIS
       #include <dlfcn.h>
       void *dlsym(void *restrict handle, const char *restrict symbol);
DESCRIPTION
       The  function  dlsym() takes a "handle" of a dynamic loaded shared object returned by
       dlopen(3) along with a null-terminated symbol name, and  returns  the  address  where
       that  symbol is loaded into memory. 

用法:使用dlsym找回被覆盖的符号时,第一个参数固定为 RTLD_NEXT,第二个参数为符号的名称。比如下述的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef void* (*malloc_func_t)(size_t size);

// 函数指针用于保存libc中的malloc的地址
malloc_func_t sys_malloc_func = NULL;

// hook malloc,其中增加自定义的操作(此处仅打印)
// 这里重定义会导致libc中的同名符号被覆盖
void *malloc(size_t size) {
    // 先调用标准库里的malloc申请内存
    void *ptr = sys_malloc_func(size);
    fprintf(stderr, "malloc: ptr=%p, length=%ld\n", ptr, size);
    return ptr;
}

int main()
{
    // 通过dlsym找到标准库中的malloc的符号地址
    // 赋值给全局的函数指针,上述自定义malloc中会用到该函数指针
    sys_malloc_func = dlsym(RTLD_NEXT, "malloc");

    // 由于上述定义了和libc中malloc相同签名的函数,会使用上述自定义函数
    char *ptrs = malloc(100);
}

4. sylar中的hook模块

sylar中,只对socket的fd进行了hook,若不是socket则调用系统默认接口(其中进行了判断)。

  • 使用FdManager单例类管理所有分配过的fd上下文(用FdCtx类结构表示)。
  • 并且其中的hook是针对线程为粒度,可对单个线程设置是否启用hook。

sylar中的hook模块中有三类接口需要hook:

  • sleep延时系列接口
  • socket io系列接口
  • socket/fcntl/ioctl等,处理fd上下文,设置超时、非阻塞选项等

实现上,用前面所述的dlsym获取被hook接口的原始地址,sylar中定义了一个宏HOOK_FUN来简化操作。

  • extern "C"空间中,对所有被hook函数定义了全局的函数指针变量,比如sleep函数:sleep_fun sleep_f = nullptr;
  • sylar命名空间中,通过dlsym获取原始函数的符号地址给这些全局的函数指针赋值,比如:sleep_f = (sleep_fun)dlsym(RTLD_NEXT, "sleep");
  • 头文件中,则定义了各个函数签名,比如typedef unsigned int (*sleep_fun) (unsigned int seconds);,并声明了一下和系统函数相同的函数,基于全局符号介入机制来覆盖系统默认函数,如unsigned int sleep(unsigned int seconds);

获取各默认接口的符号地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// coroutine-lib/fiber_lib/6hook/hook.cpp
#define HOOK_FUN(XX) \
    XX(sleep) \
    XX(usleep) \
    XX(nanosleep) \
    XX(socket) \
    XX(connect) \
    XX(accept) \
    XX(read) \
    XX(readv) \
    XX(recv) \
    XX(recvfrom) \
    XX(recvmsg) \
    XX(write) \
    XX(writev) \
    XX(send) \
    XX(sendto) \
    XX(sendmsg) \
    XX(close) \
    XX(fcntl) \
    XX(ioctl) \
    XX(getsockopt) \
    XX(setsockopt) 

namespace sylar{

void hook_init()
{
    static bool is_inited = false;
    if(is_inited){
        return;
    }
    // test
    is_inited = true;

// 定义临时的`XX(name)`宏,`HOOK_FUN(XX)`则是对所有的系统接口都执行 `XX(接口名)`
// 以sleep接口展开为例,查看过程:
    // `XX(sleep)`展开即:sleep_f = (sleep_fun)dlsym(RTLD_NEXT, "sleep");
    // 定义了一个变量 `sleep_f`
#define XX(name) name ## _f = (name ## _fun)dlsym(RTLD_NEXT, #name);
    HOOK_FUN(XX)
#undef XX
}

struct HookIniter
{
    HookIniter()
    {
        hook_init();
    }
};

static HookIniter s_hook_initer;

} // end namespace sylar

// 不在sylar命名空间内,且以C方式定义函数符号
extern "C"{
// 定义临时的`XX(name)`宏,注意和上面不一样
// 以sleep接口为例,查看过程:
    // `XX(sleep)`展开即:sleep_fun sleep_f = nullptr;
    // 定义了一个全局变量`sleep_f`,其类型是 `sleep_fun`
#define XX(name) name ## _fun name ## _f = nullptr;
    HOOK_FUN(XX)
#undef XX
}

而各个xxx_fun的函数类型,则定义在hook.h中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// coroutine-lib/fiber_lib/6hook/hook.h
extern "C"
{
    // 1、对每个被hook的系统函数,定义跟其一样的签名
    typedef unsigned int (*sleep_fun) (unsigned int seconds);
    extern sleep_fun sleep_f;

    typedef int (*usleep_fun) (useconds_t usec);
    extern usleep_fun usleep_f;
    ...

    // 2、并定义各系统函数的hook函数
    unsigned int sleep(unsigned int seconds);
    int usleep(useconds_t usce);
    ...
}

socket 函数来看下hook的实现:

  • socket_f是系统默认的函数地址
  • (对于sleep,用的是定时器,并没有用系统默认的函数地址)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// coroutine-lib/fiber_lib/6hook/hook.cpp
int socket(int domain, int type, int protocol)
{
    if(!sylar::t_hook_enable)
    {
        return socket_f(domain, type, protocol);
    }

    int fd = socket_f(domain, type, protocol);
    if(fd==-1)
    {
        std::cerr << "socket() failed:" << strerror(errno) << std::endl;
        return fd;
    }
    sylar::FdMgr::GetInstance()->get(fd, true);
    return fd;
}

5. 小结

介绍hook概念和实现方式,并简单梳理sylar中的hook模块实现。

6. 参考

This post is licensed under CC BY 4.0 by the author.