【sylar-webserver】8 HOOK模块

在写之前模块的时候,我一直在困惑 协程是如何高效工作的,毕竟协程阻塞线程也就阻塞了。
HOOK模块解开了我的困惑。😎

知识点

HOOK实现方式

动态链接中的hook实现

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

基于动态链接的hook有两种方式:

非侵入式hook

第一种是外挂式hook,也称为非侵入式hook,通过优先加自定义载动态库来实现对后加载的动态库进行hook,这种hook方式不需要重新编译代码,考虑以下例子:

#include <unistd.h>
#include <string.h>

int main(){
    write(STDOUT_FILENO, "hello world\n", strlen("hello world\n")); // 调用系统调用write写标准输出文件描述符
    return 0;
}

编译运行

# gcc main.c
# ./a.out
hello world

ldd命令查看可执行程序的依赖的共享库

# ldd ./a.out 
        linux-vdso.so.1 (0x00007ffde42a4000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f80ec76e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f80ecd61000)

可以看到其依赖libc共享库,write系统调用就是由libc提供的。

下面在不重新编译代码的情况下,用自定义的动态库来替换掉可执行程序a.out中的write实现,新建hook.cc

#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
 
ssize_t write(int fd, const void *buf, size_t count) {
    syscall(SYS_write, STDOUT_FILENO, "12345\n", strlen("12345\n"));
}
gcc -fPIC -shared hook.cc -o libhook.so	# 把hook.cc编译成动态库

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

# LD_PRELOAD="./libhook.so" ./a.out 
12345

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

侵入式hook ⭐⭐⭐

libco,libgo 也是使用这种方式

第二种方式的hook是侵入式的,需要改造代码或是重新编译一次以指定动态库加载顺序。

覆盖系统调用接口

unsigned int sleep(unsigned int seconds){
	... 
}

直接写入文件,只需要比 libc 提前链接即可。

获取被全局符号介入机制覆盖的系统调用接口

dslym 函数原型

#define _GNU_SOURCE
#include <dlfcn.h>
 
void *dlsym(void *handle, const char *symbol);
  • 链接需要指定 -ldl 参数。
  • 使用dlsym找回被覆盖的符号,第一个参数固定为RTLD_NEXT,第二个参数是符号的名称。

具体实现

CMakeLists.txt

set(LIBS
    sylar
    yaml-cpp
    pthread
    dl
)
extern "C"{
 	// sleep 
    // 定义了函数指针类型 sleep_fun
    // 该类型对应原生 sleep 函数的签名(接收 unsigned int 参数,返回 unsigned int)
    typedef unsigned int (*sleep_fun)(unsigned int seconds);
    // 声明外部的全局函数指针变量 sleep_f,用于保存原始 sleep 函数的地址
    // 通过 sleep_f 仍能调用原版函数
    extern sleep_fun sleep_f;
}
#define HOOK_FUN(XX) \
		XX(sleep)

void hook_init(){
    static bool is_inited = false;
    if(is_inited){
        return;
    }
    //保存原函数:hook_init() 通过 dlsym(RTLD_NEXT, "sleep") 获取系统原版 sleep 函数的地址,保存到 sleep_f 指针
#define XX(name) name ## _f = (name ## _fun)dlsym(RTLD_NEXT, #name);
    HOOK_FUN(XX);
#undef XX 
}

