ip_file_Hook项目解读

程序流程

执行文件访问拦截和 IP 地址拦截的流程:

文件访问拦截功能:

  1. 当应用程序尝试执行文件操作,例如打开文件,调用的是 openopenat 函数。

  2. 由于这两个函数已经被重定向为自定义的版本,所以实际上调用的是 openopenat 函数的自定义替代版本。

  3. 自定义的 openopenat 函数首先检查传递的文件路径和执行文件操作的进程是否符合访问控制策略。

  4. 它检查文件路径是否在黑名单或白名单中,并检查执行文件操作的进程是否在白名单中。如果文件或进程不符合策略,函数会拒绝文件操作。

  5. 如果文件和进程都符合策略,它会记录文件访问操作的结果,然后调用真正的 openopenat 函数执行操作。

  6. 如果文件或进程不符合策略,它会记录拒绝的结果,并设置 errnoEPERM,表示权限被拒绝。

IP 地址拦截功能:

  1. 当应用程序尝试执行网络连接操作,例如使用 connect 函数或发送数据到远程主机,调用的是 connectsendto 函数。

  2. 与文件访问拦截类似,这两个函数也已被重定向为自定义版本,因此实际上调用的是自定义的 connectsendto 函数。

  3. 自定义的 connectsendto 函数首先检查传递的目标 IP 地址以及执行连接或数据发送操作的进程是否符合访问控制策略。

  4. 它检查 IP 地址是否在黑名单或白名单中,并检查执行操作的进程是否在白名单中。如果 IP 地址或进程不符合策略,函数会拒绝操作。

  5. 如果 IP 地址和进程都符合策略,它会记录 IP 操作的结果,然后调用真正的 connectsendto 函数执行操作。

  6. 如果 IP 地址或进程不符合策略,它会记录拒绝的结果,并设置 errnoEPERM,表示权限被拒绝。

总的来说,这两段代码的流程是先检查文件或 IP 地址是否符合访问控制策略,如果符合则允许操作并记录结果,如果不符合则拒绝操作并记录结果,以确保系统的安全性和遵守访问策略。

libhook.cpp

//验证文件访问是否受到权限限制
static bool
allow_open(const char *exe, const char *path, string &return_full_path) noexcept(true)
{
	try
	{
		// 检查传入的参数是否为NULL
		if (exe == NULL || path == NULL)
			return false;

		// 获取目标文件的绝对路径
		const string real_absolute_path = get_real_path(get_absolute_path(path).c_str());
		return_full_path = real_absolute_path;

		// 遍历文件黑名单,检查是否有文件受黑名单保护
		for (auto &i : file_black_list())
		{
			if (i.first == real_absolute_path)
			{
				// 文件在黑名单中,检查程序是否在黑名单
				if (i.second.count(exe) > 0)
					return false;
			}
		}

		// 遍历文件白名单,检查是否有文件受白名单保护
		for (auto &i : file_white_list())
		{
			if (i.first == real_absolute_path)
			{
				// 文件在白名单中,检查程序是否不在白名单
				if (i.second.count(exe) == 0)
					return false;
			}
		}
		
		// 文件访问权限通过,返回true
		return true;
	}
	catch (...)
	{
		// 捕获异常,如果出现异常,也返回true
		return true;
	}
}
  • 这个函数接受两个C风格字符串(exepath)以及一个字符串引用参数(return_full_path)。
  • 函数首先检查传入的exepath是否为NULL,如果是NULL,则直接返回false,表示不允许访问。
  • 然后,它获取目标文件的绝对路径,将其保存到real_absolute_path变量中,并将其赋值给return_full_path,以便后续使用。
  • 函数接着遍历文件黑名单(file_black_list())和文件白名单(file_white_list()),分别检查目标文件是否在黑名单或白名单中,以及程序是否在对应的名单中。
  • 如果文件在黑名单中,并且程序也在黑名单中,或者文件在白名单中,但程序不在白名单中,则返回false,表示不允许访问。
  • 最后,如果一切正常,或者在处理过程中出现异常,都会返回true,表示允许文件访问。

这段代码的目的是在文件访问时检查权限,以确保只有在允许名单中的程序可以访问允许名单中的文件,并且不在黑名单中。如果条件不满足,它返回false,表示拒绝访问。

