- 准备:提供两个接口控制当前线程的HOOK状态
- 准备:完成HOOK的初始化
- 辅助类:文件句柄类
FdCtx/文件句柄管理类FdManager - 1. HOOK
sleep、usleep、nanosleep系统函数 - C\C++知识点补充复习:dlsym函数
- C\C++知识点补充复习:nanosleep()函数
- C\C++知识点补充复习:attribute关键字
- C\C++知识点补充复习:fstat()函数
- C\C++知识点补充复习:ioctl()函数
- C\C++知识点补充复习:setsockopt()、getsockopt()函数
- include
- include
- include
- include
- C\C++知识点补充复习:
readv()/wrietv()函数 - C\C++知识点补充复习:
recvmsg()/sendmsg()函数 - C\C++知识点补充复习:可变参数列表
va_list - include
- include
概念:API HOOK顾名思义是,”钩住”函数接口,拦截、控制某一些API函数的调用。用于改造、改变、隐藏(木马病毒正是使用的这一种手段)API执行结果的一种技术。
注意:这里的所说的API HOOK和windows下的 Messages HOOK完全不同。API HOOK 是拦截目标程序中调用的函数,替换或者修改调用函数的功能。Messages HOOK是截获程序中的消息或事件
- 按照实现原理分:API HOOK 和 Messages HOOK
- 按照作用范围分:全局HOOK 和 局部HOOK
- 按照权限分:系统内核HOOK 和 用户态HOOK 分别作用在Ring 0 Ring 3
- 按照实现方式:内联HOOK(inline HOOK) 和 导入表HOOK(IAT HOOK)
- 内联HOOK(inline HOOK)(本次开发中使用)
程序在编译链接后成了二进制代码,我们可以找到需要Hook的函数的地址,然后把这个函数在内存中的二进制代码改为一个JMP指令,令其跳转到执行我们自己构造的函数。函数一般都存在于DLL中,当DLL中某个函数被调用后,其所在的DLL将会被映射到进程地址空间中。我们可以通过DLL这个模块找到我们需要Hook的函数的地址。然后在内存中改变其地址,使跳转到我们制定的位置。
(简而言之,内联HOOK的代码段是在运行起来之后,实施的代码段的动态替换)
- 导入表HOOK(IAT HOOK)
这种Hook技术是通过分析目标程序PE结构,替换目标API在IAT中的地址为钩子函数的地址来实现。
IAT表大致了解:可执行文件中使用其他 DLL 可执行文件的代码或数据,称为导入或者输入,当 PE 文件载入内存时,加载器会定位所有导入的函数或数据将定位到的内容填写至可执行文件的某个位置供其使用,而这个操作是需要借助导入表来完成的。
导入表中存放了程序所有使用的 DLL 模块名称及导入的函数名称或函数序号。

