1、Hook概述
- hook实际上就是对系统调用API进行一次封装,将其封装成一个与原始的系统调用API同名的接口,应用在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API。这些添加进去的操作为了完整想要的需求,而系统中提供的API又没有这些需求。比如可以对系统提供malloc()和free()进行hook,在真正进行内存分配和释放之前,统计内存的引用计数,以排查内存泄露问题。sylar框架中的Hook定义了线程局部变量t_hook_enable,用于表示当前线程是否启用hook,使用线程局部变量表示hook模块是线程粒度的,各个线程可单独启用或关闭hook
2、框架中Hook的设计目的 - 主要目的是对一些阻塞的操作进行hook,在用户看来这些操作还是阻塞的,但是系统中,这些阻塞的操作时间可以拿出来做有意义的事情,而不是让cpu阻塞在那,浪费资源。实现这些功能,就用到了之前实现的定时器和协程相关的功能。
3、如何使Hook函数生效
Sylar的代码中,hook.h头文件并没有函数声明,为什么可以在其他cc文件中,调用一系列的api呢?这是因为在CMakeList中编译可执行程序时,将sylar的动态库加载到了程序的进程空间,使得在执行可执行程序之前,其全局符号表中就有了函数对应的符号,这样在后续加载libc共享库时,由于全局符号介入机制,libc中的函数符号不会再被加入全局符号表,所以全局符号表中的函数就变成了我们自己的实现。通过ldd 可执行程序可以看到可执行程序依赖于sylar动态库:
其实还可以通过另一种方式使hook生效,就是在声明一遍函数签名,这样全局符号表中的符号自然就是自己写的函数。
上面的过程涉及到了查找后加载的动态库里被覆盖的符号地址问题。首先,这个操作本身就具有合理性,因为程序运行时,依赖的动态库无论是先加载还是后加载,最终都会被加载到程序的进程空间中,也就是说,那些因为加载顺序靠后而被覆盖的符号,它们只是被“雪藏”了而已,实际还是存在于程序的进程空间中的,通过一定的办法,可以把它们再找回来。在Linux中,这个方法就是dslym,它的函数原型如下:
// 使用dlsym找回被覆盖的符号时,第一个参数固定为 RTLD_NEXT,第二个参数为符号的名称
void *dlsym(void *handle, const char *symbol);
4、代码实现
- Socket Fd相关
/**
* @brief 文件句柄上下文类
* @details 管理文件句柄类型(是否socket)
* 是否阻塞,是否关闭,读/写超时时间
*/
//FdCtx类在用户态记录了fd的读写超时和非阻塞信息,其中非阻塞包括用户显式设置的非阻塞和hook内部设置的非阻塞,区分这两种非阻塞可以有效应对用户对fd设置/获取NONBLOCK模式的情形。
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;
- 查找原始函数地址
#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
}
- sleep/usleep/nanosleep的hook实现
//以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();
iom->addTimer(seconds * 1000, std::bind((void(sylar::Scheduler::*)
(sylar::Fiber::ptr, int thread))&sylar::IOManager::schedule
,iom, fiber, -1));
sylar::Fiber::GetThis()->yield();
//这一段是精华,利用定时器实现sleep,当时间到的时候,会再执行当前协程,只剩return
// 而在sleep的过程中,协程让出了cpu资源去做其它有意义的事情
return 0;
}
- connect_with_timeout
int connect_with_timeout(int fd, const struct sockaddr* addr, socklen_t addrlen, uint64_t timeout_ms) {
if(!sylar::t_hook_enable) {
return connect_f(fd, addr, addrlen);
}
sylar::FdCtx::ptr ctx = sylar::FdMgr::GetInstance()->get(fd);
if(!ctx || ctx->isClose()) {
errno = EBADF;
return -1;
}
if(!ctx->isSocket()) {
return connect_f(fd, addr, addrlen);
}
if(ctx->getUserNonblock()) {
return connect_f(fd, addr, addrlen);
}
int n = connect_f(fd, addr, addrlen);
if(n == 0) {
return 0;
} else if(n != -1 || errno != EINPROGRESS) {
return n;
}
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) {
timer = iom->addConditionTimer(timeout_ms, [winfo, fd, iom]() {
auto t = winfo.lock();
if(!t || t->cancelled) {
return;
}
t->cancelled = ETIMEDOUT;
iom->cancelEvent(fd, sylar::IOManager::WRITE);
}, winfo);
}
int rt = iom->addEvent(fd, sylar::IOManager::WRITE);
if(rt == 0) {
sylar::Fiber::GetThis()->yield();
// 这里的唤醒有两种方式
// 1、定时器超时
// 2、Event触发
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);
if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len)) {
return -1;
}
if(!error) {
return 0;
} else {
errno = error;
return -1;
}
}
/*
1、判断传入的fd是否为套接字,如果不为套接字,则调用系统的connect函数并返回。
2、判断fd是否被显式设置为了非阻塞模式,如果是则调用系统的connect函数并返回。
3、调用系统的connect函数,由于套接字是非阻塞的,这里会直接返回EINPROGRESS错误。
4、如果超时参数有效,则添加一个条件定时器,在定时时间到后通过t->cancelled设置超时标志并触发一次WRITE事件。
5、添加WRITE事件并yield,等待WRITE事件触发再往下执行。
6、等待超时或套接字可写,如果先超时,则条件变量winfo仍然有效,通过winfo来设置超时标志并触发WRITE事件,协程从yield点返回,返回之后通过超时标志设置errno并返回-1;如果在未超时之前套接字就可写了,那么直接取消定时器并返回成功
*/
- io相关的函数和上面的connect_with_timeout实现思想类似