• 【服务器学习】hook模块


    hook模块

    以下是从sylar服务器中学的,对其的复习;
    参考资料

    hook系统底层和socket相关的API,socket IO相关的API,以及sleep系列的API。hook的开启控制是线程粒度的,可以自由选择。通过hook模块,可以使一些不具异步功能的API,展现出异步的性能,如MySQL。

    hook概述

    hook实际上就是对系统调用API进行一次封装,将其封装成一个与原始的系统调用API同名的接口,应用在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API。

    hook技术可以使应用程序在执行系统调用之前进行一些隐藏的操作,比如可以对系统提供malloc()和free()进行hook,在真正进行内存分配和释放之前,统计内存的引用计数,以排查内存泄露问题。

    • hook功能

    hook的目的是在不重新编写代码的情况下,把老代码中的socket IO相关的API都转成异步,以提高性能。

    • hook实现

    hook的实现机制非常简单,就是通过动态库的全局符号介入功能,用自定义的接口来替换掉同名的系统调用接口。由于系统调用接口基本上是由C标准函数库libc提供的,所以这里要做的事情就是用自定义的动态库来覆盖掉libc中的同名符号。

    基于动态链接的hook有两种方式,第一种是外挂式hook,也称为非侵入式hook,通过优先加自定义载动态库来实现对后加载的动态库进行hook,这种hook方式不需要重新编译代码
    下面在不重新编译代码的情况下,用自定义的动态库来替换掉可执行程序a.out中的write实现,新建hook.c

    #include 
    #include 
    #include 
     
    ssize_t write(int fd, const void *buf, size_t count) {
        syscall(SYS_write, STDOUT_FILENO, "12345\n", strlen("12345\n"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里实现了一个write函数,这个函数的签名和libc提供的write函数完全一样,函数内容是用syscall的方式直接调用编号为SYS_write的系统调用,实现的效果也是往标准输出写内容,只不过这里我们将输出内容替换成了其他值。将hook.c编译成动态库:

    gcc -fPIC -shared hook.c -o libhook.so
    
    • 1

    通过设置 LD_PRELOAD环境变量,将libhoook.so设置成优先加载,从面覆盖掉libc中的write函数

    # LD_PRELOAD="./libhook.so" ./a.out
    12345
    
    • 1
    • 2

    这里我们并没有重新编译可执行程序a.out,但是可以看到,write的实现已经替换成了我们自己的实现。究其原因,就是LD_PRELOAD环境变量,它指明了在运行a.out之前,系统会优先把libhook.so加载到了程序的进程空间,使得在a.out运行之前,其全局符号表中就已经有了一个write符号,这样在后续加载libc共享库时,由于全局符号介入机制,libc中的write符号不会再被加入全局符号表,所以全局符号表中的write就变成了我们自己的实现。

    第二种方式的hook是侵入式的,需要改造代码或是重新编译一次以指定动态库加载顺序。如果是以改造代码的方式来实现hook,那么可以像下面这样直接将write函数的实现放在main.c里,那么编译时全局符号表里先出现的必然是main.c中的write符号:

    #include 
    #include 
    #include 
     
    ssize_t write(int fd, const void *buf, size_t count) {
        syscall(SYS_write, STDOUT_FILENO, "12345\n", strlen("12345\n"));
    }
     
    int main() {
        write(STDOUT_FILENO, "hello world\n", strlen("hello world\n")); // 这里调用的是上面的write实现
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    关于如何找回已经被全局符号介入机制覆盖的系统调用接口,因为大部分情况下,系统调用提供的功能都是无可替代的,我们虽然可以用hook的方式将其替换成自己的实现,但是最终要实现的功能,还是得由原始的系统调用接口来完成。在Linux中,这个方法就是dslym

    sylar hook模块设计

    sylar的hook功能以线程为单位,可自由设置当前线程是否使用hook。默认情况下,协程调度器的调度线程会开启hook,而其他线程则不会开启。

    sylar对以下函数进行了hook,并且只对socket fd进行了hook,如果操作的不是socket fd,那会直接调用系统原本的API,而不是hook之后的API

    sleep
    usleep
    nanosleep
    socket
    connect
    accept
    read
    readv
    recv
    recvfrom
    recvmsg
    write
    writev
    send
    sendto
    sendmsg
    close
    fcntl
    ioctl
    getsockopt
    setsockopt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    除此外,sylar还增加了一个 connect_with_timeout 接口用于实现带超时的connect。

    为了管理所有的socket fd,sylar设计了一个FdManager类来记录所有分配过的fd的上下文,这是一个单例类,每个socket fd上下文记录了当前fd的读写超时,是否设置非阻塞等信息。

    关于hook模块和IO协程调度的整合。一共有三类接口需要hook,如下:

    1. sleep延时系列接口,包括sleep/usleep/nanosleep。对于这些接口的hook,只需要给IO协程调度器注册一个定时事件,在定时事件触发后再继续执行当前协程即可。当前协程在注册完定时事件后即可yield让出执行权。

    2. socket IO系列接口,包括read/write/recv/send…等,connect及accept也可以归到这类接口中。这类接口的hook首先需要判断操作的fd是否是socket fd,以及用户是否显式地对该fd设置过非阻塞模式,如果不是socket fd或是用户显式设置过非阻塞模式,那么就不需要hook了,直接调用操作系统的IO接口即可。如果需要hook,那么首先在IO协程调度器上注册对应的读写事件,等事件发生后再继续执行当前协程。当前协程在注册完IO事件即可yield让出执行权。

    3. socket/fcntl/ioctl/close等接口,这类接口主要处理的是边缘情况,比如分配fd上下文,处理超时及用户显式设置非阻塞问题。

    socket fd上下文和FdManager的实现,这两个类用于记录fd上下文和保存全部的fd上下文,它们的关键实现如下

    /**
     * @brief 文件句柄上下文类
     * @details 管理文件句柄类型(是否socket)
     *          是否阻塞,是否关闭,读/写超时时间
     */
    class FdCtx : public std::enable_shared_from_this<FdCtx> {
    public:
        typedef std::shared_ptr<FdCtx> ptr;
        /**
         * @brief 通过文件句柄构造FdCtx
         */
        FdCtx(int fd);
        /**
         * @brief 析构函数
         */
        ~FdCtx();
        ....
    private:
        /// 是否初始化
        bool m_isInit: 1;
        /// 是否socket
        bool m_isSocket: 1;
        /// 是否hook非阻塞
        bool m_sysNonblock: 1;
        /// 是否用户主动设置非阻塞
        bool m_userNonblock: 1;
        /// 是否关闭
        bool m_isClosed: 1;
        /// 文件句柄
        int m_fd;
        /// 读超时时间毫秒
        uint64_t m_recvTimeout;
        /// 写超时时间毫秒
        uint64_t m_sendTimeout;
    };
     
    /**
     * @brief 文件句柄管理类
     */
    class FdManager {
    public:
        typedef RWMutex RWMutexType;
        /**
         * @brief 无参构造函数
         */
        FdManager();
     
        /**
         * @brief 获取/创建文件句柄类FdCtx
         * @param[in] fd 文件句柄
         * @param[in] auto_create 是否自动创建
         * @return 返回对应文件句柄类FdCtx::ptr
         */
        FdCtx::ptr get(int fd, bool auto_create = false);
     
        /**
         * @brief 删除文件句柄类
         * @param[in] fd 文件句柄
         */
        void del(int fd);
    private:
        /// 读写锁
        RWMutexType m_mutex;
        /// 文件句柄集合
        std::vector<FdCtx::ptr> m_datas;
    };
     
    /// 文件句柄单例
    typedef Singleton<FdManager> FdMgr;
    
    • 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
    • 67
    • 68
    • 69

    FdCtx类在用户态记录了fd的读写超时和非阻塞信息,其中非阻塞包括用户显式设置的非阻塞和hook内部设置的非阻塞,区分这两种非阻塞可以有效应对用户对fd设置/获取NONBLOCK模式的情形。

    另外注意一点,FdManager类对FdCtx的寻址采用了和IOManager中对FdContext的寻址一样的寻址方式,直接用fd作为数组下标进行寻址。

    接下来是hook的整体实现。首先定义线程局部变量t_hook_enable,用于表示当前线程是否启用hook,使用线程局部变量表示hook模块是线程粒度的,各个线程可单独启用或关闭hook。然后是获取各个被hook的接口的原始地址, 这里要借助dlsym来获取。

    #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)
     
    extern "C" {
    #define XX(name) name ## _fun name ## _f = nullptr;
        HOOK_FUN(XX);
    #undef XX
    }
     
    void hook_init() {
        static bool is_inited = false;
        if(is_inited) {
            return;
        }
    #define XX(name) name ## _f = (name ## _fun)dlsym(RTLD_NEXT, #name);
        HOOK_FUN(XX);
    #undef XX
    }
    
    • 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

    上面的宏展开之后的效果如下:

    
    extern "C" {
        sleep_fun sleep_f = nullptr; \
        usleep_fun usleep_f = nullptr; \
        ....
        setsocketopt_fun setsocket_f = nullptr;
    };
     
    hook_init() {
        ...
         
        sleep_f = (sleep_fun)dlsym(RTLD_NEXT, "sleep"); \
        usleep_f = (usleep_fun)dlsym(RTLD_NEXT, "usleep"); \
        ...
        setsocketopt_f = (setsocketopt_fun)dlsym(RTLD_NEXT, "setsocketopt");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    hook_init() 放在一个静态对象的构造函数中调用,这表示在main函数运行之前就会获取各个符号的地址并保存在全局变量中。

    最后是各个接口的hook实现,这部分和上面的全局变量定义要放在extern "C"中,以防止C++编译器对符号名称添加修饰。由于被hook的接口要完全模拟原接口的行为,所以这里要小心处理好各种边界情况以及返回值和errno问题。

    1. sleep/usleep/nanosleep的hook实现,它们的实现思路完全一样,即先添加定时器再yield
    2. socket接口的hook实现,socket用于创建套接字,需要在拿到fd后将其添加到FdManager中
    3. connect和connect_with_timeout的实现,由于connect有默认的超时,所以这里只需要实现connect_with_timeout即可

  • 相关阅读:
    win10 ping localhost 的IP被解析为::1
    企业级容器云PaaS解决方案【厚PaaS+轻应用+微服务】---(3)
    java实现hbase数据导出
    CTF-sql注入(get)【简单易懂】
    重学Elasticsearch第9章 : ES集群概念、节点故障恢复问题、路由计算、协调节点、倒排索引
    智能佳-E-PUCK2分布式单体智能集群协作机器人
    微信早安推送+定时任务配置(精简图文版)
    猿创征文|vue3+ts封装table组件并注册发布
    Postman —— 配置环境变量
    Android AIDL跨进程通信基础(多端情况)
  • 原文地址:https://blog.csdn.net/qq_60755751/article/details/134483311