Linux环境下需要获取.exe等文件的详细信息(Linux C++实现)

本文详细描述了如何在C++中解析WindowsPE文件结构,包括查找DOS头、NT头、可选头,以及定位和处理资源目录,特别是版本信息部分。
摘要由CSDN通过智能技术生成

解析Windows PE文件结构

推荐PE文件结构详解

PE结构解析
小甲鱼 PE 结构 详解

Linux下C++实现

实现步骤

/*
lpszFileFullPath:文件路径
lpVersionBuf:获取的文件版本信息
lpVerBufLen:长度
*/
STATUS Version::GetFileVersion(const CHAR* lpszFileFullPath, LPSTR lpVersionBuf, PDWORD lpVerBufLen)
{
	//获取文件句柄
	STATUS status;
	//指向文件缓存区指针
	PVOID pFileBuf = nullptr;
	//读取文件,获取文件句柄
	Version::ReadFile(pFileBuf, lpszFileFullPath);
	if(!pFileBuf){
		strcpy(status.errorMsg, "pFileBuf is nullptr");
		status.success = 0;
		return status;
	}
	//判断是否为PE文件
	//获取DOS头
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuf;
	if (!pDosHeader || pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		if(!pDosHeader)
			strcpy(status.errorMsg, "pDosHeader is nullptr");
		else 
			strcpy(status.errorMsg, "pDosHeader->e_magic != IMAGE_DOS_SIGNATURE");
		status.success = 0;
		return status;
	}
	//获取Nt头
	//e_lfanew,这里是指pe的偏移量,用于找到pe头的位置
    PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)((size_t)pFileBuf + pDosHeader->e_lfanew);
	if (!pNtHeader || pNtHeader->Signature != IMAGE_NT_SIGNATURE)
	{
		if(!pNtHeader)
			strcpy(status.errorMsg, "pNtHeader is nullptr");
		else 
			strcpy(status.errorMsg, "pNtHeader->e_magic != IMAGE_NT_SIGNATURE");
		
		status.success = 0;
		return status;
	}
	//获取可选头信息
    PIMAGE_OPTIONAL_HEADER32	pOptionalHeader = &(pNtHeader->OptionalHeader);
	if(!pOptionalHeader){
		strcpy(status.errorMsg, "pOptionalHeader is nullptr");
		status.success = 0;
		return status;
	}
	//获取15种目录表头指针
	//返回pDataDirectory是一个目录表的数组,有15种目录表,在这里获取的是IMAGE_DIRECTORY_ENTRY_RESOURCE资源目录表指针
	PIMAGE_DATA_DIRECTORY pDataDirectory = pOptionalHeader->DataDirectory;
	//需求根据相对虚拟地址VirtualAddress进行计算节,得到真实地址
	PIMAGE_RESOURCE_DIRECTORY pResourceTable = (PIMAGE_RESOURCE_DIRECTORY)GetPtrFromRVA(pDataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress, pNtHeader, pFileBuf);
	if(!pResourceTable){
		strcpy(status.errorMsg, "pResourceTable is nullptr");
		status.success = 0;
		return status;
	}
	//根据获取的资源表指针进行三级循环遍历,获取需要的信息
	bool flag = GetVersioninfo(status, pResourceTable, pNtHeader, pFileBuf, lpVersionBuf, lpVerBufLen);
	if(!flag){
		strcpy(status.errorMsg, "prase resource directory fail");
		status.success = flag;
		return status;
	}
	status.success = flag;
	return status;
}

获取文件缓存区指针

size_t Version::ReadFile(PVOID& pFileBuffer, const CHAR* lpszFile)
{
	FILE* pFile = nullptr;
    size_t FileSize = 0;
	size_t n = 0;
	//打开文件
    pFile = fopen(lpszFile, "rb");
    if (pFile == nullptr)
	{
		printf("文件打开失败");
		return 0;
	}
	//计算文件长度
	fseek(pFile, 0, SEEK_END);
	FileSize = ftell(pFile);
	fseek(pFile, 0, SEEK_SET);
	//分配缓冲区
	pFileBuffer = new CHAR[FileSize];
	if (pFileBuffer == nullptr)
	{
		printf("缓冲区分配失败");
		fclose(pFile);
		return 0;
	}
	//初始化缓冲区
	memset(pFileBuffer, 0, FileSize);
	n = fread(pFileBuffer, FileSize, 1, pFile);
	if (n == 0)
	{
		fclose(pFile);
		free(pFileBuffer);
		pFileBuffer = nullptr;
		return 0;
	}
	//关闭文件
	fclose(pFile);
	return n;
}

地址转换

