文章目錄
- 知識點
- 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);
}