9.2 Windows驱动开发:内核解析PE结构导出表

在笔者的上一篇文章《内核特征码扫描PE代码段》LyShark带大家通过封装好的LySharkToolsUtilKernelBase函数实现了动态获取内核模块基址,并通过ntimage.h头文件中提供的系列函数解析了指定内核模块的PE节表参数,本章将继续延申这个话题,实现对PE文件导出表的解析任务,导出表无法动态获取,解析导出表则必须读入内核模块到内存才可继续解析,所以我们需要分两步走,首先读入内核磁盘文件到内存,然后再通过ntimage.h中的系列函数解析即可。

PE结构(Portable Executable Structure)是Windows操作系统用于执行可执行文件和动态链接库(DLL)的标准格式。导出表(Export Table)是PE结构中的一个部分,它记录了一个DLL中所有可供外部调用的函数和变量。

导出表通常位于PE结构的数据目录中。它包含两个重要的表格:导出名称表格和导出地址表格。导出名称表格列出了DLL中所有导出函数和变量的名称,而导出地址表格列出了这些函数和变量的内存地址。

当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。导出函数在DLL文件中,导出信息被保存在导出表,导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器能够通过这些信息来完成动态链接的整个过程。

导出函数存储在PE文件的导出表里,导出表的位置存放在PE文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是导出表的RVA值,导出表同样可以使用函数名或序号这两种方法导出函数。

导出表的起始位置有一个IMAGE_EXPORT_DIRECTORY结构,与导入表中有多个IMAGE_IMPORT_DESCRIPTOR结构不同,导出表只有一个IMAGE_EXPORT_DIRECTORY结构,该结构定义如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD   Characteristics;DWORD   TimeDateStamp;        // 文件的产生时刻WORD    MajorVersion;WORD    MinorVersion;DWORD   Name;                  // 指向文件名的RVADWORD   Base;                  // 导出函数的起始序号DWORD   NumberOfFunctions;     // 导出函数总数DWORD   NumberOfNames;         // 以名称导出函数的总数DWORD   AddressOfFunctions;    // 导出函数地址表的RVADWORD   AddressOfNames;        // 函数名称地址表的RVADWORD   AddressOfNameOrdinals; // 函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

其中,Name字段指向了该DLL的名称字符串,Base字段为该DLL的加载基地址,NumberOfFunctionsNumberOfNames分别表示导出函数和变量的数量,AddressOfFunctions、AddressOfNamesAddressOfNameOrdinals则是三个表格的地址。

总的来说,导出表是DLL中非常重要的一个部分,它提供了一种方便的方法,使其他程序可以调用DLL中的函数和变量。

上面的_IMAGE_EXPORT_DIRECTORY 结构如果总结成一张图,如下所示:

在上图中最左侧AddressOfNames结构成员指向了一个数组,数组里保存着一组RVA,每个RVA指向一个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的结构成员,该对应项存储的正是函数的唯一编号并与AddressOfFunctions结构成员相关联,形成了一个导出链式结构体。

获取导出函数地址时,先在AddressOfNames中找到对应的名字MyFunc1,该函数在AddressOfNames中是第1项,然后从AddressOfNameOrdinals中取出第1项的值这里是1,然后就可以通过导出函数的序号AddressOfFunctions[1]取出函数的入口RVA,然后通过RVA加上模块基址便是第一个导出函数的地址,向后每次相加导出函数偏移即可依次遍历出所有的导出函数地址。

其解析过程与应用层基本保持一致,如果不懂应用层如何解析也可以去看我以前写过的《PE格式:手写PE结构解析工具》里面具体详细的分析了解析流程。

首先使用InitializeObjectAttributes()打开文件,打开后可获取到该文件的句柄,InitializeObjectAttributes宏初始化一个OBJECT_ATTRIBUTES结构体, 当一个例程打开对象时由此结构体指定目标对象的属性,此函数的微软定义如下;

VOID InitializeObjectAttributes([out]          POBJECT_ATTRIBUTES   p,      // 权限[in]           PUNICODE_STRING      n,      // 文件名[in]           ULONG                a,      // 输出文件[in]           HANDLE               r,      // 权限[in, optional] PSECURITY_DESCRIPTOR s       // 0
);