extern "C" {
#define XX(name) name ## _fun name ## _f = nullptr;		// 初始化 sleep_fun sleep_f = nullptr;
    HOOK_FUN(XX);
#undef XX

// sleep
unsigned int sleep(unsigned int seconds){
    if(!sylar::t_hook_enable){
        return sleep_f(seconds);
    }

    sylar::Fiber::ptr fiber = sylar::Fiber::GetThis();
    sylar::IOManager* iom = sylar::IOManager::GetThis();
    /**
     * C++规定成员函数指针的类型包含类信息,即使存在继承关系,&IOManager::schedule 和 &Scheduler::schedule 属于不同类型。
     * 通过强制转换,使得类型系统接受子类对象iom调用基类成员函数的合法性。
     * 
     * schedule是模板函数
     * 子类继承的是模板的实例化版本,而非原始模板
     * 直接取地址会导致函数签名包含子类类型信息
     * 
     * std::bind 的类型安全机制
     * bind要求成员函数指针类型与对象类型严格匹配。当出现以下情况时必须转换:
     * 
     * 总结,当需要绑定 子类对象调用父类模板成员函数,父类函数需要强转成父类
     * (存在多继承或虚继承导致this指针偏移)
     * 
     * 或者
     * std::bind(&Scheduler::schedule, static_cast<Scheduler*>(iom), fiber, -1)
     * 
     */
    iom->addTimer(seconds * 1000 , std::bind((void(sylar::Scheduler::*)(sylar::Fiber::ptr, int thread))
                                                &sylar::IOManager::schedule, iom, fiber, -1));
    sylar::Fiber::GetThis()->yield();
    return 0;
}

C++ 模板成员函数继承 和 成员函数指针类型匹配 ⭐⭐⭐⭐⭐

  1. 模板成员函数继承的指针类型问题
// 基类 Scheduler 的模板函数
class Scheduler {
public:
    template<class FiberOrCb>
    void schedule(FiberOrCb fc, int thread = -1); // 模板函数
};

// 子类 IOManager 继承模板函数的实例化版本
class IOManager : public Scheduler {
    // 继承 schedule<sylar::Fiber::ptr> 的实例化版本
};
  • 问题本质:当子类继承模板成员函数时,&IOManager::schedule 的类型实际上时 void (IOManager::*)(sylar::Fiber::ptr, int)
  • (我的理解是模板函数的参数类型不确定,必须显示的转成确定的函数类型指针)
  • 类型不匹配:std::bind 要求成员函数指针类型必须与对象类型严格匹配。
  1. 多继承场景下的 this 指针偏移风险
// 强制转换的语法含义
(void(sylar::Scheduler::*)(sylar::Fiber::ptr, int)) 
    &sylar::IOManager::schedule
  • 类型安全:通过强制转换为基类成员函数指针类型:
    • 确保调用时正确进行 this 指针调整
    • 避免多继承场景下潜在的指针偏移错误
iom->addTimer(
    usec / 1000,
    std::bind(
        (void(Scheduler::*)(Fiber::ptr, int))  // 关键转换
            &IOManager::schedule,  // 原始成员函数指针
        iom,   // IOManager* 类型的对象
        fiber, // 参数1
        -1     // 参数2
    )
);

FdCtx 和 FdManager ⭐⭐

FdManager::get(fd) 
	| 
	|
	|
new FdCtx(fd) 
	| 
	|
	|
FdCtx::init()	// 获取到fd的基础信息 m_isInit,m_isSocket,m_sysNonblock(默认true), m_userNonblock(默认false,通过hook fcntl操作记录),m_isClosed, m_recvTimeout(-1),m_sendTimeout(-1)

m_userNonblock 阻塞属性 通过 hook fcntl -> setUserNonblock ⭐
m_recvTimeout,m_sendTimeout 超时事件 通过 hook setsockopt -> setTimeout 设置⭐

判断socket的小技巧

		/**
         * stat族 ⭐
         * 
         * 获取fd信息
         * int fstat(int filedes, struct stat *buf);
         * 返回值: 执行成功则返回0,失败返回-1,错误代码存于errno
         * 
         * 查看 stat 里的 st_mode 属性
         * 
         * 常用宏
            S_ISLNK(st_mode):是否是一个连接.
            S_ISREG是否是一个常规文件.
            S_ISDIR是否是一个目录
            S_ISCHR是否是一个字符设备.
            S_ISBLK是否是一个块设备
            S_ISFIFO是否是一个FIFO文件.
            S_ISSOCK是否是一个SOCKET文件. 
         */
        struct stat fd_stat;
        if(-1 == fstat(m_fd, &fd_stat)){
            m_isInit = false;
            m_isSocket = false;
        }else{
            m_isInit = true;
            m_isSocket = S_ISSOCK(fd_stat.st_mode);
        }

FdCtx


class FdCtx : public std::enable_shared_from_this<FdCtx>{
public:
    typedef std::shared_ptr<FdCtx> ptr;
    FdCtx(int fd);
    ~FdCtx();
    bool init();
    bool isInit() const {return m_isInit;}
    bool isSocket() const {return m_isSocket;}
    bool isClose() const {return m_isClosed;}
    void setUserNonblock(bool v){m_userNonblock = v;}
    bool getUserNonblock() const {return m_userNonblock;}
    void setSysNonblock(bool v){m_sysNonblock = v;}
    bool getSysNonblock() const {return m_sysNonblock;}

    /**
     * @brief 设置超时时间
     * @param[in] type 类型SO_RCVTIMEO(读超时), SO_SNDTIMEO(写超时)
     * @param[in] v 时间毫秒
     */
    void setTimeout(int type, uint64_t v);

    /**
     * @brief 获取超时时间
     * @param[in] type 类型SO_RCVTIMEO(读超时), SO_SNDTIMEO(写超时)
     * @return 超时时间毫秒
     */
    int getTimeout(int type);

private:
	// 使用位域,可能考虑到会有大量的fd连接,节省空间。⭐
    bool m_isInit: 1;
    bool m_isSocket: 1;
    bool m_sysNonblock: 1;   // 是否 hook 非阻塞
    bool m_userNonblock: 1;  // 是否 用户主动设置 非阻塞
    bool m_isClosed: 1;
    int m_fd;
    uint64_t m_recvTimeout;  // 读超时时间毫秒
    uint64_t m_sendTimeout;  // 写超时时间毫秒
};

FdManager

class FdManager{
public:
    typedef RWMutex RWMutexType;

    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;
};

connect hook ⭐

int connect_with_timeout(int fd, const struct sockaddr *addr, socklen_t addrlen, uint64_t timeout_ms){

    ... 
    
	/**
     * 非阻塞connect调用会立即返回EINPROGRESS错误码,表示连接正在建立
     * 此时不需要也不能重复调用connect,否则可能触发EALREADY错误 (和 do_io 不同的地方) ⭐
     * 通过等待WRITE事件即可判断连接是否建立完成
     */
    int n = connect_f(fd, addr, addrlen);
    if(n == 0){
        return 0;
    }else if(n != -1 || errno != EINPROGRESS){ 
        return n;
    }
	// 下面和 do_io 类似
	sylar::IOManager* iom = sylar::IOManager::GetThis();
    sylar::Timer::ptr timer;
    std::shared_ptr<timer_info> tinfo(new timer_info);
    std::weak_ptr<timer_info> winfo(tinfo);
    
	if(timeout_ms != (uint64_t)-1){
        iom->addConditionTimer(timeout_ms, [winfo, fd, iom](){
            auto it = winfo.lock();
            if(!it || it->cancelled){
                return;
            }
            it->cancelled = ETIMEDOUT;
            iom->cancelEvent(fd, sylar::IOManager::Event::WRITE);
        }, winfo);
    }
	int rt = iom->addEvent(fd, sylar::IOManager::Event::WRITE);
    if(rt == 0){
        sylar::Fiber::GetThis()->yield();
        if(timer){
            timer->cancel();
        }
        if(tinfo->cancelled){
            errno = tinfo->cancelled;
            return -1;
        }
    }else{
        if(timer) {
            timer->cancel();
        }
        SYLAR_LOG_ERROR(g_logger) << "connect addEvent(" << fd << ", WRITE) error";
    }

    int error = 0;
    socklen_t len = sizeof(int);
    // 非阻塞 connect 操作返回 EINPROGRESS 后,通过监听写事件完成连接建立,此时需要检查实际连接结果
    if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len)){
        return -1;
    }

    if(!error){
        return 0;
    }else{
        errno = error;
        return -1;
    }
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen){
	 // static uint64_t s_connect_timeout = -1;
    return connect_with_timeout(sockfd, addr, addrlen, sylar::s_connect_timeout);
}