准备:提供两个接口控制当前线程的HOOK状态
bool IsHookEnable()://当前线程是否被hookbool IsHookEnable(){return t_hook_enable;}
void SetHookEnable(bool flag)//设置当前线程的hook状态void SetHookEnable(bool flag){t_hook_enable = flag;}
准备:完成HOOK的初始化
HOOK工作需要在程序真正开始执行前完成,即:在进入main()之前执行完毕。
namespace kit_server{static thread_local bool t_hook_enable = false;//小技巧,宏定义传参方便初始化/*等价于sleep_f = (sleep_func)dlsym(RTLD_NEXT, sleep);usleep_f = (usleep_func)dlsym(RTLD_NEXT, sleep);*/#define HOOK_FUNC(XX) \XX(sleep)\XX(usleep)void hook_init(){static bool is_inited = false;if(is_inited)return;#define XX(name) name ## _f = (name ## _func)dlsym(RTLD_NEXT, #name);HOOK_FUNC(XX)#undef XX}......}/*----------------------------------------------------------*/extern "C"{#define XX(name) name ## _func name ## _f = nullptr;HOOK_FUNC(XX)#undef XX/* 等价于sleep_func sleep_f = nullpt;usleep_func usleep_f = nullpt;*/}typedef unsigned int (*sleep_func)(unsigned int secends);extern sleep_func sleep_f;typedef int (*usleep_func)(useconds_t usec);extern usleep_func usleep_f;
方法一:利用C++特性,构造结构体变量,声明为static,利用其结构体的构造函数执行HOOK初始化
//在main() 函数之前执行hook初始化struct _HookIniter{_HookIniter(){hook_init();}};static struct _HookIniter _hook_initer;
方法二:利用C特性,使用__attribute__((constructor))声明,将一个函数置在main()之前执行
void hook_init() __attribute__((constructor));void hook_init(){static bool is_inited = false;if(is_inited)return;#define XX(name) name ## _f = (name ## _func)dlsym(RTLD_NEXT, #name);HOOK_FUNC(XX)#undef XX}
辅助类:文件句柄类FdCtx/文件句柄管理类FdManager
目的:在用户态记录文件句柄相关的状态/信息,方便HOOK操作时候,获取对应文件句柄,也能减少系统调用对文件句柄的检查。
· 类关系
1. FdCtx类
1.1 成员变量
/*** @brief 文件句柄类* @details 管理文件句柄的类型、阻塞状态、关闭状态、读/写超时时间*/class FdCtx: public std::enable_shared_from_this<FdCtx>{......private:/*采用位域 来记录文件句柄上的一些状态*//// 是否初始化bool m_isInit: 1;/// 是否是套接字socket类型bool m_isSocket: 1;/// 是否是用户主动设置为非阻塞bool m_userNonblock: 1;/// 是否hook非阻塞bool m_sysNonblock: 1;/// 是否关闭bool m_isClosed: 1;/// 文件句柄int m_fd;/// 读超时时间uint64_t m_recvTimeout = 0;/// 写超时时间uint64_t m_sendTimeout = 0;};
1.2 接口
1.2.1 构造函数
/*** @brief 文件句柄类构造函数* @param[in] fd 传入fd文件句柄*/FdCtx(int fd);FdCtx::FdCtx(int fd):m_isInit(false),m_isSocket(false),m_userNonblock(false),m_sysNonblock(false),m_isClosed(false),m_fd(fd),m_recvTimeout(-1),m_sendTimeout(-1){init();}
1.2.1 init()(私有)(核心)
功能:文件句柄初始化,如果为socket套接字句柄fd要将其置为非阻塞。
/*** @brief 文件句柄初始化,如果为socket套接字句柄fd置为非阻塞* @return true 初始化成功* @return false 初始化失败*/bool init();bool FdCtx::init(){//句柄初始化过了 就返回if(m_isInit)return true;//struct stat 获取当前系统文件句柄的状态struct stat fd_stat;if(fstat(m_fd, &fd_stat) < 0){m_isInit = false;m_isSocket = false;KIT_LOG_ERROR(g_logger) << "FdCtx init(): fstat() error";}else{m_isInit = true;//取出状态位 判断句柄类型m_isSocket = S_ISSOCK(fd_stat.st_mode);}//如果是socket 句柄 设置为非阻塞if(m_isSocket){int flags = fcntl_f(m_fd, F_GETFL, 0);//如果句柄阻塞 要设置为非阻塞if(!(flags & O_NONBLOCK)){fcntl_f(m_fd, F_SETFL, flags | O_NONBLOCK);}m_sysNonblock = true;}else{m_sysNonblock = false;}m_userNonblock = false;m_isClosed = false;return m_isInit;}
1.2.2 setTimeout()/getTimeout() (核心)
功能:设置/获取文件句柄上的超时时间,分为读(SO_REVTIMEO)/写(SO_SNDTIMEO)超时类型。
/*** @brief 设置IO超时时间* @param[in] type 读(SO_REVTIMEO)/写(SO_SNDTIMEO)超时类型* @param[in] time 超时时间,单位ms*/void setTimeout(int type, uint64_t time);void FdCtx::setTimeout(int type, uint64_t time){if(type == SO_RCVTIMEO)m_recvTimeout = time;elsem_sendTimeout = time;}/*** @brief 获取IO超时时间* @param[in] type 读(SO_REVTIMEO)/写(SO_SNDTIMEO)超时类型* @return uint64_t*/uint64_t getTimeout(int type);uint64_t FdCtx::getTimeout(int type){if(type == SO_RCVTIMEO)return m_recvTimeout;elsereturn m_sendTimeout;}
1.2.3 其他常用接口
/*** @brief 文件句柄是否已经初始化* @return true 已经初始化* @return false 没有初始化*/bool isInit() const {return m_isInit;}/*** @brief 文件句柄是否是套接字socket类型* @return true 是socket* @return false 不是socket*/bool isSocket() const {return m_isSocket;}/*** @brief 文件句柄是否已经关闭* @return true 已经关闭* @return false 没有关闭*/bool isClose() const {return m_isClosed;}//设定用户级 非阻塞状态/*** @brief 设定是否是用户设置非阻塞状态* @param[in] flag*/void setUserNonblock(bool flag) {m_userNonblock = flag;}/*** @brief 获取是否是用户设置非阻塞状态* @return true 是* @return false 不是*/bool getUserNonblock() const {return m_userNonblock;}/*** @brief 设置系统级 非阻塞状态* @param[in] flag*/void setSysNonblock(bool flag) {m_sysNonblock = flag;}/*** @brief 获取系统级 非阻塞状态* @return true 是* @return false 不是*/bool getSysNonblock() const {return m_sysNonblock;}/*** @brief 获取文件句柄* @return int*/int getFd() const {return m_fd;}
2. FdManager类
功能:获取/设置fd属性的操作属于”读多写少”的场景,使用读写锁管理文件句柄的队列。并且将该类置为单例模式。
2.1 成员变量
/*** @brief 文件句柄管理类*/class FdManager{......private:/// 读写锁MutexType m_mutex;/// 文件句柄对象队列std::vector<FdCtx::ptr> m_fds;};//置为单例typedef Single<FdManager> FdMgr;
2.2 接口
2.2.1 构造函数
/*** @brief 文件句柄管理类构造函数*/FdManager();FdManager::FdManager(){m_fds.resize(64);}
2.2.2 get() (核心)
功能:获取文件句柄对象,如果文件句柄对象不存在可以选择创建
/*** @brief 获取文件句柄对象* @param[in] fd 文件句柄* @param[in] auto_create 是否自动创建文件句柄对象* @return FdCtx::ptr*/FdCtx::ptr get(int fd, bool auto_create = false);FdCtx::ptr FdManager::get(int fd, bool auto_create){if(fd == -1)return nullptr;//加读锁MutexType::ReadLock lock(m_mutex);//如果fd越界说明容量不够if((int)m_fds.size() <= fd){//不需要创建if(!auto_create)return nullptr;}else //fd没有越界{if(m_fds[fd] || !auto_create)return m_fds[fd];}lock.unlock();//加写锁MutexType::WriteLock lock2(m_mutex);FdCtx::ptr ctx(new FdCtx(fd));if(fd >= (int)m_fds.size())m_fds.resize(fd * 1.5);m_fds[fd] = ctx;return ctx;}
2.2.3 del()
功能:删除文件句柄对象
/*** @brief 删除文件句柄对象* @param fd*/void del(int fd);void FdManager::del(int fd){MutexType::WriteLock lock(m_mutex);if((int)m_fds.size() <= fd)return;KIT_LOG_DEBUG(g_logger) << "移除当前fd = " << fd;m_fds[fd].reset();}
1. HOOKsleep、usleep、nanosleep系统函数
功能:使用我们自定义的休眠函数,替换系统调用。
在作用域extern "C"中加入对应的休眠函数的实现,防止C++重载特性破坏。
小难点:bind()函数绑定模板类函数,需要将传入的参数类型显式指定,并且默认参数值也需要显示指定
**sleep()**函数: ```cpp extern “C” {… …
unsigned int sleep(unsigned int secends) { //如果当前线程没有被hook就返回旧的系统调用执行 if(!kit_server::t_hook_enable) return sleep_f(secends);
kit_server::Coroutine::ptr cor = kit_server::Coroutine::GetThis();kit_server::IOManager* iom = kit_server::IOManager::GetThis();//bind 模板函数时候 需要把对应的参数类型设定好 默认参数也需要显示指定iom->addTimer(secends * 1000, std::bind((void(kit_server::Scheduler::*)(kit_server::Coroutine::ptr, int))&kit_server::IOManager::schedule, iom, cor, -1));// iom->addTimer(secends * 1000, [iom, cor](){// iom->schedule(cor);// });kit_server::Coroutine::YieldToHold();return 0;
}
- `**usleep()**`**函数:**```cppextern "C"{......int usleep(useconds_t usec){//如果当前线程没有被hook就返回旧的系统调用执行if(!kit_server::t_hook_enable)return usleep_f(usec);kit_server::Coroutine::ptr cor = kit_server::Coroutine::GetThis();kit_server::IOManager* iom = kit_server::IOManager::GetThis();iom->addTimer(usec / 1000, std::bind((void(kit_server::Scheduler::*)(kit_server::Coroutine::ptr, int))&kit_server::IOManager::schedule, iom, cor, -1));// iom->addTimer(usec / 1000, [iom, cor](){// iom->schedule(cor);// });kit_server::Coroutine::YieldToHold();return 0;}
**nanosleep()**: ```cpp int nanosleep(const struct timespec req, struct timespec rem) { if(!kit_server::t_hook_enable)return nanosleep_f(req, rem);
kit_server::Coroutine::ptr cor = kit_server::Coroutine::GetThis();kit_server::IOManager* iom = kit_server::IOManager::GetThis();uint64_t ms = 0;if(errno == EINTR){if(rem != nullptr)ms = rem->tv_sec * 1000 + rem->tv_nsec / 1000 / 1000;}elsems = req->tv_sec * 1000 + req->tv_nsec / 1000 / 1000;iom->addTimer(ms, std::bind((void(kit_server::Scheduler::*)(kit_server::Coroutine::ptr, int))&kit_server::IOManager::schedule, iom, cor, -1));// iom->addTimer(ms, [iom, cor](){// iom->schedule(cor);// });kit_server::Coroutine::YieldToHold();return 0;
}
<a name="OJpmd"></a># 2. HOOK socket API目的:通过截取系统事件对应的系统调用函数,在其函数中重新自定义我们所需要的一些操作。结合本使用案例,就是需要完成`socket`原有API的功能基础上,配合`class:FdCtx`套接字状态类以及`class:FdManager`套接字管理类,能够在用户态层面管理套接字的相关属性、信息。<a name="pDLWO"></a>## 2.1 核心模板函数:`do_io`功能:复用HOOK IO时候重复的一些代码功能。尤其对于读写IO,执行的操作基本是一样的。对于一些特定的IO就写特定的功能即可。```cpptemplate<class OriginFunc, typename... Args>static ssize_t do_io(int fd, OriginFunc func, const char *hook_func_name,uint32_t event, int timeout_so, Args&&... args)
- 核心逻辑:
从
FdManager中通过get()获取当前文件描述符fd的对象FdCtxFdManger不存在当前的文件描述符fd,我们认为它不是一个socket,执行原来的系统调用- 该描述符是
socket但是已经被关闭,就返回错误 - 该描述符明确不是
socket或者 已经被设置为NonBlock非阻塞状态,执行原来的系统调用
获取当前套接字上的读/写超时时间
getTimeout(),并且通过shared_ptr<>设置一个定时器的条件(结构体struct timer_info)用于判定定时器是否是超时被执行。
**RETRY:**
对读写IO执行一次IO操作。
ssize_t n = func(fd, std::forward<Args>(args)...);- 判断返回值是否出错,是否属于被中断
errno = EINTR。如果是,需要对重试IO操作 - 判断返回值是否出错,是否属于阻塞状态
errno = EAGAIN。如果是:- 添加一个条件定时器
ConditionTimer,将 2. 中的设置的条件赋值给弱指针weak_ptr<>管理。定时器到时后,取消对应fd上的事件并且强制触发回调函数。 IOManagerIO调度器为fd添加所需的读/写事件,设置异步回调- 将当前协程挂起让出执行权
Coroutine::YieldToHold()
- 添加一个条件定时器
- 判断返回值是否出错,是否属于被中断
等待协程切回(从
Coroutine::YieldToHold()返回),读事件READ返回继续执行read;写事件WRITE返回继续执行write。
- 切回的条件:
- IO没有数据到达,条件定时器超时强制唤醒:
取消全部套接字全部事件cancelEvent(),没有指定回调函数function<> cb,主动触发事件triggerEvent()默认将当前的协程作为任务加入队列,执行上一次没执行完的协程内容。
- 定时器还没有超时,IO活跃有数据到达触发回调
根据epoll_wait()带回的活跃IO信息epoll_event拿到对应的套接字对象FdCtx,主动触发事件triggerEvent()默认将当前的协程作为任务加入队列,执行上一次没执行完的协程内容。
- 检查之前的条件定时器是否还存在,存在就要将其取消,否则还会超时触发
- 检查超时条件是否有值,有值说明还没释放,返回错误
goto RETRY继续IO操作,读写数据
2.2 需要HOOK的一些API
2.2.1 读/写相关API
统一使用do_io()函数处理,因为这些API几乎都是容易阻塞的,而阻塞就需要HOOK来和协程结合,达到同步编程异步效率的效果。
read/writereadv/writevrecv/sendrecvfrom/sendtorecvmsg/sendmsg
2.2.2 socket相关API
这类接口比较特殊,需要单独处理,不能使用do_io()来概括。
2.2.2.1 socket()
功能:创建套接字的同时,需要把系统返回的fd通过FdManager在用户态维护在FdCtx中。
int socket(int domain, int type, int protocol){if(!kit_server::t_hook_enable)return socket_f(domain, type, protocol);KIT_LOG_DEBUG(g_logger) << "hook socket start";int fd = socket_f(domain, type, protocol);if(fd < 0)return fd;//由FdManager创建一个fdkit_server::FdMgr::GetInstance()->get(fd, true);return fd;}
2.2.2.2 accept()
功能:接受客户端连接返回通信套接字,要把系统返回的fd通过FdManager在用户态维护在FdCtx中。
由于accept本身是必然的会阻塞的,因此可以使用do_io()来HOOK它,和socket()类似
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){int fd = do_io(sockfd, accept_f, "accept", kit_server::IOManager::Event::READ,SO_RCVTIMEO, addr, addrlen);if(fd >= 0){//把新建立的通信套接字加入到FdManager中去管理kit_server::FdMgr::GetInstance()->get(fd, true);}return fd;}
2.2.2.3 close()
功能:关闭文件描述符,要使用系统调用的之前,需要检查对应的fd是否存在于FdManager中,存在需要先将其删除,在关闭。
int close(int fd){if(!kit_server::t_hook_enable)return close_f(fd);KIT_LOG_DEBUG(g_logger) << "hook close start";kit_server::FdCtx::ptr ctx = kit_server::FdMgr::GetInstance()->get(fd);//如果是socketif(ctx){KIT_LOG_DEBUG(g_logger) << "get ctx fd=" << ctx->getFd();auto iom = kit_server::IOManager::GetThis();if(iom){iom->cancelAll(fd);}kit_server::FdMgr::GetInstance()->del(fd);}return close_f(fd);}
2.2.2.4 connect()/connect_with_timeout()(重点)
功能:连接服务器,原本的系统调用不具备手动设置超时的功能,导致默认的超时设置时间太长,不同环境下时长不同,在75s到几分钟之间;而且远端服务器不存在或者未开启将一直处于阻塞(套接字为阻塞状态时)
冷门易错点①:通过man查看Linux手册,connect返回的错误码中有一个是EINPROGRESS,对应的描述大致是:在非阻塞模式下,如果连接不能马上建立成功就会返回该错误码。返回该错误码,可以通过使用select或者poll来查看套接字是否可写,如果可写,再调用getsockopt来获取套接字层的错误码errno来确定连接是否真的建立成功,如果getsockopt返回的错误码是0则表示连接真的建立成功,否则返回对应失败的错误码。
冷门易错点②:针对select/poll/epoll监测管理的IO上的可读/可写事件,有两条规则:
- 当连接建立成功时,套接口描述符变成可写(连接建立时,写缓冲区空闲,所以可写)
- 当连接建立出错时,套接口描述符变成既可读又可写(由于有未决的错误,从而可读又可写)
[
](https://blog.csdn.net/li_qinging/article/details/102574446)
实现connect()的超时设置有如下思路:
利用
**select()/poll()**检测**socket**。- 创建一个
socket - 将套接字设置为
NonBlock非阻塞状态 - 执行
connect()之后,利用select检测该套接字上的是否可写,来判断connect()的结果 select()返回后还要进一步判断,getsockopt()检测连接是否有问题。有问题返回错误- 没有问题返回0,将
socket重新设置为非阻塞的
- 创建一个
本例利用条件定时器ConditionTimer,配合配置系统设定一个
**connect_with_timeout**的超时时间当系统调用
connect()没马上建立连接时:返回的是EINPROGRESS,利用ConditionTimer条件定时器,超时(默认5000ms)触发强行唤醒协程执行connect()成功时,epoll管理的IO会触发WRITE写事件;connect仍然存在异常时,epoll管理的IO会同时触发READ/WRITE可读可写事件,触发异步回调唤醒协程执行。协程切回后使用getsockopt()进一步判断连接是否真的成功建立
**connect()**返回错误码中还有一个是**EISCONN**,表示连接已经建立。
我们可以再一次调用connect()函数,如果返回的错误码是EISCONN,则表示连接建立成功,否则认为连接建立失败。
//带超时功能的connectint connect_with_timeout(int sockfd, const struct sockaddr *addr, socklen_t addrlen, uint64_t timeout_ms){if(!kit_server::t_hook_enable)return connect_f(sockfd, addr, addrlen);KIT_LOG_DEBUG(g_logger) << "hook connect start";kit_server::FdCtx::ptr ctx = kit_server::FdMgr::GetInstance()->get(sockfd);if(!ctx || ctx->isClose()){errno = EBADF;return -1;}if(!ctx->isSocket())return connect_f(sockfd, addr, addrlen);if(ctx->getUserNonblock())return connect_f(sockfd, addr, addrlen);//创建socket时候已经设置为 非阻塞的 因此这里不会阻塞int n = connect_f(sockfd, addr, addrlen);if(n == 0){return 0;}else if(n != -1 || errno != EINPROGRESS){return n;}kit_server::IOManager* iom = kit_server::IOManager::GetThis();kit_server::Timer::ptr timer;std::shared_ptr<kit_server::timer_info> tinfo(new kit_server::timer_info);std::weak_ptr<kit_server::timer_info> winfo(tinfo);if(timeout_ms != (uint64_t)-1){timer = iom->addConditionTimer(timeout_ms, [iom, winfo, sockfd](){auto t = winfo.lock();if(!t || t->canceled)return;t->canceled = ETIMEDOUT;iom->cancelEvent(sockfd, kit_server::IOManager::Event::WRITE);}, winfo);}//添加写事件是因为 connect成功后马上可写int ret = iom->addEvent(sockfd, kit_server::IOManager::Event::WRITE);if(ret == 0) //如果添加写事件成功{//切出kit_server::Coroutine::YieldToHold();//切回if(timer)timer->cancel();if(tinfo->canceled){errno = tinfo->canceled;return -1;}}else{if(timer)timer->cancel();KIT_LOG_ERROR(g_logger) << "connect: addEvent(" << sockfd << ", WRITE) error=" << errno << "is:" << strerror(errno);}//协程切回后 检查一下socket上是否有错误 才能最终判断连接是否建立int m_error = 0;socklen_t len = sizeof(int);if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &m_error, &len) < 0){KIT_LOG_ERROR(g_logger) << "connect with timeout getsockopt error";return -1;}//检测sockfd 上是否有错误发生 有就通过int error变量带回//没有错误才是真的建立连接成功if(!m_error)return 0;else{errno = m_error;return -1;}}//connect 较为复杂!int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen){return connect_with_timeout(sockfd, addr, addrlen, kit_server::s_connect_timeout);}
2.2.2.5 fcntl()
功能:设置/获取系统fd上的相关状态。同时还要将状态同步到用户态的FdCtx上
小技巧:HOOK fcntl()需要把它内部所有标志位都罗列重写,否则导致部分功能不可用。通过man查看Linux手册,按照传参类型进行一个分类,重点实现与我们代码模块相关的标志位,忽略其他无关的标志位,复用减少代码量,按int、void、锁相关struct flock*、进程组相关struct f_owner_ex *进行分类
- 重点实现:
**F_SETFL**设置文件状态
该标志位为int,通过可变参数传递,设置当前用户态FdCtx的信息,并设置系统中fd的状态是否是非阻塞的O_NONBLOCK
**F_GETFL**获取文件状态
该标志位为void,直接调用系统调用,根据传回的结果,又将用户态FdCtx对象的信息更新即可
//fcntl 用户级nonblockint 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);kit_server::FdCtx::ptr ctx = kit_server::FdMgr::GetInstance()->get(fd);if(!ctx || ctx->isClose() || !ctx->isSocket())return fcntl_f(fd, cmd, arg);ctx->setUserNonblock(arg & O_NONBLOCK);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);kit_server::FdCtx::ptr ctx = kit_server::FdMgr::GetInstance()->get(fd);if(!ctx || ctx->isClose() || !ctx->isSocket())return arg;if(ctx->getUserNonblock()){arg |= O_NONBLOCK;}else{arg &= ~O_NONBLOCK;}return arg;}break;/*int*/case F_DUPFD:case F_DUPFD_CLOEXEC:case F_SETFD:case F_SETOWN:case F_SETSIG:case F_SETLEASE:case F_NOTIFY:case F_SETPIPE_SZ:{int arg = va_arg(va, int);va_end(va);return fcntl_f(fd, cmd, arg);}break;/*void*/case F_GETFD:case F_GETOWN:case F_GETSIG:case F_GETLEASE:case F_GETPIPE_SZ:{va_end(va);return fcntl_f(fd, cmd);}break;/*锁*/case F_SETLK:case F_SETLKW:case F_GETLK:case F_OFD_SETLK:case F_OFD_SETLKW:case F_OFD_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_ex* arg = va_arg(va, struct f_owner_ex*);va_end(va);return fcntl_f(fd, cmd, arg);}break;default:va_end(va);return fcntl_f(fd, cmd);}}
2.2.2.6 ioctl()
功能:作为IO操作的杂项补充。主要关注套接字的非阻塞状态,设置该状态时候用户态FdCtx的信息也改
int ioctl(int fd, unsigned long request, ...){va_list va;va_start(va, request);void *arg = va_arg(va, void *);va_end(va);if(FIONBIO == request){bool user_nonblock = *(int*)arg;kit_server::FdCtx::ptr ctx = kit_server::FdMgr::GetInstance()->get(fd);if(!ctx || ctx->isClose() || !ctx->isSocket())return ioctl_f(fd, request, arg);ctx->setUserNonblock(user_nonblock);}return ioctl_f(fd, request, arg);}
2.2.2.7setsockopt()/getsockopt()
功能:设置/获取套接字的设置与属性。getsockopt()不需要进行HOOK操作;setsockopt()主要关注套接字收数据超时SO_RCVTIMEO/发数据超时SO_SNDTIMEO的设置,需要将信息也同步至用户态FdCtx
//getsockopt 不需要hookint getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen){return getsockopt_f(sockfd, level, optname, optval, optlen);}//setsockoptint setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen){if(!kit_server::t_hook_enable)return setsockopt_f(sockfd, level, optname, optval, optlen);if(level == SOL_SOCKET){if(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO){kit_server::FdCtx::ptr ctx = kit_server::FdMgr::GetInstance()->get(sockfd);if(ctx){const struct timeval *tv = (const struct timeval*)(optval);ctx->setTimeout(optname, tv->tv_sec * 1000 + tv->tv_usec / 1000);}}}return setsockopt_f(sockfd, level, optname, optval, optlen);}
C\C++知识点补充复习:dlsym函数
功能:从一个动态链接库或者可执行文件获取到符号地址
第一个参数:
handle是一个指向已经加载动态目标的句柄,可由dlopen来获取。- 提供两种特殊伪句柄:
- RTLD_DEFAULT
使用默认的共享目标搜索顺序来找所需符号第一次出现的地方。这个搜索范围包含可执行文件中的全局符号以及这个可执行文件的依赖项(也就是使用RTLD_GLOBAL标志动态加载的共享目标)
- RTLD_NEXT (通常使用)
在当前搜索顺序中找,在当前的动态库之后加载进来的动态库中寻找对应符号的地址。 这就允许向在另一个共享目标中的函数提供一层封装。这样一来,在一个预先加载的共享目标中定义的函数中,就可以找到并调用在另一个共享目标中的真正要执行的函数
- 第二个参数:
symbol是一个以null结尾的符号名,即:某一个已经编译通过生成动态库的函数/变量
#include <dlfcn.h>void *dlsym(void *handle, const char *symbol);Link with -ldl.
**first.c**:
gcc -fpic --shared first.c -o libfirst.so 编译为动态库的形式
#include <stdio.h>#include <dlfcn.h>void print_msg(){printf("hello im first lib\n");}void first(){printf("first init\n");}
**second.c**:
gcc -fpic --shared second.c -o libsecond.so 编译为动态库的形式
#include <stdio.h>#include <dlfcn.h>void print_msg(){printf("hello im second lib\n");}void second(){printf("second init\n");}
**wrap.c**:
gcc -fpic --shared wrap.c -o libwrap.so 编译为动态库的形式
#define _GNU_SOURCE#include <stdio.h>#include <dlfcn.h>#include <unistd.h>#include <sys/types.h>#include <stdlib.h>void (*f)();void (*f1)();void (*f2)();//在main()函数之前 加载load_func()函数void load_func() __attribute__((constructor));void load_func(){//打开libfirst.so动态库void (*p1)() = (void (*)())dlopen("./libfirst.so", RTLD_NOW);//打开libsecond.so动态库void (*p2)() = (void (*)())dlopen("./libsecond.so", RTLD_NOW);//从libfirst.so动态库中找出print_msg()f1 = (void (*)())dlsym(p1, "print_msg");//从libsecond.so动态库中找出print_msg()f2 = (void (*)())dlsym(p2, "print_msg");//使用dlsym 加载目标函数f = (void (*)())dlsym(RTLD_NEXT, "print_msg");char *s = dlerror();if(s)printf("dl error: %s\n", s);/*验证究竟先加载的是哪一个库里的函数*/printf("libfirst func is %p\n", f1);printf("libsecond func is %p\n", f2);printf("load first func is %p\n", f);dlclose(p1);dlclose(p2);}void print_msg(){printf("hello im wrap lib\n");f();}
**test_dl.c**:
gcc test_dl.c -c 编译为动态库的形式
#include <stdio.h>#include <dlfcn.h>#include <errno.h>void print_msg();void first();void second();int main(){first();second();print_msg();return 0;}
执行程序前需要设置一下环境变量LD_LIBRARY_PATH主要用于查找共享库时除了默认路径外的其他路径;
此处是把当前路径加入到查找路径的意思export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:**.** “ . “ 表示当前目录加入到查找路径
- 优先链接
first.c:
gcc -o a test_dl.o -lwrap -lfirst -lsecond -ldl -L.
- 优先链接
second.c:
gcc -o a test_dl.o -lwrap -lsecond -lfirst -ldl -L.
C\C++知识点补充复习:nanosleep()函数
功能:暂停当前线程的执行。直到指定的时间 req过去, 或发生中断。req:存储指定的超时时间rem:发生中断时,存储剩余的超时时间
#include <time.h>int nanosleep(const struct timespec *req, struct timespec *rem);
- 和
**sleep()**、**usleep()**相比优势:
- 睡眠时间精度更高,能够到纳秒级别
- 不与信号交互,使得恢复被信号中断的休眠任务更加容易
C\C++知识点补充复习:attribute关键字
功能:主要是用来在函数或数据声明中设置其属性。给函数赋给属性的主要目的在于让编译器进行优化。attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)
相关变量属性:
**__attribute__(aligned (xxx))**
指定变量或结构域的起始地址对齐(以字节为单位)
int x __attribute__ ((aligned (16))) = 0; // 16字节对齐struct foo {int x[2] __attribute__ ((aligned (8))); // 8字节对齐};
**__arrtibue__((packed))**
变量或结构域以最小对齐单位对齐,如变量以字节对齐,结构域以位对齐
struct foo{char a;int x[2] __attribute__ ((packed)); // it immediately follows a};
相关函数属性:
**__attribute__((constructor/destructor))**
该constructor属性使函数在执行进入之前被自动调用main ()。同样,destructor属性使函数在main ()完成或exit ()已被调用后自动调用。具有这些属性的函数对于初始化将在程序执行期间隐式使用的数据很有用。
C\C++知识点补充复习:fstat()函数
功能:获取文件相关信息。依赖struct stat结构体返回获取我们需要的文件信息。
#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>int stat(const char *pathname, struct stat *statbuf);int fstat(int fd, struct stat *statbuf);int lstat(const char *pathname, struct stat *statbuf);struct stat {undefineddev_t st_dev; // 文件的设备编号ino_t st_ino; // 结点mode_t st_mode; // 文件的类型和存取的权限nlink_t st_nlink; // 连到该文件的硬链接数目,新建的文件则硬连接数为 1uid_t st_uid; // 用户IDgid_t st_gid; // 组IDdev_t st_rdev; // 若此文件为设备文件,则为其设备的编号off_t st_size; // 文件字节数(文件大小)blksize_t st_blksize; // 块大小blkcnt_t st_blocks; // 块数time_t st_atime; // 最后一次访问时间time_t st_mtime; // 最后一次修改时间time_t st_ctime; // 最后一次改变时间};
常关注:权限位
mode_t st_mode该变量占 2 byte,共16位文件类型(12-15bit)<br /> (a) 、S_IFSOCK 0140000 socket(套接字)<br /> (b) 、S_IFLNK 0120000 symbolic link(符号链接--软连接)<br /> (c) 、S_IFREG 0100000 regular file(普通文件)<br /> (d)、 S_IFBLK 0060000 block device(块设备) <br /> (e) 、S_IFDIR 0040000 directory(目录)<br /> (f) 、 S_IFCHR 0020000 character device(字符设备)<br /> (g)、 S_IFIFO 0010000 FIFO(管道)
C\C++知识点补充复习:ioctl()函数
功能:作为IO操作的杂项补充,终端IO是使用ioctl操作最多的地方。ioctl() 系统调用操作特殊文件的底层设备参数。 特别是,字符特殊文件(例如,终端)的许多操作特性可以通过 ioctl() 请求进行控制。
一般在驱动开发上是侧重点。
#include <sys/ioctl.h>int ioctl(int fd, unsigned long request, ...);
C\C++知识点补充复习:setsockopt()、getsockopt()函数
功能:设置和获取socket套接字上的设置属性。
int getsockopt(int sockfd, int level, int optname, void optval, socklen_t optlen); int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- `**SOL_SOCKET**`**级别下,常用的套接字设置:**1. `SO_REUSEADDR` 地址复用功能。和TCP三次握手中`TIMEWAIT`状态有关,将这个参数置1的话,关闭`close/shutdown`的套接字的TCB 套接字控制块空间不会马上回收,会复用关闭连接的一些连接信息。<br />经典现象:TCP连接建立后,主动关闭服务器,导致的连接断开,不需要等待`TIMEWAIT`状态超时才释放连接,客户端再一次发起连接仍然能够连接成功服务器。2. `SO_SNDBUF/ SO_RCVBUF` 设置发送/接收缓冲区的大小2. `SO_KEEPALIVE` 套接字保活, 从内核层面开启TCP的keep-alive机制。2. `SO_SNDTIMEO/SO_RCVTIMEO` 设置发送/接收超时时间,一旦超时不管数据收发成功与否立马返回<a name="Y6O5H"></a># C\C++知识点补充复习:完美转发forward<>()函数**出处:《深入理解C++11新特性解析与应用》 P88**<br />概念:所谓完美转发(perfect forwarding),指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数。为了配合"右值引用"达到转发的"完美",C++11引入了"引用折叠"的新语言规则。```cpptypedef XX TR /*XX的类型见下表*/TR& a = 1;TR&& b = 2;

自己通俗的理解:一个模板函数的参数的不确定的情况下,如果它的参数需要一个传入一个左值对象,forward()就会自动的传入一个左值;相反,需要一个右值对象,forward()就会自动传入一个右值。并且这样传参还不产生额外的开销。
using namespace std;
void test(int &&v) { cout << “test1 run, v=” << v << endl; }
void test(int &v) { cout << “test2 run, v=” << v << endl; }
void test(const int &&v) { cout << “test3 run, v=” << v << endl; }
void test(const int &v) { cout << “test4 run, v=” << v << endl; }
template
int main() { int a = 1; int b = 2; const int c = 3; const int d = 4;
RunTest(a); //作为左值引用被转发RunTest(std::move(b)); //作为右值引用被转发RunTest(c); //作为const左值引用被转发RunTest(std::move(d)); //作为const右值引用被转发return 0;
}
- **用例2**```cpp#include <iostream>using namespace std;class A{public:A(int &&v) { cout << "int&&, v=" << v << endl;}A(int &v) { cout << "int&, v=" << v << endl;}};class B{public:template<class T1, class T2, class T3>B(T1 &&t1, T2 &&t2, T3 &&t3):_a1(std::forward<T1>(t1)),_a2(std::forward<T2>(t2)),_a3(std::forward<T3>(t3)){cout << "B coonstruct" << endl;}private:A _a1, _a2, _a3;};template<class T, class U>T run(U &&v){cout << "run1 run:";return T(std::forward<U>(v));}template<class T, class ...U>T run(U&& ...v){cout << "run2 run:";return T(std::forward<U>(v)...);}int main(){auto a = run<A>(1);int v = 2;auto b = run<A>(v);auto c = run<B>(1, v, 3);return 0;}

C\C++知识点补充复习:readv()/wrietv()函数
出处:《UNIX 环境高级编程 第3版》P420
解决的问题:①read()读取不连续的内存/write()发送不连续的内存时,需要经过多次的read()/write()函数调用,就会造成不停地调用系统调用和数据拷贝,开销较大。②read()若干分批将数据读入不同区域/write()若干分批将不同区域的数据连续写入文件,也会①中类似的问题
概念:readv()/write()函数可以只通过一次系统调用和数据拷贝,就能完成在文件以及进程间多个缓冲区之间传递数据。(完成"scatter read"分散读、"gather write"集中写的功能)
#include <sys/uio.h>/*iov ----- 数组首地址*//*iovcnt------ 数组元素的个数*/ssize_t readv(int fd, const struct iovec *iov, int iovcnt);ssize_t writev(int fd, const struct iovec *iov, int iovcnt);/*依赖的系统定义的结构体*/struct iovec {void *iov_base; /* Starting address 数据起始地址*/size_t iov_len; /* Number of bytes to transfer 数据缓存的大小*/};

- 用例

#include <iostream>#include <sys/uio.h>#include <sys/socket.h>#include <string.h>#include <unistd.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <errno.h>#include <fstream>using namespace std;int main(){struct iovec iov[10];for(int i = 0; i < 10;++i){iov[i].iov_base = new char[10];iov[i].iov_len = 10;}int fd = open("./tests/a.txt", O_RDONLY);if(fd < 0){cout << "open read erro, errno=" << errno << ",is:" << strerror(errno) << endl;return 0;}int ret = readv(fd, iov, 10);if(ret < 0){cout << "readv erro, errno=" << errno << ",is:" << strerror(errno) << endl;return 0;}cout << "read ret size = "<< ret << endl;for(int i = 0; i < 10;++i){cout << i << ": " << (char*)iov[i].iov_base << endl;}close(fd);int fd2 = open("./tests/b.txt", O_CREAT | O_RDWR, 0777);if(fd2 < 0){cout << "open write erro, errno=" << errno << ",is:" << strerror(errno) << endl;return 0;}ret = writev(fd2, iov, 10);if(ret < 0){cout << "readv erro, errno=" << errno << ",is:" << strerror(errno) << endl;return 0;}cout << "write ret size = "<< ret << endl;close(fd2);return 0;}

C\C++知识点补充复习:recvmsg()/sendmsg()函数
概念:就是recv()/write()、readv()/writev()函数的一个集合。这些函数的收发功能集合到了一起,方便使用。配置结构体struct msghdr的参数即可。
#include <sys/types.h>#include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);struct iovec { /* Scatter/gather array items */void *iov_base; /* Starting address */size_t iov_len; /* Number of bytes to transfer */};struct msghdr {void *msg_name; /* Optional address */socklen_t msg_namelen; /* Size of address */struct iovec *msg_iov; /* Scatter/gather array */size_t msg_iovlen; /* # elements in msg_iov */void *msg_control; /* Ancillary data, see below */size_t msg_controllen; /* Ancillary data buffer len */int msg_flags; /* Flags on received message */};
C\C++知识点补充复习:可变参数列表va_list
概念:va_list是C语言中解决可变参数问题的一组宏,可以解决传入不确定个数的参数以及不确定类型的参数,没有名字与参数与之对应,使得传参非常的灵活。
#include <stdarg.h>void va_start(va_list ap, last);type va_arg(va_list ap, type);void va_end(va_list ap);void va_copy(va_list dest, va_list src);
- 基本使用规则:
- 首先在函数里定义一个
va_list型的变量,这个变量是指向参数的指针; - 然后用VA_START宏初始化变量刚定义的
va_list变量; - 然后用
va_arg返回可变的参数,va_arg的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用va_arg获取各个参数) - 最后用
va_end宏结束可变参数的获取。
using namespace std;
void test(int num, …) { va_list vl; va_start(vl, num);
cout << "arg:";while(num--){cout << va_arg(vl, int) << " ";}cout << endl;va_end(vl);
}
int main() {
test(2, 1, 2);test(4, 5, 6, 7, 8);return 0;
}
```