当权限句柄被初始化后则即调用ZwOpenFile()打开一个文件使用权限FILE_SHARE_READ打开,打开文件函数微软定义如下;

NTSYSAPI NTSTATUS ZwOpenFile([out] PHANDLE            FileHandle,         // 返回打开文件的句柄[in]  ACCESS_MASK        DesiredAccess,      // 打开的权限,一般设为GENERIC_ALL。[in]  POBJECT_ATTRIBUTES ObjectAttributes,   // OBJECT_ATTRIBUTES结构[out] PIO_STATUS_BLOCK   IoStatusBlock,      // 指向一个结构体的指针。该结构体指明打开文件的状态。[in]  ULONG              ShareAccess,        // 共享的权限。可以是FILE_SHARE_READ 或者 FILE_SHARE_WRITE。[in]  ULONG              OpenOptions         // 打开选项,一般设为 FILE_SYNCHRONOUS_IO_NONALERT。
);

接着文件被打开后,我们还需要调用ZwCreateSection()该函数的作用是创建一个Section节对象,并以PE结构中的SectionALignment大小对齐映射文件,其微软定义如下;

NTSYSAPI NTSTATUS ZwCreateSection([out]          PHANDLE            SectionHandle,            // 指向 HANDLE 变量的指针,该变量接收 section 对象的句柄。[in]           ACCESS_MASK        DesiredAccess,            // 指定一个 ACCESS_MASK 值,该值确定对 对象的请求访问权限。[in, optional] POBJECT_ATTRIBUTES ObjectAttributes,         // 指向 OBJECT_ATTRIBUTES 结构的指针,该结构指定对象名称和其他属性。[in, optional] PLARGE_INTEGER     MaximumSize,              // 指定节的最大大小(以字节为单位)。[in]           ULONG              SectionPageProtection,    // 指定要在 节中的每个页面上放置的保护。 [in]           ULONG              AllocationAttributes,     // 指定确定节的分配属性的SEC_XXX 标志的位掩码。 [in, optional] HANDLE             FileHandle                // (可选)指定打开的文件对象的句柄。
);

最后读取导出表就要将一个磁盘中的文件映射到内存中,内存映射核心文件时ZwMapViewOfSection()该系列函数在应用层名叫MapViewOfSection()只是一个是内核层一个应用层,这两个函数参数传递基本一致,以ZwMapViewOfSection为例,其微软定义如下;

NTSYSAPI NTSTATUS ZwMapViewOfSection([in]                HANDLE          SectionHandle,          // 接收一个节对象[in]                HANDLE          ProcessHandle,          // 进程句柄,此处使用NtCurrentProcess()获取自身句柄[in, out]           PVOID           *BaseAddress,           // 指定填充地址[in]                ULONG_PTR       ZeroBits,               // 0[in]                SIZE_T          CommitSize,             // 每次提交大小 1024[in, out, optional] PLARGE_INTEGER  SectionOffset,          // 0[in, out]           PSIZE_T         ViewSize,               // 浏览大小[in]                SECTION_INHERIT InheritDisposition,     // ViewShare[in]                ULONG           AllocationType,         // 分配类型 MEM_TOP_DOWN[in]                ULONG           Win32Protect            // 权限 PAGE_READWRITE(读写)
);

将如上函数研究明白那么代码就变得很容易了,首先InitializeObjectAttributes设置文件权限与属性,然后调用ZwOpenFile打开文件,接着调用ZwCreateSection创建节对象,最后调用ZwMapViewOfSection将磁盘文件映射到内存,这段代码实现起来很简单,完整案例如下所示;