PIMAGE_SECTION_HEADER Version::GetEnclosingSectionHeader(size_t rva, PIMAGE_NT_HEADERS32 pNTHeader) {
	PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(pNTHeader);
	unsigned i;

	for (i = 0; i < pNTHeader->FileHeader.NumberOfSections; i++, section++) {
		DWORD size = section->Misc.VirtualSize;
		if (0 == size)
			size = section->SizeOfRawData;

		// 检查RVA是否在此节内
		if ((rva >= section->VirtualAddress) &&
			(rva < (section->VirtualAddress + size)))
			return section;
	}
	return 0;
}

PVOID Version::GetPtrFromRVA(size_t rva, PIMAGE_NT_HEADERS32 pNTHeader, PVOID imageBase) {
	PIMAGE_SECTION_HEADER pSectionHdr;
	int delta;

	pSectionHdr = GetEnclosingSectionHeader(rva, pNTHeader);
	if (!pSectionHdr)
		return 0;

	delta = (int)(pSectionHdr->VirtualAddress - pSectionHdr->PointerToRawData);
    return (PVOID)((size_t)imageBase + rva - delta);
}

遍历资源目录,获取需要信息

资源表结构详解可以参考资源表

bool Version::GetVersioninfo(STATUS& status, PIMAGE_RESOURCE_DIRECTORY& pResourceTable, PIMAGE_NT_HEADERS32 pNtHeader, PVOID pFileBuf, PSTR szMsg, PDWORD dwMsgLen){
	
	VS_VERSIONINFO* pVersionInfo = nullptr;
	VS_FIXEDFILEINFO* pFixedFileInfo = nullptr;
	//pResourceTable + 1 表示资源表指针后面紧跟着是对应的资源目录指针
	PIMAGE_RESOURCE_DIRECTORY_ENTRY pResourceEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pResourceTable + 1);
	if(!pResourceEntry){
		strcpy(status.errorMsg, "1,pResourceEntry is nullprt");
		status.success = 0;
		return false;
	}
	//解析第一层
	//pResourceTable->NumberOfIdEntries + pResourceTable->NumberOfNamedEntries本目录中目录项的总和
	size_t dwTypeCount = pResourceTable->NumberOfIdEntries + pResourceTable->NumberOfNamedEntries;
	for (size_t i = 0; i < dwTypeCount; i++) {
        PIMAGE_RESOURCE_DIRECTORY_ENTRY pDirEntry = &pResourceEntry[i];
        if(!pDirEntry){
            strcpy(status.errorMsg, "1, pDirEntry is nullptr");
			status.success = 0;
			return false;
		}	
		//第二层
		//id==16对应版本信息子目录
		//pDirEntry->DataIsDirectory为1,表示还存在目录
        if (pDirEntry->Id == 16 && pDirEntry->DataIsDirectory) {
			// 获取版本信息的子目录
			// pDirEntry->OffsetToDirectory偏移量,相对于最开始的资源表指针pResourceTable 
			//(size_t)pResourceTable转换地址最好使用较大的类型,否则发生截断,就会导致地址读取错误
            PIMAGE_RESOURCE_DIRECTORY pVersionInfoDir = (PIMAGE_RESOURCE_DIRECTORY)((size_t)pResourceTable + pDirEntry->OffsetToDirectory);
			if(!pVersionInfoDir){
				strcpy(status.errorMsg, "2, pVersionInfoDir is nullptr");
				status.success = 0;
				return false;
			}	
			size_t dwVersionCount = pVersionInfoDir->NumberOfIdEntries + pVersionInfoDir->NumberOfNamedEntries;
			for (size_t j = 0; j < dwVersionCount; j++) {

				PIMAGE_RESOURCE_DIRECTORY_ENTRY pVersionEntry = &((PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pVersionInfoDir + 1))[j];
				if(!pVersionEntry){
					strcpy(status.errorMsg, "2, pVersionEntry is nullptr");
					status.success = 0;
					return false;
				}	
				//同上,解析第三层
                if (pVersionEntry->DataIsDirectory) {
                    PIMAGE_RESOURCE_DIRECTORY pVersionDataDir = (PIMAGE_RESOURCE_DIRECTORY)((size_t)pResourceTable + pVersionEntry->OffsetToDirectory);
                    if(!pVersionDataDir){
						strcpy(status.errorMsg, "3, pVersionDataDir is nullptr");
						status.success = 0;
						return false;
					}
					size_t dwDataCount = pVersionDataDir->NumberOfIdEntries + pVersionDataDir->NumberOfNamedEntries;
					for (size_t k = 0; k < dwDataCount; k++) {
						PIMAGE_RESOURCE_DIRECTORY_ENTRY pActualVersionEntry = &((PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pVersionDataDir + 1))[k];
                        if(!pActualVersionEntry){
							strcpy(status.errorMsg, "3, pActualVersionEntry is nullptr");
							status.success = 0;
							return false;
						}
                        // 获取版本信息的最终数据条目
                        //pActualVersionEntry->DataIsDirectory为0
                        if (!pActualVersionEntry->DataIsDirectory) {
							// 转换RVA为文件偏移量
                            PIMAGE_RESOURCE_DATA_ENTRY pDataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)((size_t)pResourceTable + pActualVersionEntry->OffsetToData);
							// 获取VS_VERSIONINFO结构
							if (pDataEntry != nullptr)
								//pDataEntry->OffsetToData是虚拟地址,使用GetPtrFromRVA进行转换
								//VS_VERSIONINFO信息结构
								pVersionInfo = (VS_VERSIONINFO*)GetPtrFromRVA(pDataEntry->OffsetToData, pNtHeader, pFileBuf);
							else
								continue;
							// 打印版本信息
							pFixedFileInfo = &pVersionInfo->Value;
                            sprintf(szMsg, "%d,%d,%d,%d",
                                            (int)pFixedFileInfo->dwProductVersionMS >> 16,
                                            (int)pFixedFileInfo->dwProductVersionMS & 0xFFFF,
                                            (int)pFixedFileInfo->dwProductVersionLS >> 16,
                                            (int)pFixedFileInfo->dwProductVersionLS & 0xFFFF);
							*dwMsgLen = strlen(szMsg);
						}
					}
				}
			}
		}
	}
    return true;
}