//验证IPv4地址的访问权限
static bool
allow_ipv4(int sockfd, const struct sockaddr *address, socklen_t addrlen, string &return_ip) noexcept(true)
{
	try {
		char buf[INET_ADDRSTRLEN];
		// 强制将传入的地址指针转换为IPv4地址结构
		struct sockaddr_in *addr_in = (struct sockaddr_in *)address;

		// 检查地址结构长度,以及地址类型是否为IPv4
		if (addrlen < sizeof(sockaddr_in) ||
			addr_in->sin_family != AF_INET)
			return true;

		// 将IPv4地址转换为可读的字符串形式
		if (inet_ntop(AF_INET, &addr_in->sin_addr, buf, sizeof(buf)) == NULL)
			return true;

		// 将IPv4地址字符串赋值给字符串变量ip
		const string ip = string(buf);
		return_ip = ip;

		// 获取当前进程的可执行文件路径
		const string exe = get_real_exe_by_pid(getpid());

		// 遍历IP地址黑名单,检查是否有IP在黑名单中
		for (auto &i : ip_black_list())
		{
			if (i.first == ip)
			{
				// 如果IP在黑名单中,检查程序是否在黑名单中
				if (i.second.count(exe) > 0)
					return false;
			}
		}

		// 遍历IP地址白名单,检查是否有IP在白名单中
		for (auto &i : ip_white_list())
		{
			if (i.first == ip)
			{
				// 如果IP在白名单中,检查程序是否不在白名单中
				if (i.second.count(exe) == 0)
					return false;
			}
		}

		// IP地址访问权限通过,返回true
		return true;
	}
	catch (...)
	{
		// 捕获异常,如果出现异常,也返回true
		return true;
	}
}
// 检查地址结构长度,以及地址类型是否为IPv4
if (addrlen < sizeof(sockaddr_in) || addr_in->sin_family != AF_INET)
	return true;

这段代码的目的是在处理套接字地址之前,确保传入的地址是有效的 IPv4 地址,并且包含足够的信息来处理,以避免内存越界错误或无效的操作。如果传入的地址不满足这些条件,函数将立即返回 true,表示允许访问(不进行限制)。这有助于确保代码的稳定性和安全性。

typedef int (*open_func_t)(const char *, int, ...); // 定义一个函数指针类型,用于指向与标准 open 函数具有相同参数和返回类型的函数
int
open(const char *path, int flags, ...) // 重载标准 open 函数,用于拦截文件操作
{
	static open_func_t old_open = NULL; // 静态函数指针,用于存储原始的 open 函数地址
	if (old_open == NULL) // 如果第一次调用这个函数
		old_open = (open_func_t)dlsym(RTLD_NEXT, "open"); // 使用 dlsym 获取标准 open 函数的地址

	mode_t mode = 0; // 定义文件操作的模式

	if (flags & O_CREAT) // 如果传入的 flags 参数包含 O_CREAT 标志(表示在文件不存在时创建文件)
	{
		va_list args; // 定义可变参数列表
		va_start(args, flags); // 初始化可变参数列表
		mode = va_arg(args, mode_t); // 获取可变参数列表中的 mode_t 参数值
		va_end(args); // 清理可变参数列表
	}

	std::string full_path; // 用于存储文件的完整路径

	if (allow_open(get_real_exe_by_pid(getpid()).c_str(), path, full_path)) // 调用 allow_open 函数检查是否允许打开文件
	{
		log(RESULT::ALLOW, TYPE::FILE, full_path, get_real_exe_by_pid(getpid()), getpid()); // 记录允许的日志
		return old_open(path, flags, mode); // 调用原始的 open 函数
	}
	else
	{
		log(RESULT::DENY, TYPE::FILE, full_path, get_real_exe_by_pid(getpid()), getpid()); // 记录拒绝的日志
		errno = EPERM; // 设置 errno 为 EPERM(权限错误)
		return -1; // 返回 -1,表示打开文件失败
	}
}

这段代码是一个重载的open函数,它用于拦截文件操作,并在操作之前进行访问控制。以下是对这段代码的解释:

  • typedef int (*open_func_t)(const char *, int, ...);:这行代码定义了一个函数指针类型open_func_t,该函数指针可以指向与标准open函数具有相同参数和返回类型的函数。

  • static open_func_t old_open = NULL;:这行代码定义了一个静态函数指针old_open,并将其初始化为NULL。这个指针将用于保存原始的open函数的地址。

  • if (old_open == NULL):这是一个条件语句,用于检查old_open是否为空,如果为空,表示第一次调用这个函数。

  • old_open = (open_func_t)dlsym(RTLD_NEXT, "open");:在第一次调用时,这行代码使用dlsym函数获取标准open函数的地址,并将其存储在old_open函数指针中。这是为了能够在拦截函数中调用原始的open函数。

  • mode_t mode = 0;:这行代码定义了一个mode_t类型的变量mode,并将其初始化为0。

  • if (flags & O_CREAT):这行代码检查传入的flags参数是否包含O_CREAT标志。O_CREAT标志表示在文件不存在时创建文件。如果flags包含O_CREAT,则进入条件块,否则跳过。

  • va_list args; va_start(args, flags); mode = va_arg(args, mode_t); va_end(args);:在包含O_CREAT标志的情况下,这部分代码使用可变参数列表来提取额外的参数。特别是,它使用va_list来存储可变参数,通过va_start来初始化列表,然后使用va_arg来获取参数的值(在这种情况下,获取了mode的值),最后通过va_end来清理列表。

  • std::string full_path;:这行代码定义了一个std::string类型的变量full_path,用于存储文件的完整路径。

  • if (allow_open(get_real_exe_by_pid(getpid()).c_str(), path, full_path)):这行代码调用allow_open函数来检查是否允许打开文件。它传递了三个参数:当前进程的可执行文件路径、传入的文件路径path,以及用于存储文件的完整路径的full_path。如果allow_open函数返回true,表示允许打开文件,那么它记录了一个允许的日志,并通过old_open调用原始的open函数。

  • else:如果allow_open函数返回false,表示拒绝打开文件,这行代码记录了一个拒绝的日志,并设置了errnoEPERM(表示权限错误),然后返回-1,表示打开文件失败。