#include <ntifs.h>
#include <ntimage.h>
#include <ntstrsafe.h>// 内存映射文件
NTSTATUS KernelMapFile(UNICODE_STRING FileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress)
{NTSTATUS status = STATUS_SUCCESS;HANDLE hFile = NULL;HANDLE hSection = NULL;OBJECT_ATTRIBUTES objectAttr = { 0 };IO_STATUS_BLOCK iosb = { 0 };PVOID pBaseAddress = NULL;SIZE_T viewSize = 0;// 设置文件权限InitializeObjectAttributes(&objectAttr, &FileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);// 打开文件status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttr, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);if (!NT_SUCCESS(status)){return status;}// 创建节对象status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile);if (!NT_SUCCESS(status)){ZwClose(hFile);return status;}// 映射到内存status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE);if (!NT_SUCCESS(status)){ZwClose(hSection);ZwClose(hFile);return status;}// 返回数据*phFile = hFile;*phSection = hSection;*ppBaseAddress = pBaseAddress;return status;
}VOID UnDriver(PDRIVER_OBJECT driver)
{DbgPrint("驱动卸载 \n");
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");NTSTATUS status = STATUS_SUCCESS;HANDLE hFile = NULL;HANDLE hSection = NULL;PVOID pBaseAddress = NULL;UNICODE_STRING FileName = {0};// 初始化字符串RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");// 内存映射文件status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);if (NT_SUCCESS(status)){DbgPrint("读取内存地址 = %p \n", pBaseAddress);}Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

运行这段程序,即可读取到ntoskrnl.exe磁盘所在文件的内存映像基地址,效果如下所示;

如上代码读入了ntoskrnl.exe文件,接下来就是解析导出表,首先将pBaseAddress解析为PIMAGE_DOS_HEADER获取DOS头,并在DOS头中寻找PIMAGE_NT_HEADERS头,接着在NTHeader头中得到数据目录表,此处指向的就是导出表PIMAGE_EXPORT_DIRECTORY通过pExportTable->NumberOfNames可得到导出表的数量,通过(PUCHAR)pDosHeader + pExportTable->AddressOfNames得到导出表的地址,依次循环读取即可得到完整的导出表。

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");NTSTATUS status = STATUS_SUCCESS;HANDLE hFile = NULL;HANDLE hSection = NULL;PVOID pBaseAddress = NULL;UNICODE_STRING FileName = { 0 };LONG FunctionIndex = 0;// 初始化字符串RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntoskrnl.exe");// 内存映射文件status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);if (NT_SUCCESS(status)){DbgPrint("[LyShark] 读取内存地址 = %p \n", pBaseAddress);}// Dos 头PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;// NT 头PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);// 导出表PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);// 有名称的导出函数个数ULONG ulNumberOfNames = pExportTable->NumberOfNames;DbgPrint("[lyshark] 导出函数个数: %d \n\n", ulNumberOfNames);// 导出函数名称地址表PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);PCHAR lpName = NULL;// 开始遍历导出表(输出ulNumberOfNames导出函数)for (ULONG i = 0; i < ulNumberOfNames; i++){lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);// 获取导出函数地址USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);// 获取SSDT函数IndexFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4);DbgPrint("序号: [ %d ] | Hint: %d | 地址: %p | 函数名: %s \n", i, uHint, lpFuncAddr, lpName);}// 释放指针ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);ZwClose(hSection);ZwClose(hFile);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

代码运行后即可获取到当前ntoskrnl.exe程序中的所有导出函数,输出效果如下所示;

  • SSDT表通常会解析\\??\\C:\\Windows\\System32\\ntoskrnl.exe
  • SSSDT表通常会解析\\??\\C:\\Windows\\System32\\win32k.sys

根据上方的函数流程将其封装为GetAddressFromFunction()用户传入DllFileName指定的PE文件,以及需要读取的pszFunctionName函数名,即可输出该函数的导出地址。