涉及的结构体与类型定义

#pragma once
typedef unsigned short WORD;
typedef unsigned short WCHAR;
typedef unsigned int  DWORD;
typedef unsigned char BYTE;
typedef void* PVOID;
typedef char CHAR;
typedef unsigned long long int64__;
typedef int64__ ULONG_PTR;
typedef long long LONG_PTR, *PLONG_PTR;
typedef CHAR *LPSTR, *PSTR;
typedef DWORD *PDWORD;
typedef int LONG;

#define IMAGE_SIZEOF_SHORT_NAME 8
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
#define IMAGE_DOS_SIGNATURE 0x5A4D
#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

typedef struct _IMAGE_FILE_HEADER {
	WORD Machine;
	WORD NumberOfSections;
	DWORD TimeDateStamp;
	DWORD PointerToSymbolTable;
	DWORD NumberOfSymbols;
	WORD SizeOfOptionalHeader;
	WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

//pe head struct
typedef struct _IMAGE_DATA_DIRECTORY {
      DWORD VirtualAddress;
      DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

//option header
typedef struct _IMAGE_OPTIONAL_HEADER64 {
	WORD Magic;
	BYTE MajorLinkerVersion;
	BYTE MinorLinkerVersion;
	DWORD SizeOfCode;
	DWORD SizeOfInitializedData;
	DWORD SizeOfUninitializedData;
	DWORD AddressOfEntryPoint;
	DWORD BaseOfCode;
    int64__ ImageBase;
	DWORD SectionAlignment;
	DWORD FileAlignment;
	WORD MajorOperatingSystemVersion;
	WORD MinorOperatingSystemVersion;
	WORD MajorImageVersion;
	WORD MinorImageVersion;
	WORD MajorSubsystemVersion;
	WORD MinorSubsystemVersion;
	DWORD Win32VersionValue;
	DWORD SizeOfImage;
	DWORD SizeOfHeaders;
	DWORD CheckSum;
	WORD Subsystem;
	WORD DllCharacteristics;
    int64__ SizeOfStackReserve;
    int64__ SizeOfStackCommit;
    int64__ SizeOfHeapReserve;
    int64__ SizeOfHeapCommit;
	DWORD LoaderFlags;
	DWORD NumberOfRvaAndSizes;
	IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
typedef struct _IMAGE_OPTIONAL_HEADER {
	// Standard fields.
	WORD    Magic;
	BYTE    MajorLinkerVersion;
	BYTE    MinorLinkerVersion;
	DWORD   SizeOfCode;
	DWORD   SizeOfInitializedData;
	DWORD   SizeOfUninitializedData;
	DWORD   AddressOfEntryPoint;
	DWORD   BaseOfCode;
	DWORD   BaseOfData;
	// NT additional fields.
	DWORD   ImageBase;
	DWORD   SectionAlignment;
	DWORD   FileAlignment;
	WORD    MajorOperatingSystemVersion;
	WORD    MinorOperatingSystemVersion;
	WORD    MajorImageVersion;
	WORD    MinorImageVersion;
	WORD    MajorSubsystemVersion;
	WORD    MinorSubsystemVersion;
	DWORD   Win32VersionValue;
	DWORD   SizeOfImage;
	DWORD   SizeOfHeaders;
	DWORD   CheckSum;
	WORD    Subsystem;
	WORD    DllCharacteristics;
	DWORD   SizeOfStackReserve;
	DWORD   SizeOfStackCommit;
	DWORD   SizeOfHeapReserve;
	DWORD   SizeOfHeapCommit;
	DWORD   LoaderFlags;
	DWORD   NumberOfRvaAndSizes;
	IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

//NT header
typedef struct _IMAGE_NT_HEADERS64 {
	DWORD Signature;
	IMAGE_FILE_HEADER FileHeader;
	IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
	DWORD Signature;
	IMAGE_FILE_HEADER FileHeader;
	IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

#ifdef _WIN64
typedef IMAGE_NT_HEADERS64                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64                 PIMAGE_NT_HEADERS;
typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64            PIMAGE_OPTIONAL_HEADER;
#else
typedef IMAGE_NT_HEADERS32                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32                 PIMAGE_NT_HEADERS;
typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32            PIMAGE_OPTIONAL_HEADER;
#endif


typedef struct tagVS_FIXEDFILEINFO
{
    DWORD   dwSignature;            /* e.g. 0xfeef04bd */
    DWORD   dwStrucVersion;         /* e.g. 0x00000042 = "0.42" */
    DWORD   dwFileVersionMS;        /* e.g. 0x00030075 = "3.75" */
    DWORD   dwFileVersionLS;        /* e.g. 0x00000031 = "0.31" */
    DWORD   dwProductVersionMS;     /* e.g. 0x00030010 = "3.10" */
    DWORD   dwProductVersionLS;     /* e.g. 0x00000031 = "0.31" */
    DWORD   dwFileFlagsMask;        /* = 0x3F for version "0.42" */
    DWORD   dwFileFlags;            /* e.g. VFF_DEBUG | VFF_PRERELEASE */
    DWORD   dwFileOS;               /* e.g. VOS_DOS_WINDOWS16 */
    DWORD   dwFileType;             /* e.g. VFT_DRIVER */
    DWORD   dwFileSubtype;          /* e.g. VFT2_DRV_KEYBOARD */
    DWORD   dwFileDateMS;           /* e.g. 0 */
    DWORD   dwFileDateLS;           /* e.g. 0 */
} VS_FIXEDFILEINFO;

typedef struct {
	WORD             wLength;
	WORD             wValueLength;
	WORD             wType;
	WCHAR            szKey[16];
	WORD             Padding1;
	VS_FIXEDFILEINFO Value;
	WORD             Padding2;
	WORD             Children;
} VS_VERSIONINFO;
typedef struct {
	WORD  wLength;
	WORD  wValueLength;
	WORD  wType;
	WCHAR szKey;
	WORD  Padding;
	WORD  Value;
} String;
typedef struct {
	WORD   wLength;
	WORD   wValueLength;
	WORD   wType;
	WCHAR  szKey;
	WORD   Padding;
	String Children;
} StringTable;
typedef struct {
	WORD        wLength;
	WORD        wValueLength;
	WORD        wType;
	WCHAR       szKey;
	WORD        Padding;
	StringTable Children;
} StringFileInfo;

//dos header
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

//section
typedef struct _IMAGE_SECTION_HEADER {
	BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
	union {
DWORD PhysicalAddress;
DWORD VirtualSize;
	} Misc;
	DWORD VirtualAddress;
	DWORD SizeOfRawData;
	DWORD PointerToRawData;
	DWORD PointerToRelocations;
	DWORD PointerToLinenumbers;
	WORD NumberOfRelocations;
	WORD NumberOfLinenumbers;
	DWORD Characteristics;
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

#define FIELD_OFFSET(type, field)    ((LONG)(LONG_PTR)&(((type *)0)->field))
#define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER)        \
    ((ULONG_PTR)(ntheader) +                                            \
     FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) +                 \
     ((ntheader))->FileHeader.SizeOfOptionalHeader   \
    ))
	
//1 resource directory
typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    WORD    NumberOfNamedEntries;
    WORD    NumberOfIdEntries;
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
//2 
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
        struct {
            DWORD NameOffset:31;
            DWORD NameIsString:1;
        } DUMMYSTRUCTNAME;
        DWORD   Name;
        WORD    Id;
    } DUMMYUNIONNAME;
    union {
        DWORD   OffsetToData;
        struct {
            DWORD   OffsetToDirectory:31;
            DWORD   DataIsDirectory:1;
        } DUMMYSTRUCTNAME2;
    } DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
//3
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;
    DWORD   Size;
    DWORD   CodePage;
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
struct STATUS
{
	STATUS(){
		memset(errorMsg, 0, sizeof errorMsg);
		success = -1;
	}
	CHAR errorMsg[128]; //错误信息
	DWORD success;		//状态码,0 失败,1 成功
};

注意点

         要主要实现中不同系统下对基本类型字节长度的情况,上述代码使用与32位与64位下进行数据读取,64位下使用要将32结构体都换成64位,
其中差别主要是int64__与DWORD之间字节长度的不同,这部分建议查看官方文档,再根据自己的需求进行改进。
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值