总之,这段代码的目的是拦截标准open函数的调用,并在打开文件之前进行访问控制。如果满足控制条件,它将允许打开文件,并调用原始的open函数。如果不满足条件,它将拒绝打开文件,并返回一个错误。这有助于实施文件访问控制策略。

typedef int (*openat_func_t)(int fd, const char *, int, ...);
int openat(int fd, const char *path, int flags, ...)
{
    // 定义一个指向函数指针的变量 old_openat,并初始化为 NULL
    static openat_func_t old_openat = NULL;

    // 第一次调用时,通过 dlsym 函数获取真正的 openat 函数的地址
    if (old_openat == NULL)
        old_openat = (openat_func_t)dlsym(RTLD_NEXT, "openat");

    // 定义文件打开权限 mode 为 0
    mode_t mode = 0;

    // 如果 flags 包含 O_CREAT 标志,获取变长参数列表中的文件权限 mode
    if (flags & O_CREAT)
    {
        va_list args;
        va_start(args, flags);
        mode = va_arg(args, mode_t);
        va_end(args);
    }

    std::string full_path; // 用于存储实际文件的全路径

    // 调用 allow_open 函数来检查文件的访问权限
    if (allow_open(get_real_exe_by_pid(getpid()).c_str(), path, full_path))
    {
        // 如果允许打开文件,记录相应的日志并调用真正的 openat 函数
        log(RESULT::ALLOW, TYPE::FILE, full_path, get_real_exe_by_pid(getpid()), getpid());
        return old_openat(fd, path, flags, mode);
    }
    else
    {
        // 如果不允许打开文件,记录相应的日志,设置错误号 errno 为 EPERM,并返回 -1 表示打开失败
        log(RESULT::DENY, TYPE::FILE, full_path, get_real_exe_by_pid(getpid()), getpid());
        errno = EPERM;
        return -1;
    }
}

tool.cpp

string
get_real_path (const char *path) // 函数名及参数说明
{
	char buf[PATH_MAX + 1]; // 声明一个字符数组用于存储路径
	if (realpath (path, buf) == NULL) // 调用realpath函数,将传入的路径规范化为绝对路径
		return path; // 如果realpath调用失败,返回原始路径
	return buf; // 如果realpath调用成功,返回规范化后的绝对路径
}
string get_remote_ip_by_fd(int sockfd) noexcept(false)
{
    struct sockaddr_storage addr; // 声明一个套接字地址结构体,用于存储远程主机的地址信息
    socklen_t addrlen = sizeof(addr); // 声明并初始化地址结构体的长度变量

    // 获取远程主机的地址信息并存储到 addr 结构体中,如果失败则抛出异常
    if (getpeername(sockfd, (sockaddr *)&addr, &addrlen) == -1)
        throw SocketException();

    sockaddr_in *tcp_addr = (sockaddr_in *)&addr; // 将 addr 转换为 IPv4 地址结构体
    char ip[INET_ADDRSTRLEN]; // 用于存储 IP 地址的字符数组
    // 将二进制形式的 IPv4 地址转换为文本形式的 IP 地址,并存储到 ip 数组中
    if (inet_ntop(AF_INET, &tcp_addr->sin_addr, ip, sizeof(ip)) == NULL)
        throw SocketException();

    return string(ip); // 将 IP 地址转换为 C++ 字符串并返回
}
string get_real_exe_by_pid(pid_t pid) // 函数签名,获取指定进程的可执行文件路径
{
    string buf = format("/proc/%d/exe", pid); // 构造进程的符号链接路径,如 "/proc/1234/exe"
    char exe[PATH_MAX]; // 创建一个字符数组用于存储符号链接指向的可执行文件路径
    ssize_t nread = readlink(buf.c_str(), exe, sizeof(exe) - 1); // 通过 readlink 函数获取符号链接指向的路径
    if (nread == -1) // 如果读取失败(返回值为-1),说明可能进程不存在或者没有符号链接
        return string(); // 返回一个空字符串,表示获取可执行文件路径失败
    exe[nread] = '\0'; // 在字符数组的结尾添加 null 终止符,以确保它是一个以 null 结尾的 C 字符串
    return get_real_path(exe); // 调用函数 get_real_path,以获取真实的路径并返回
}
string get_absolute_path(const char *path) // 函数签名,用于获取绝对路径
{
    if (path == NULL || path[0] == '/') // 如果输入路径为空或已经是绝对路径,直接返回原路径
        return path;
    char buf[PATH_MAX + 1]; // 创建一个字符数组用于存储当前工作目录路径
    if (getcwd(buf, sizeof(buf) - 1) == NULL) // 通过 getcwd 函数获取当前工作目录路径
        return string(); // 如果获取失败,返回一个空字符串,表示无法获得绝对路径
    return string(buf) + "/" + path; // 构造并返回合并后的绝对路径,将当前工作目录和输入路径拼接
}
vector<pid_t> get_all_pids() // 函数签名,用于获取系统中所有的进程ID
{
    vector<pid_t> pids; // 创建一个存储进程ID的向量
    DIR *p_dir = opendir("/proc"); // 打开位于 /proc 目录下的目录流,该目录通常包含进程信息
    if (p_dir == NULL) // 如果目录流打开失败
        return pids; // 返回一个空向量,表示无法获取进程ID
    for (;;)
    {
        dirent *p_file = readdir(p_dir); // 读取目录中的下一个条目
        if (p_file == NULL) // 如果没有更多的条目可读
            break; // 退出循环
        pid_t pid; // 创建一个变量用于存储进程ID
        if (p_file->d_type == DT_DIR && (pid = atoi(p_file->d_name)) != 0) // 如果是目录且目录名能够转换为有效的进程ID
            pids.push_back(pid); // 将该进程ID添加到向量中
    }
    closedir(p_dir); // 关闭目录流,释放资源
    return pids; // 返回包含所有有效进程ID的向量
}

