- 准备:提供两个接口控制当前线程的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()
://当前线程是否被hook
bool 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;
else
m_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;
else
return 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()**`**函数:**
```cpp
extern "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;
}
else
ms = 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就写特定的功能即可。
```cpp
template<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
的对象FdCtx
FdManger
不存在当前的文件描述符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
上的事件并且强制触发回调函数。 IOManager
IO调度器为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
/write
readv
/writev
recv
/send
recvfrom
/sendto
recvmsg
/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创建一个fd
kit_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);
//如果是socket
if(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
,则表示连接建立成功,否则认为连接建立失败。
//带超时功能的connect
int 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 用户级nonblock
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);
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 不需要hook
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen)
{
return getsockopt_f(sockfd, level, optname, optval, optlen);
}
//setsockopt
int 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 {undefined
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // 结点
mode_t st_mode; // 文件的类型和存取的权限
nlink_t st_nlink; // 连到该文件的硬链接数目,新建的文件则硬连接数为 1
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_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引入了"引用折叠"的新语言规则。
```cpp
typedef 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;
}
![image.png](https://cdn.nlark.com/yuque/0/2022/png/25460685/1644049877297-fe2b9eb9-6c0d-4eb8-9674-3a11a407411b.png#clientId=u329b0b2f-5593-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=104&id=u89b7b35f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=104&originWidth=623&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10657&status=done&style=none&taskId=uf0c48bba-6abd-4855-b7c9-febf6ac24cb&title=&width=623)
- **用例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;
} ```