do_io模板 ⭐⭐⭐⭐⭐

/**
 * 重点 !!!
 * 
 * 模板函数,通用的 read-write api hook 操作
 * 
 * Args&& 万能引用,根据传入实参自动推导
 * 
 * 这里Args,可能是左值,也可能是右值
 * 
 * std::forward 保持参数的原始值类别 
 */ 
template<typename OriginFun, typename ... Args> // 常用⭐
static ssize_t do_io(int fd, 						
                    OriginFun fun, 					// hook的原库函数
                    const char* hook_fun_name, 		// debug输出,hook的函数名
                    uint32_t event, 
                    int timeout_so,     			// 读 / 写 超时 宏标签
                    Args&&... args)
{
	// Scheduler::run() 设置当前线程是否hook ⭐
	if(!sylar::t_hook_enable){
		return fun(fd, std::forward<Args>(args)...);
	}

	// fd 添加到 FdMgr
	sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
	if(!ctx){
		return fun(fd, std::forward<Args>(args)...);
	}

	// 如果ctx关闭
    if(ctx->isClose()){
        errno = EBADF;
        return -1;
    }

	// 不是 socket 或者 用户设定了非阻塞
	// 用户设定了非阻塞,意味着自行处理非阻塞逻辑
	f(!ctx->isSocket() || ctx->getUserNonblock()){
        return fun(fd, std::forward<Args>(args)...);
    }

	uint64_t to = ctx->getTimeout(timeout_so);	// 获取时间超时时间,通过setsockopt hook写入
	/*
struct timer_info{
    int cancelled = 0;
};
	*/
	std::shared_ptr<timer_info> tinfo(new timer_info);
	
retry:
	ssize_t n = fun(fd, std::forward<Args>(args)...);
	while(n == -1 && errno == EINTR){		// 系统调用被信号中断
		n = fun(fd, std::forward<Args>(args)...);
	}
	if(n == -1 && errno == RAGAIN){			// 非阻塞操作无法立即完成
		sylar::IOManager* iom = sylar::IOManager::GetThis();
		sylar::Timer::ptr timer;
		std::weak_ptr<timer_info> winfo(tinfo);
		
		if(to != (uint64_t)-1){
			// 添加一个条件定时器,如果 tinfo 还在意味着 fd还没等到event触发。
            // 到了超时时间,就直接取消事件。
            timer = iom->addConditionTimer(to, [iom, winfo, fd, event](){
				auto it = winfo.lock();
				if(!it || it->cancelled){ // 双重验证⭐
					return;
				}
				it->cancelled = ETIMEDOUT;
				// cancelEvent 取消事件触发条件,直接触发事件 ⭐
				iom->cancelEvent(fd, (sylar::IOManager::Event)event);
			}, winfo);
		}
		
		// 没传入fd,把当前协程传入。当事件触发,会回到这个协程继续运行
		int rt = iom->addEvent(fd, (sylar::IOManager::Event)event); // 正式 注册事件 ⭐
		if(rt != 0){	// 添加失败
			// 定时器删除
			if(timer){
				timer->cancel();   // 删除定时器的权利 交给了定时器
			}
			return rt;
		}else{
			sylar::Fiber::GetThis()->yield();
			
			/*
 			再次回到这里,两种情况:
 			1. 定时器触发之前,事件触发
 			2. 定时器触发,事件超时
			*/
			if(timer){
				timer->cancel();
			}
			if(tinfo->cancelled){		// 2. 超时
				errno = tinfo->cancelled;
				return -1;
			}
			goto retry;		// 1. 重新操作 fd
		}
	}
	return n;
}