ptrace_tool.cpp

//从目标进程的内存中读取64位整数数据('WORD'类型)
[[deprecated]]static void
get_tracee_words (pid_t pid, WORD *src, WORD *dest, size_t len) noexcept (false)
{
	for (size_t i = 0; i < len; ++i) {
		dest[i] = ptrace (PTRACE_PEEKDATA, pid, src + i, NULL);
		if (dest[i] == -1)
			throw PtraceException ();
	}
}
  • 它接受参数 pid,要读取的数据的地址 src,以及存储读取数据的数组 dest 以及要读取的数据的长度 len
  • 使用 ptrace 调用的 PTRACE_PEEKDATA 选项从目标进程的内存中读取数据,并将数据存储在 dest 数组中。
  • 如果读取失败,会抛出 PtraceException 异常。

PTRACE_POKEDATA 是 Linux 操作系统提供的 ptrace 系统调用的一个选项之一,它用于将数据写入远程进程的内存空间。这个选项通常用于修改目标进程的内存中的数据,允许一个进程追踪和修改另一个进程的执行状态和内存。

具体解释如下:

  1. ptrace 系统调用: ptrace 是一个用于进程追踪的系统调用,允许一个进程(通常是父进程)监视和控制另一个进程。通过 ptrace,一个进程可以读取和写入目标进程的寄存器、内存,以及控制目标进程的执行。

  2. PTRACE_POKEDATA: PTRACE_POKEDATAptrace 的一个选项(请求),它表示要将数据写入目标进程的内存。这个选项通常与 ptrace 函数一起使用,用于修改目标进程的内存。

  3. 用途: PTRACE_POKEDATA 主要用于在调试或进程注入等场景中,以编程方式修改目标进程的内存数据。例如,可以使用它来修改目标进程的变量值,注入代码,或执行其他需要改变内存数据的操作。

  4. 参数: 调用 ptrace 时,需要提供目标进程的进程 ID,要写入的目标地址,以及要写入的数据。

  5. 注意事项: 使用 PTRACE_POKEDATA 需要特权,通常只能由具有足够权限的进程来执行,例如,需要具有 root 或 debug 能力的权限。

总之,PTRACE_POKEDATAptrace 的一个选项,用于在目标进程的内存中写入数据,通常用于调试、注入或修改目标进程的内存。

//向目标进程的内存中写入64位整数数据
[[deprecated]]static void
set_tracee_words (pid_t pid,  WORD *src,  WORD *dest, size_t len)
{
	for (size_t i = 0; i < len; ++i) {
		if (ptrace (PTRACE_POKEDATA, pid, &dest[i], src[i]) == -1)
			throw PtraceException ();
	}
}
  • 它接受参数 pid,要写入的数据 src,以及要写入的目标地址 dest 以及数据的长度 len
  • 使用 ptrace 调用的 PTRACE_POKEDATA 选项将数据写入目标进程的内存。
  • 如果写入失败,会抛出 PtraceException 异常。
//计算目标进程内存中以"remote_str"开头的字符串的长度
int get_tracee_strlen (pid_t pid, char *remote_str)
{
	user_regs_struct regs;
	int len = 0;
	for (int i = 0; ; ++i) {
		WORD word = ptrace (PTRACE_PEEKDATA, pid, remote_str + i * sizeof (WORD), NULL);
		if (word == -1)
			throw PtraceException ();
		char *end = (char *) memchr (&word, '\0', sizeof (word));
		if (end == NULL)
			len += sizeof (WORD) / sizeof (char);
		else {
			len += end - (char *) &word;
			break;
		}
	}
	return len;
}
  • 它接受参数 pidremote_str,表示目标进程的进程 ID 和指向目标进程内存的指针。
  • 通过循环逐个读取字符并查找字符串的终止符 \0 来计算字符串的长度。
  • 如果读取失败,会抛出 PtraceException 异常。
