文章目录
- 知识点
- HOOK实现方式
- 非侵入式hook
- 侵入式hook ⭐⭐⭐
- 覆盖系统调用接口
- 获取被全局符号介入机制覆盖的系统调用接口
- 具体实现
- 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(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;
}
FdCtx 和 FdManager
把 fd 封装一遍,添加非阻塞。
最后再还原会之前的设置
connect hook
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 或者 用户设定了非阻塞
}