文章目录
- 知识点
- HOOK实现方式
- 非侵入式hook
- 侵入式hook ⭐⭐⭐
- 覆盖系统调用接口
- 获取被全局符号介入机制覆盖的系统调用接口
- 具体实现
- C++ 模板成员函数继承 和 成员函数指针类型匹配 ⭐⭐⭐⭐⭐
- FdCtx 和 FdManager ⭐⭐
- 判断socket的小技巧
- FdCtx
- FdManager
- connect hook ⭐
- do_io模板 ⭐⭐⭐⭐⭐
- 记录超时信息,阻塞信息
在写之前模块的时候,我一直在困惑 协程是如何高效工作的,毕竟协程阻塞线程也就阻塞了。
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(LIBSsylaryaml-cpppthreaddl
)
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++ 模板成员函数继承 和 成员函数指针类型匹配 ⭐⭐⭐⭐⭐
- 模板成员函数继承的指针类型问题
// 基类 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
要求成员函数指针类型必须与对象类型严格匹配。
- 多继承场景下的 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 添加到 FdMgrsylar::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);// 获取 FdCtxsylar::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_SZcase 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_SZcase 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);
}