//从目标进程的内存中复制字符串并返回复制后的字符串
char * get_tracee_strdup (pid_t pid, char *remote_str)
{
	const int len = get_tracee_strlen (pid, remote_str);
	char buf[len + 1];
	get_tracee_bytes (pid, remote_str, buf, len + 1);
	return strdup (buf);
}
  • 此函数用于从目标进程的内存中复制字符串并返回复制后的字符串。
  • 它首先调用 get_tracee_strlen 计算字符串的长度,然后调用 get_tracee_bytes 读取字符串的字节数据。
  • 最后,它使用 strdup 函数分配新的字符串并返回。
// 从目标进程的内存中读取字节数据
void get_tracee_bytes (pid_t pid, void *remote_src, void *local_dest, size_t len)
{
	const int word_len = len / sizeof (WORD);
	for (size_t i = 0; i < word_len ; ++i) {
		WORD ret = ptrace (PTRACE_PEEKDATA, pid, (WORD *) remote_src + i, NULL);
		if (ret == -1)
			throw PtraceException ();
		( (WORD *) local_dest) [i] =  ret;
	}
	if (word_len > 1) {
		void *dest_last_word = (WORD *) ( (char *) local_dest + len) - 1;
		void *src_last_word = (WORD *) ( (char *) remote_src + len) - 1;
		if (len % sizeof (WORD) != 0) {
			WORD ret = ptrace (PTRACE_PEEKDATA, pid, src_last_word, NULL);
			if (ret == -1)
				throw PtraceException ();
			* (WORD *) dest_last_word = ret;
		}
	}
}
  • 它接受参数 pid,指向目标进程内存的指针 remote_src,以及存储读取数据的本地缓冲区 local_dest 以及要读取的数据的长度 len
  • 该函数首先将数据以 64 位整数(WORD)的形式逐个字的方式读取,然后将它们存储在 local_dest 中。
  • 最后,如果 len 不是 WORD 大小的整数倍,它会单独读取最后一个字。
// 从目标进程的内存中读取字节数据
void set_tracee_bytes (pid_t pid, void *remote_src, void *local_dest, size_t len)
{
	const int word_len = len / sizeof (WORD);
	for (size_t i = 0; i < word_len ; ++i) {
		if (ptrace (PTRACE_POKEDATA, pid, (WORD *) local_dest + i, * ( (WORD *) remote_src + i)) == -1)
			throw PtraceException ();
	}
	if (word_len > 1) {
		void *dest_last_word = (WORD *) ( (char *) local_dest + len) - 1;
		void *src_last_word = (WORD *) ( (char *) remote_src + len) - 1;
		if (len % sizeof (WORD) != 0) {
			if (ptrace (PTRACE_POKEDATA, pid,  dest_last_word, * (WORD *) src_last_word) == -1)
				throw PtraceException ();
		}
	}
}

它的参数和操作方式与 get_tracee_bytes 函数类似,但是它用 ptrace 调用的 PTRACE_POKEDATA 选项将数据写入目标进程的内存。

总结:

inet_ntop

inet_ntop 函数用于将二进制形式的网络地址转换为人类可读的IPv4或IPv6地址表示。

其原型如下:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af:地址族(Address Family),可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • src:指向要转换的二进制网络地址的指针。
  • dst:指向用于存储结果的缓冲区。
  • sizedst 缓冲区的大小。

inet_ntop 函数的原理是将二进制网络地址转换为可读的点分十进制(IPv4)或冒号十六进制(IPv6)表示。函数根据给定的地址族 af,采用不同的格式化方式来完成这一转换。

对于 IPv4 地址(afAF_INET),inet_ntop 函数将 32 位的二进制地址拆分成四个 8 位的部分,然后将它们以点分十进制的形式表示。例如,二进制地址 0x7F000001 被转换为字符串 “127.0.0.1”。

对于 IPv6 地址(afAF_INET6),inet_ntop 函数将 128 位的二进制地址按照冒号十六进制表示。例如,IPv6 地址 2001:0db8:85a3:0000:0000:8a2e:0370:7334 保持不变。

dlsym

dlsym 函数是动态链接库操作的一部分,用于在共享库中查找符号(函数或变量)。其原型如下:

void *dlsym(void *handle, const char *symbol);
  • handle:表示已经打开的动态链接库的句柄,通常是由 dlopen 函数返回的。
  • symbol:是你想查找的符号(函数或变量)的名称。

dlsym 函数的工作原理涉及以下几个步骤:

  1. dlsym 函数通过 handle 参数确定要在哪个已加载的共享库中查找符号。
  2. 它会在给定的共享库中查找具有名称 symbol 的符号。
  3. 如果找到符号,dlsym 返回指向该符号的指针(函数指针或变量指针);否则,返回 NULL