使用案例

int accept(int s, struct sockaddr *addr, socklen_t *addrlen){
    int fd = do_io(s, accept_f, "accept", sylar::IOManager::Event::READ, SO_RCVTIMEO, addr, addrlen);
    if(fd != -1){
        sylar::FdMgr::GetInstance()->get(fd, true);
    }
    return fd;
}

ssize_t write(int fd, const void *buf, size_t count){
    return do_io(fd, write_f, "write", sylar::IOManager::Event::WRITE, SO_SNDTIMEO, buf, count);
}

记录超时信息,阻塞信息

// 增加fd事件超时选项,设置了超时事件,上面的hook才会有定时器,不然fd事件会一直存在
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen){
    if(!sylar::t_hook_enable){
        return setsockopt_f(sockfd, level, optname, optval, optlen);
    }
    if(level == SOL_SOCKET){
        if(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO){   // 超时事件设置
            sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(sockfd);
            if(ctx){
                const timeval* v = (const timeval*)optval;
                ctx->setTimeout(optname, v->tv_sec* 1000 + v->tv_usec / 1000);
            }
        }
    }
    return setsockopt_f(sockfd, level, optname, optval, optlen);
}
int fcntl(int fd, int cmd, ... /* arg */ ){
    va_list va;
    va_start(va, cmd);
    switch(cmd){
        case F_SETFL:
            {
                int arg = va_arg(va, int);
                va_end(va);
                // 获取 FdCtx
                sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
                if(!ctx || ctx->isClose() || !ctx->isSocket()){
                    return fcntl_f(fd, cmd, arg);
                }
                // 检查args,用户是否设置 非阻塞。
                // FdCtx里的m_userNonblock,这里设置。 ⭐
                ctx->setUserNonblock(arg & O_NONBLOCK);

                // 要执行了,所以把 hook 非阻塞直接加上。
                if(ctx->getSysNonblock()){
                    arg |= O_NONBLOCK;
                }else{
                    arg &= ~O_NONBLOCK;
                }

                return fcntl_f(fd, cmd, arg);
            }
            break;
        case F_GETFL:
            {
                va_end(va);
                int arg = fcntl_f(fd, cmd);
                sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
                if(!ctx || ctx->isClose() || !ctx->isSocket()){
                    return arg;
                }
                // 设置 用户是否判断 非阻塞。
                if(ctx->getUserNonblock()){
                    return arg | O_NONBLOCK;
                }else{ // 如果之前就没有,那么需要恢复默认。(Hook默认加上了非阻塞)⭐
                    return arg & ~O_NONBLOCK;
                }
            }
            break;
        case F_DUPFD:
        case F_DUPFD_CLOEXEC:
        case F_SETFD:
        case F_SETOWN:
        case F_SETSIG:
        case F_SETLEASE:
        case F_NOTIFY:
    #ifdef F_SETPIPE_SZ
        case F_SETPIPE_SZ:
    #endif
            {
                int arg = va_arg(va, int);
                va_end(va);
                return fcntl_f(fd, cmd, arg); 
            }
            break;
        case F_GETFD:
        case F_GETOWN:
        case F_GETSIG:
        case F_GETLEASE:
    #ifdef F_GETPIPE_SZ
        case F_GETPIPE_SZ:
    #endif
            {
                va_end(va);
                return fcntl_f(fd, cmd);
            }
             break;
        case F_SETLK:
        case F_SETLKW:
        case F_GETLK:
            {
                struct flock* arg = va_arg(va, struct flock*);
                va_end(va);
                return fcntl_f(fd, cmd, arg);
            }
            break;
        case F_GETOWN_EX:
        case F_SETOWN_EX:
            {
                struct f_owner_exlock* arg = va_arg(va, struct f_owner_exlock*);
                va_end(va);
                return fcntl_f(fd, cmd, arg);
            }
            break;
        default:
            va_end(va);
            return fcntl_f(fd, cmd);
    }
}

// ioctl 用于 设备驱动程序中设备控制接口函数 ⭐ 没用过
int ioctl(int d, unsigned long int request, ...){
    va_list va;
    va_start(va, request);
    void* arg = va_arg(va, void*);
    va_end(va);
    // FIONBIO(设置非阻塞模式)
    if(FIONBIO == request){ // 主要用于处理文件描述符的非阻塞模式设置
        bool user_nonblock = !!*(int*)arg;   // 将参数转换为布尔值
        sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(d);
        if(!ctx || ctx->isClose() || !ctx->isSocket()){
            return ioctl_f(d, request, arg);
        }
        ctx->setUserNonblock(user_nonblock);
    }
    return ioctl_f(d, request, arg);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值