// 寻找指定函数得到内存地址
ULONG64 GetAddressFromFunction(UNICODE_STRING DllFileName, PCHAR pszFunctionName)
{NTSTATUS status = STATUS_SUCCESS;HANDLE hFile = NULL;HANDLE hSection = NULL;PVOID pBaseAddress = NULL;// 内存映射文件status = KernelMapFile(DllFileName, &hFile, &hSection, &pBaseAddress);if (!NT_SUCCESS(status)){return 0;}PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);ULONG ulNumberOfNames = pExportTable->NumberOfNames;PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);PCHAR lpName = NULL;for (ULONG i = 0; i < ulNumberOfNames; i++){lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);if (_strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)) == 0){ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);ZwClose(hSection);ZwClose(hFile);return (ULONG64)lpFuncAddr;}}ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);ZwClose(hSection);ZwClose(hFile);return 0;
}VOID UnDriver(PDRIVER_OBJECT driver)
{DbgPrint("驱动卸载 \n");
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{DbgPrint("hello lyshark \n");UNICODE_STRING FileName = { 0 };ULONG64 FunctionAddress = 0;// 初始化字符串RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");// 取函数内存地址FunctionAddress = GetAddressFromFunction(FileName, "ZwQueryVirtualMemory");DbgPrint("ZwQueryVirtualMemory内存地址 = %p \n", FunctionAddress);Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

如上程序所示,当运行后即可获取到ntdll.dll模块内ZwQueryVirtualMemory的导出地址,输出效果如下所示;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/159011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何用CHAT写励志文章?

问CHAT&#xff1a;写一篇以《过了60岁要积极面对身体疾病的坎儿》为题目&#xff0c;写一篇300字励志文章 CHAT回复&#xff1a; 标题&#xff1a;《过了60岁要积极面对身体疾病的坎儿》 人生&#xff0c;有时会像一趟不期而遇的旅程&#xff0c;各自带着乐观或悲观、阳光或…

(论文阅读51-57)图像描述3 53

51.文献阅读笔记&#xff08;KNN&#xff09; 简介 题目 Exploring Nearest Neighbor Approaches for Image Captioning 作者 Jacob Devlin, Saurabh Gupta, Ross Girshick, Margaret Mitchell, C. Lawrence Zitnick, arXiv:1505.04467 原文链接 http://arxiv.org/pdf/1…

如何使用无代码系统搭建软件平台?有哪些开源无代码开发平台?

无代码是什么 无代码开发&#xff0c;也称为零代码&#xff08;Zero Code&#xff09;开发&#xff0c;是一种技术概念。无代码开发无需代码基础&#xff0c;适合业务人员、IT开发及其他各类人员使用。他们通过无代码开发平台快速构建应用&#xff0c;并适应各种需求变化&#…

JDBC基本操作

JDBC基本操作 create table user( id int primary key auto_increment, name varchar(50) ) ENGINE InnoDB DEFAULT CHARSET utf8; JDBC概念 JDBC是一个独立于特定数据库管理系统、通用的SQL数据库存取和操作的公共接口&#xff0c;定义了用来访问数据库的标准的Jav…

深入理解Java虚拟机-GC

深入理解Java虚拟机-GC 当需要排查各种内存溢出、内存泄漏时&#xff0c;当垃圾回收成为系统到达更高并发量的瓶颈时&#xff0c;我们必须对内存动态分配和内存回收技术这样的“自动化”技术采用必要的监控和调节。 Java堆和方法区&#xff1a;一个接口的多个实现类需要的内存…

element表格头部加入图标

首先看看效果 下面是代码 <el-table-column prop"integralBalance"><template slot"header" slot-scope"scope"><div style"display: flex;justify-content: center;align-items: center;">积分余额<i class&qu…

android APP使用指定网络上网的原理

【精选】Android app 指定网络发送数据包的实现与原理分析_bindprocesstonetwork-CSDN博客 补充&#xff1a; frameworks/base/core/java/android/net/ConnectivityManager.java 函数&#xff1a; bindProcessToNetwork 调用到了 NetworkUtils.bindProcessToNetwork 但是N…

Linux yum 使用时提示 获取 GPG 密钥失败Couldn‘t open file RPM-GPG-KEY-EPEL-7

获取 GPG 密钥失败&#xff1a;[Errno 14] curl#37 - “Couldn’t open file /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7” 这个就是安装的时候会检查key这个可以再repo文件中关闭即可 $ vim /etc/yum.repos.d/epel.repo 文件内容如下&#xff1a; [epel]nameExtra Packages for…

苹果ios企业签名一个月多少钱?

苹果ios企业签名一个月的费用因签名类型、应用类型等因素而异。 在某些情况下&#xff0c;苹果ios企业签名一个月的费用可能在2000&#xff5e;3000元之间。但也有特殊行业&#xff0c;例如金融行业&#xff0c;其企业签名费用可能会高于这个价格。同时&#xff0c;一些苹果io…

【Android】Hilt比Android好在哪里

Hilt框架的功能和设计理念&#xff0c;和Dagger基本是完全一致的&#xff0c;Hilt也是完全在Dagger基础上进行开发的 但是Dagger的用法比较繁琐&#xff0c;Hilt主要是做了便用性上的改进&#xff0c;主要有以下点 提供常用Component&#xff0c;不用再为每个InjectTarget都创…

创建 Springboot 项目

前言 创建 Spring Boot 项目是很多Java开发人员入门的重要一步&#xff01; 欢迎来到本篇关于创建 Spring Boot 项目的博客&#xff01;Spring Boot作为一个快速、便捷的开发框架&#xff0c;为我们提供了简化和加速应用程序开发的利器。 在这个数字化时代&#xff0c;快速响…

C语言从入门到实战——数组和指针的强化练习题

数组和指针的强化练习题 前言1. sizeof和strlen的对比1.1 sizeof1.2 strlen1.3 sizeof和strlen的对⽐ 2. 数组和指针笔试题解析2.1 一维数组2.2 字符数组2.3 二维数组 3. 指针运算笔试题解析3.1 题目1&#xff1a;3.2 题目23.3 题目33.4 题目43.5 题目53.6 题目63.7 题目7 前言…

rabbit MQ的延迟队列处理模型示例(基于SpringBoot死信模式)

说明&#xff1a; 生产者P 往交换机X&#xff08;typedirect&#xff09;会发送两种消息&#xff1a;一、routingKeyXA的消息&#xff08;消息存活周期10s&#xff09;&#xff0c;被队列QA队列绑定入列&#xff1b;一、routingKeyXB的消息&#xff08;消息存活周期40s&#xf…

迪文科技工业串口屏(DMG10600C070-03WTC)更新程序烧录刷机

迪文科技工业串口屏(DMG10600C070-03WTC)更新程序烧录刷机 问题 使用SD卡上电烧录&#xff0c;SD卡文件路径如下&#xff1a; 烧录时&#xff0c;无法写入&#xff0c;成功烧录文件数为0 解决方法 格式化读卡器 格式化脚本 echo off %1 %2 ver|find "5.">…

Android 13.0 无源码app修改它的icon图标

1.概述 在13.0的系统产品rom定制化开发中,有些产品需要对Launcher3中桌面显示的app的icon做替换,如果没有源码的话更换会麻烦点,需要从pms解析app的时候, 可以替换掉app的icon图标就可以了,接下来就来实现相关的功能 2.无源码app修改它的icon图标的相关核心类 framework…

Debian 12 / Ubuntu 22.04 安装 Docker 以及 Docker Compose 教程

Debian 12 / Ubuntu 22.04 安装 Docker 以及 Docker Compose 教程 本文将指导如何在 Debian 12 和 Ubuntu 22.04 下安装 Docker 以及 Docker Compose。 PS&#xff1a;本文同时适用于 Debian 11 以及 Ubuntu 20.04 什么是 Docker&#xff1f; Docker 是一种容器化技术&#x…

工厂模式之工厂方法模式(常用)

工厂方法模式 简单工厂模式违背了开闭原则&#xff0c;而工厂方法模式则是简单工厂模式的进一步深化&#xff0c;其不像简单工厂模式通过一个工厂来完成所有对象的创建&#xff0c;而是通过不同的工厂来创建不同的对象&#xff0c;每个对象有对应的工厂创建。 定义&#xff…

git 把项目托管到码云

码云&#xff1a; 把项目托管到码云 1.注册并微活码云账号(https://gitee.com/] 2.牛成井前博 SSH公钥 (运行 ssh -t gitgitee.com 构测 SSH 公明是否有开成功) 3.创建率户的码人伦;库 4.把本地项口上传到码云对应的空白仓库中 第一&#xff1a;上传个新项目 cd existing_git_…

【Redis篇】简述Redis | 详解Redis命令

文章目录 &#x1f38d;什么是Redis&#x1f38d;Redis特点&#x1f38d;Redis应用场景&#x1f354;Windows安装Redis⭐启动Redis &#x1f33a;Redis数据类型&#x1f33a;Redis常用命令⭐字符串string操作命令⭐哈希hash操作命令⭐列表list操作命令⭐集合set操作命令⭐有序集…

C++实战学习笔记

文章目录 erase()uniquevector的insert()std::string::npos erase() &#xff08;1&#xff09;erase(pos,n); 删除从pos开始的n个字符&#xff0c;比如erase(0,1)就是删除第一个字符 &#xff08;2&#xff09;erase(position);删除position处的一个字符(position是个string类…