这个函数的主要用途是在运行时从共享库中获取函数或变量的地址,以便在程序中调用或使用它们。这是一种动态加载共享库中的函数的方法,可以在程序运行时决定使用哪个共享库,并且可以根据需要加载或卸载这些库。

va_start

va_start 函数是 C/C++ 标准库中的一个宏,用于在函数内部访问可变参数列表(variable argument list)。它的原型通常定义在 <cstdarg><stdarg.h> 头文件中,但是 va_start 宏的具体实现会根据编译器和平台而有所不同。

va_start 宏的一般原型如下:

void va_start(va_list ap, last_arg);
  • va_list ap 是一个指向可变参数列表的指针,它将在函数内部用于迭代访问参数。
  • last_arg 是可变参数列表中的最后一个固定参数,用于确定可变参数列表的起始位置。

va_start 宏的原理是基于编译器和体系结构的底层机制,通常使用汇编代码来实现。它的主要任务是将 va_list 指针初始化为指向参数列表中的第一个可变参数。

具体实现方法取决于编译器和平台,但通常涉及以下步骤:

  1. 确定固定参数的位置。编译器需要知道在参数列表中哪里是可变参数的开始位置。这通常由 last_arg 参数指定。

  2. 计算可变参数列表的地址。编译器会使用一些规则来计算可变参数列表的地址。这通常涉及堆栈指针(栈帧指针)的调整和偏移计算。

  3. 初始化 va_list 指针。va_start 宏会将 va_list 指针初始化为可变参数列表的起始位置,以便函数内部可以使用 va_arg 宏来访问参数。

总之,va_start 宏的原理是在函数内部为可变参数列表创建一个指针,使得程序可以依次访问参数。不同编译器和平台的实现可能会有所不同,但通常都是基于底层的堆栈和内存管理机制。

va_arg

va_arg 函数是 C/C++ 标准库中用于访问可变参数列表(variable argument list)的宏。它的原型通常定义在 <cstdarg><stdarg.h> 头文件中,但 va_arg 宏的具体实现会根据编译器和平台而有所不同。

一般情况下,va_arg 宏的原型如下:

type va_arg(va_list ap, type);
  • va_list ap 是一个指向可变参数列表的指针,它在 va_start 函数之后初始化,用于迭代访问参数。
  • type 是要获取的参数的类型。

va_arg 宏的原理是基于编译器和体系结构的底层机制,通常使用汇编代码来实现。它的主要任务是从可变参数列表中按指定类型提取参数的值。

具体实现方法取决于编译器和平台,但通常涉及以下步骤:

  1. 计算参数的大小。编译器需要知道参数的大小,以便正确地从堆栈中读取数据。这取决于参数的类型。

  2. 更新 va_list 指针。va_arg 宏会将 va_list 指针移动到下一个参数的位置,以准备下一次调用 va_arg

  3. 从内存中读取参数值。va_arg 宏通过 va_list 指针获取参数的值,然后将指针移动到下一个参数的位置。

总之,va_arg 宏的原理是在可变参数列表中按照指定的类型提取参数值。不同编译器和平台的实现可能会有所不同,但通常都是基于底层的堆栈和内存管理机制。 va_arg 宏为处理可变参数提供了一种通用的方法,使得在不知道参数个数和类型的情况下能够访问参数。

va_end

va_end 函数是 C/C++ 标准库中用于终止可变参数列表(variable argument list)操作的宏。它的原型通常定义在 <cstdarg><stdarg.h> 头文件中,但 va_end 宏的具体实现会根据编译器和平台而有所不同。

一般情况下,va_end 宏的原型如下:

void va_end(va_list ap);
  • va_list ap 是一个指向可变参数列表的指针,它在 va_start 函数之后初始化。

va_end 宏的原理是用于清理 va_list 指针,以便资源得到正确释放。具体实现方法取决于编译器和平台,但通常会在 va_end 中执行以下操作:

  1. va_list 指针设置为一个未定义或无效的状态。这意味着该指针不再指向可变参数列表中的任何参数。

  2. 释放或清理 va_list 指针可能使用的任何资源。这通常涉及一些与堆栈或寄存器状态相关的操作,以确保不会出现内存泄漏或资源泄漏。

总之,va_end 宏的原理是用于清理和终止可变参数列表的操作,以确保不会出现资源泄漏或其他问题。不同编译器和平台的实现方式可能有所不同,但它们的共同目标是安全地结束可变参数列表的操作。

sendto

sendto 函数是用于将数据发送到指定的目标地址的系统调用,通常用于网络编程。它的原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd 是套接字文件描述符,用于标识要发送数据的套接字。
  • buf 是包含要发送的数据的缓冲区。
  • len 是要发送的数据的字节数。
  • flags 是发送选项,通常可以设置为 0。
  • dest_addr 是目标地址的指针,通常是 struct sockaddr 结构的指针,包含目标地址信息。
  • addrlen 是目标地址结构的长度。

sendto 函数的原理是将 buf 中的数据发送到指定的目标地址,然后返回发送的字节数。数据包将通过 sockfd 套接字发送,而 dest_addraddrlen 用于指定目标地址。

原理包括以下步骤:

  1. 根据 sockfd 找到关联的套接字,该套接字用于数据发送。
  2. buf 缓冲区中的数据封装成数据包,同时添加目标地址信息(由 dest_addr 指定)。
  3. 数据包被发送到目标地址,这通常涉及到网络协议栈的操作。
  4. sendto 函数返回发送的字节数或出现的错误。

sendto 可以用于 UDP 和基于 IP 的协议(例如 ICMP),以便将数据发送到指定的目标地址。通过 dest_addr 参数,您可以指定数据包要发送到的目标主机和端口。此函数常用于网络编程中,用于实现数据的发送和接收。

realpath

realpath 函数用于获取一个路径的绝对路径,将相对路径转换为绝对路径。其原型如下:

char *realpath(const char *path, char *resolved_path);
  • path 是要获取绝对路径的输入路径。
  • resolved_path 是一个缓冲区,用于存储解析后的绝对路径。它可以为 NULL,如果为 NULLrealpath 函数会自动为您分配内存。

realpath 函数的原理是将输入的相对路径 path 转换为绝对路径并存储在 resolved_path 缓冲区中。如果 resolved_path 参数为 NULL,则会自动分配内存并存储绝对路径。

原理包括以下步骤:

  1. realpath 检查输入路径 path 是否为相对路径或绝对路径。如果 path 为绝对路径(以 / 开头),则它是其自身的绝对路径,无需进一步处理。
  2. 如果 path 为相对路径,realpath 将获取当前工作目录,并将其与 path 连接,以得到绝对路径。
  3. realpath 对路径中的符号链接进行解析,以获得路径的最终绝对路径。这包括将路径中的 ... 等符号链接替换为实际目录。
  4. 最终的绝对路径存储在 resolved_path 缓冲区中,或者如果 resolved_pathNULL,则由 realpath 函数自动分配内存来存储绝对路径。
  5. realpath 返回指向 resolved_path 缓冲区的指针,其中包含了输入路径的绝对路径。这个缓冲区可以被后续代码使用。

realpath 函数通常用于获取文件的绝对路径,以确保以绝对路径方式引用文件。这在文件系统操作和路径处理中非常有用,可以避免相对路径引发的问题。

getpeername

getpeername 函数用于获取与已连接套接字关联的远程端的地址信息。其原型如下:

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd 是一个已连接套接字的文件描述符,用于表示与远程主机建立的连接。
  • addr 是一个指向 struct sockaddr 结构的指针,用于存储远程端的地址信息。
  • addrlen 是一个指向 socklen_t 类型的指针,用于存储 addr 缓冲区的长度。

getpeername 函数的原理是在套接字 sockfd 上执行操作,以获取远程端的地址信息并存储在 addr 缓冲区中。

原理包括以下步骤:

  1. getpeername 函数接收 sockfd,这是一个已连接套接字,表示与远程主机的连接。
  2. 它将 addr 缓冲区用于存储远程端的地址信息。
  3. 通过 addrlen 参数传递缓冲区的大小。
  4. getpeername 函数将远程端的地址信息填充到 addr 缓冲区中。
  5. 如果成功,函数返回0,否则返回-1,并在错误情况下设置 errno,以指示错误的类型。

getpeername 函数通常用于网络编程,以获取远程客户端的地址信息,以便了解与服务器建立连接的客户端。这是在服务器端套接字编程中的常见用例,用于识别连接到服务器的客户端的地址信息。

inet_ntop

inet_ntop 函数用于将网络字节序的 IP 地址转换为人类可读的 IP 地址字符串。其原型如下:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af 是地址族参数,通常是 AF_INET 表示 IPv4 地址族或 AF_INET6 表示 IPv6 地址族。
  • src 是指向包含二进制 IP 地址的内存地址。
  • dst 是用于存储 IP 地址字符串的缓冲区。
  • sizedst 缓冲区的大小。

原理:
inet_ntop 函数的实现依赖于不同的操作系统,但其核心思想相似。它将二进制 IP 地址转换为字符串的过程通常如下:

  1. 首先,函数根据地址族 (af) 来判断是 IPv4 还是 IPv6 地址。

  2. 然后,它根据地址族决定如何将二进制地址解析成字符串。对于 IPv4 地址,将四个 8 位整数以点分隔的形式拼接成字符串;对于 IPv6 地址,将 16 位整数以冒号分隔的形式拼接成字符串。

  3. 函数将转换后的字符串复制到 dst 缓冲区中,同时确保不会超过 size 字节的长度。

  4. 如果转换成功,函数返回指向 dst 缓冲区的指针,否则返回 NULL,并在 errno 中设置相应的错误码,如 EINVAL(无效的地址族)或 ENOSPC(缓冲区不足)。

需要注意的是,inet_ntop 是将二进制地址转换为可读字符串的逆过程,与之相反的函数是 inet_pton,它将字符串转换为二进制地址。这两个函数在网络编程中用于进行 IP 地址的解析和构建。

readlink

readlink 函数用于读取符号链接的目标路径。它的原型如下:

ssize_t readlink(const char *path, char *buf, size_t bufsiz);
  • path 是要读取的符号链接文件的路径。
  • buf 是一个字符数组,用于存储目标路径。
  • bufsizbuf 的大小,用于指定目标路径的最大长度。

原理:
readlink 函数的目的是获取符号链接文件所指向的实际目标路径。其工作原理如下:

  1. 当调用 readlink 函数时,它会打开 path 所指定的符号链接文件,并尝试读取链接文件的内容。

  2. 如果成功,它将读取的内容存储在 buf 中,并返回所复制的字符数,不包括 null 终止字符。如果 bufsiz 大于实际目标路径的长度,buf 中的数据将以 null 终止。

  3. 如果读取的内容超过了 buf 的容量(bufsiz 不足以容纳目标路径),readlink 函数会截断目标路径并返回截断后的字符数。此时,buf 中的数据仍以 null 终止。

  4. 如果读取符号链接文件失败,readlink 函数返回 -1,并在 errno 中设置相应的错误码,如 EACCES(权限不足)或 ENOENT(文件不存在)。

需要注意的是,readlink 仅适用于符号链接文件。对于硬链接或普通文件,它不起作用。符号链接是一种特殊类型的文件,其中包含对其他文件或目录的路径引用,因此读取它的内容通常是获取所引用文件的路径。

opendir

opendir 函数用于打开指定目录并返回一个指向目录流(DIR 结构体的指针)的句柄,以便后续对目录中的文件和子目录进行遍历。其原型如下:

DIR *opendir(const char *dirname);
  • dirname 是一个字符串,表示要打开的目录的路径名。

原理:

  1. 当调用 opendir 函数时,它会尝试打开指定路径的目录。

  2. 如果成功,opendir 返回一个指向 DIR 结构体的指针,该结构体用于表示打开的目录流。这个结构体包含有关目录的信息,如目录的文件描述符和目录项列表。

  3. 目录流句柄可以用于后续的目录遍历操作,例如使用 readdir 函数读取目录中的文件和子目录项。

  4. 如果打开目录失败,opendir 返回 NULL,表示出现了错误。在这种情况下,通常可以使用 errno 来获取出错的详细信息,例如 ENOENT 表示指定的目录不存在,EACCES 表示无权限访问目录等。

opendirreaddir 函数通常用于目录遍历操作,允许程序在目录中查找文件和子目录,并对它们进行处理。这对于编写文件管理和操作系统相关的程序非常有用。

readdir

readdir 函数用于从已打开的目录流中读取下一个目录项。其原型如下:

struct dirent *readdir(DIR *dirp);
  • dirp 是一个指向已打开目录流(DIR 结构体的指针)的句柄。

  • struct dirent 是一个结构体,表示目录中的一个项,包括文件名、文件类型和其他属性。

原理:

  1. readdir 函数从指定的目录流 dirp 中读取下一个目录项,并返回一个指向 struct dirent 结构体的指针,其中包含有关该目录项的信息。

  2. 如果目录流已经到达末尾(即没有更多的目录项可读取),或者出现错误,readdir 返回 NULL,表示读取结束或出现错误。此时,通常可以使用 errno 来获取出错的详细信息。

  3. 通过多次调用 readdir 函数,可以依次读取目录中的所有文件和子目录项,直到读取到末尾为止。

  4. struct dirent 结构体中包含了目录项的信息,如文件名、文件类型和其他属性。您可以使用这些信息来进一步处理目录中的文件和子目录。

readdir 函数通常与 opendir 函数一起使用,用于在目录中遍历文件和子目录。这对于需要执行文件管理或目录操作的应用程序非常有用。

atoi

atoi 函数用于将字符串转换为整数(int)。其原型如下:

int atoi(const char *str);
  • str 是一个指向包含表示整数的字符串的指针。

原理:

  1. atoi 函数从字符串 str 的起始位置开始扫描,并跳过前导空白字符(如空格、制表符等)。

  2. 一旦遇到非空白字符,atoi 将开始解析整数。它会继续读取字符,直到遇到非数字字符或字符串的末尾。

  3. 解析期间,atoi 将读取的字符转换为整数,并将其积累到一个整数值中。该整数值的初始值为零。

  4. 如果字符串中包含无效字符或字符串为空,atoi 将停止解析,并返回当前积累的整数值。

  5. 如果整数超出了 int 数据类型的范围,结果是未定义的。

  6. 返回值为解析后的整数值。

atoi 主要用于将字符串形式的数字转换为整数,常用于文本处理和输入转换,但不提供错误检测机制。如果需要更强大的字符串到整数的转换和错误处理,可以使用 strtol 函数或其他更安全的替代方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值