【免杀前置课——PE文件结构】十八、数据目录表及其内容详解——数据目录表(导出表、导入表、IAT表、TLS表)详解;如何在程序在被调试之前反击?TLS反调试(附代码)

这篇具有很好参考价值的文章主要介绍了【免杀前置课——PE文件结构】十八、数据目录表及其内容详解——数据目录表(导出表、导入表、IAT表、TLS表)详解;如何在程序在被调试之前反击?TLS反调试(附代码)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

数据目录表 IMAGE_DATA_DIRECTORY

数据目录表:可选PE头最后一个成员,就是数据目录.一共有16个
分别是:导出表、导入表、资源表、异常信息表、安全证书表、重定位表、调试信息表、版权所以表、全局指针表
TLS表、加载配置表、绑定导入表、IAT表、延迟导入表、COM信息表 最后一个保留未使用,默认为0。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
NumberOfRvaAndSize数据目录表的个数。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
VirtualAddress RVA内存偏移
FOA硬盘中的偏移
Size 大小

导出表 IMAGE_EXPORT_DIRECTORY

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
Name 导出表文件名首地址
Base 导出函数起始序号
NumberOfFunctions是dll文件中导出函数的个数:最大的序号-最小序号+1,下图为6。
NumberOfNames以名称导出函数的个数:即在dll文件中函数后面不加noname的数量,下图为2.
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

在DLL文件中如何找到要用的函数呢?

AddressOfNames存的是函数名称起始位置的偏移。
AddressOfNameOrdinals存的是序号,加上Base等于dll文件中函数后面的序号。
AddressOfFunctions存的是真正函数存储位置的偏移。

下图从右向左
要找到MessageBoxW的函数地址,首先从AddressOfNames在AddressOfNameOrdinals中的索引找到MessageBoxW的序号,在AddressOfFunctions按序号找到地址。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

数据FOA-区段FOA=数据RVA-区段RVA
区段FOA=数据FOA-数据RVA+区段RVA

导入表 IMAGE_IMPORT_DESCRIPTOR

问题
  • 1、调用dll文件函数原理?

程序在调用dl1文件函数时,并不是把d11文件函数的代码编译到当前文件中,而是把d11文件对应的函数地址保存到了当前文件中。(调用默认在内存)

  • 2、一个进程空间中的exe dll文件如何被加载到内存的?

exe被最先加载,后需要哪个dll文件再进行调用。

  • 3、Exe文件调用的动态链接库在内存中与在硬盘中有什么不同?

在硬盘中exe存储的是调用dll时用到的函数名称,在内存中是调用dll时将dll加载进内存后函数的地址。

这三个问题是非常重要的,主要是要能理解exe和dll文件之间的关系,看下图实例的讲解:
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
我们知道在一个进程中分配了4G内存空间,在我们exe文件中的该如何调用dll文件中的函数呢?
如上图右侧4G内存中,exe文件运行到要调用函数时找到函数调用的内存地址,对dll文件中函数进行调用。
但是这么多dll文件中的函数,不能保证这函数每次被加载到内存时一直在这个位置,该如何解决呢?
事实上,exe文件在硬盘中存储时在要调用dll文件(HMODULE)中函数位置存放的是函数的名称(func)当exe文件被加载进内存时发现要调用哪个dll文件,把dll文件加载进进程内存中,后利用GetProcessAddr(HMODULE,func)将在该HMODULE模块中要调用的function找到此时拿到了函数真正在内存的地址,然后才把类似上图中0x600000的地址填到exe文件中的。
也就是说exe对dll中函数的调用,dll的加载是一个动态的过程,用到哪个dll文件再加载,因为不能确定每个dll文件每次被加载进内存的位置,所以并不能一次写死,但是exe文件可以写死,因为exe是最先加载进4G内存的文件

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

导入表解析

一个文件只有一个导出表,有多个导入表
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

INT:导入名称表,无论在文件中还是在内存中都是指向函数的名称
IAT: 导入地址表,在文件中时,与INT是一样的指向函数名称,在内存中保存的是函数实际地址
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
OriginalFirstThunk指向一个结构体PIMAGE_THUNK_DATA,这结构体其中的联合体判断是按序号导入还是按名称导入,最高位如果为1就是按序号导入的,其他31位都为序号,如果不为1就是按名称导入的,其他都是函数名称。
OriginalFirstThunk(INT)中包含着IMAGE_IMPORT_BY_NAME结构体,存储的是导入函数的名称。
Name 指向被导入的dll的 RVA。
FirstThunk(IAT)中 需要根据判断TimeDataStamp是否为0,若为0则和IMAGE_IMPORT_BY_NAME一样,不为0则存储的是导入函数实际的位置。

获取导入表

文件结构

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

CPeUtil.cpp

#include "CPeUtil.h"

CPeUtil::CPeUtil()
{
	FileBuff=NULL;
	FileSize=0;
	pDosHeader = NULL;
	pNtHeaders = NULL;
	pFileHeader = NULL;
	pOptionHeader = NULL;
}

CPeUtil::~CPeUtil()
{
	if (FileBuff)
	{
		delete[]FileBuff;
		FileBuff = NULL;
	}
}
//载入文件
BOOL CPeUtil::loadFile(const char* patch)
{
	HANDLE hFile = CreateFileA(patch, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
	if (hFile==0) 
	{
		return FALSE;
	}
	//私有成员变量获取文件大小并初始化缓冲区
	FileSize = GetFileSize(hFile, 0);
	FileBuff = new char[FileSize]{0};
	DWORD realReadBytes = 0;
	//是否读取成功
	BOOL readSuccess =ReadFile(hFile,FileBuff,FileSize,&realReadBytes,0);
	if (readSuccess==0)
	{
		return FALSE;
	}
	if (InitPeInfo())
	{
		CloseHandle(hFile);
		return TRUE;
	}
	return FALSE;
}

//加载文件后初始化不同头位置
BOOL CPeUtil::InitPeInfo()
{
	//用以下两个判断该文件是否为PE文件
	pDosHeader = (PIMAGE_DOS_HEADER)FileBuff;
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		return FALSE;
	}
	pNtHeaders = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + FileBuff);
	if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
	{
		return FALSE;
	}
	pFileHeader = &pNtHeaders->FileHeader;
	pOptionHeader = &pNtHeaders->OptionalHeader;

	return TRUE;
}

//输出区段头
void CPeUtil::PrintSectionHeaders()
{
	PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders);//获取第一个区段头地址
	//遍历不同区段
	for (int i = 0; i < pFileHeader->NumberOfSections; i++)
	{
		char name[9]{ 0 };
		memcpy_s(name, 9, pSectionHeaders->Name, 8);
		printf("区段名称:%s\n", name);
		pSectionHeaders++;
	}
}

//解析导出表
void CPeUtil::GetExportTable()
{
	IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[0];
	PIMAGE_EXPORT_DIRECTORY pexport = (PIMAGE_EXPORT_DIRECTORY)RvaToFoa(directory.VirtualAddress);
	char *dllName = RvaToFoa(pexport->Name)+FileBuff;
	printf("文件名称:%s\n", dllName);
	//遍历不同函数的地址
	DWORD* funaddr = (DWORD*)(RvaToFoa(pexport->AddressOfFunctions) + FileBuff);
	WORD* peot = (WORD*)(RvaToFoa(pexport->AddressOfNameOrdinals) + FileBuff);
	DWORD* pent = (DWORD*)(RvaToFoa(pexport->AddressOfNames) + FileBuff);
	for (int i = 0; i < pexport->NumberOfFunctions; i++)
	{
		printf("函数地址为:%x\n",*funaddr);
		for (int j = 0; j < pexport->NumberOfNames; j++)
		{
			if (peot[j]==i)
			{
				char* funName = RvaToFoa(pent[j])+FileBuff;
				printf("函数名称为:%s\n", funName);
				break;
			}
		}
		funaddr++;
	}
	
}

//获取导入表
void CPeUtil::GetImportTables()
{
	//导入表也是数据目录表的一部分,作为第二个
	IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[1];
	//获取真正导入表地址
	PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(directory.VirtualAddress) + FileBuff);
	//判断联合体中是否有数据
	while (pImport->OriginalFirstThunk)
	{
		char* dllName = RvaToFoa(pImport->Name) + FileBuff;
		printf("dll文件名称为:%s\n", dllName);
		PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk) + FileBuff);
		//判断联合体中是否有数据
		while (pThunkData->u1.Function)
		{
			//判断是按序号导入还是按名称导入
			if (pThunkData->u1.Ordinal & 0x80000000)
			{
				printf("按序号导入:%d\n", pThunkData->u1.Ordinal & 0x7FFFFFFF);
			}
			else
			{
				PIMAGE_IMPORT_BY_NAME importName = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pThunkData->u1.AddressOfData) + FileBuff);
				printf("按名称导入:%s\n", importName->Name);
			}
			pThunkData++;
		}
		pImport++;
	}
}

//RVA转化FOA
DWORD CPeUtil::RvaToFoa(DWORD rva)
{
	
	PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders);//获取第一个区段头地址
	//遍历不同区段
	for (int i = 0; i < pFileHeader->NumberOfSections; i++)
	{
		if (rva >= pSectionHeaders->VirtualAddress && rva < pSectionHeaders->VirtualAddress + pSectionHeaders->Misc.VirtualSize)
		{
			//数据的FOA=数据的RVA-区段的RVA+区段的FOA
			return rva - pSectionHeaders->VirtualAddress + pSectionHeaders->PointerToRawData;
		}
		pSectionHeaders++;
	}
	return 0;
}


CPeUtil.h

#pragma once
#include<Windows.h>
#include<iostream>
class CPeUtil {
public:
	CPeUtil();
	~CPeUtil();
	BOOL loadFile(const char* patch);
	BOOL InitPeInfo();
	void PrintSectionHeaders();
	void GetExportTable();
	void GetImportTables();
private:
	char* FileBuff;
	DWORD FileSize;
	PIMAGE_DOS_HEADER pDosHeader;
	PIMAGE_NT_HEADERS pNtHeaders;
	PIMAGE_FILE_HEADER pFileHeader;
	PIMAGE_OPTIONAL_HEADER pOptionHeader;
	DWORD RvaToFoa(DWORD rva);
};

main.cpp

#include<iostream>
#include"CPeUtil.h"

int main()
{
	CPeUtil peUtil;
	BOOL ifSuccess = peUtil.loadFile("D:\\code\\VisualStudio2022\\FirstDLL\\Debug\\FirstDLL.dll");
	if (ifSuccess)
	{
		peUtil.GetImportTables();
		//peUtil.GetExportTable();
		//peUtil.PrintSectionHeaders();
		return 0;
	}
	printf("加载PE文件失败!\n");
	return 0;
}

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

重定位表 IMAGE_BASE_RELOCATION

在代码中有很多像这种的全局变量,但是像我们之前说的,如果她是直接写死的,就无法确定在加载到内存后还在那个位置,该怎么办?
PE文件创建了多张表来存放这些写死地址的数据位置,用的时候到表中遍历找到相应数据即可。

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
1.VirtualAddress 是 Base Relocation Table 的位置它是一个 RVA 值;
2.SizeOfBlock 是 Base Relocation Table 的大小;
3.TypeOffset是一个数组,数组每项大小为两个字节(16位),它由高 4位和低 12位组成,高 4位代表重定位类型,低 12位是重定位地址,它与 VirtualAddress 相加即是指向PE 映像中需要修改的那个代码的地址。

下图就是重定位表的实例。假如有个0x4002/0x4018/0x4088被存入,则会存在第一个0x4000重定位表中,0x2/0x18/0x88,为什么要有多个重定位表,若为0x5000以上该怎么存?
放到下一个重定位表中,因为其为分页存储,每页偏移为0x1000,所以不同位置的相隔0x1000进行存储。
VirtualAddress + WORD格子中的偏移 = 要修复的地址RVA
但是像这些格子中,拿到的数据可能并不需要修复,如何判断呢?
在这个结构体中除了VirtualAddress和sizeofBlock是DWORD类型的,其他的是WORD类型,也就是16字节,在16字节中最高4位如果等于3,说明它是需要被修复的。也就是高4位是标识,后12位才是真正的偏移值。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

TLS

线程A去修改TLS变量时线程B是不会受影响的,因为每个线程都拥有一个TLS变量的副本。
什么是TLS?
TLS是 Thread Local Storage的缩写线程局部存储。主要是为了解决多线程中变量同步的问题。
TLS的意义?
进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量。在一个程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交变得非常快捷。说它是缺点,多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相的 BUG。
如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为staticmemory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS
TLS变量只需要定义一次,类似全局变量,但定义完后每一个线程都能获取TLS变量的副本,解决了不能同步访问TLS的问题。节约了时间和成本。

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

#include<Windows.h>
#include<iostream>
#pragma comment(linker,"/INCLUDE:__tls_used")
_declspec(thread) int g_number = 100;
HANDLE hEvent = NULL;

DWORD WINAPI threadProc1(LPVOID lparam)
{
	g_number = 200;
	printf("threadProc1 g_number=%d\n", g_number);
	SetEvent(hEvent);
	return 0;
}

DWORD WINAPI threadProc2(LPVOID lparam)
{
	WaitForSingleObject(hEvent, -1);
	printf("threadProc2 g_number=%d\n", g_number);
	return 0;
}

void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	printf("TLS函数执行了\n");
}

#pragma data_seg(".CRT$XLX")
//存储回调函数地址
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { t_TlsCallBack_A,0 };
#pragma data_seg()

int main()
{
	hEvent = CreateEventA(NULL, FALSE, FALSE, NULL);
	HANDLE hThread1 = CreateThread(NULL, NULL, threadProc1, NULL, NULL, NULL);
	HANDLE hThread2 = CreateThread(NULL, NULL, threadProc2, NULL, NULL, NULL);
	WaitForSingleObject(hThread1,-1);
	WaitForSingleObject(hThread2,-1);
	CloseHandle(hEvent);
	system("pause");
	return 0;
}

TLS函数我们可以看到执行了五次,他有点像PHP中的魔法函数,分别在进程创建/销毁,线程创建/销毁时调用TLS回调函数。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Reserved)中:
Reason是调用函数的时机,可以进行判断(即进程创建/销毁,线程创建/销毁时机判断)。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
在晋城创建时调用TLS函数,这也说明了TLS是最先执行的。
OD加载一个程序,OEP我们之前认为是最先执行的,实际上TLS在OEP执行之前就已经执行了。

TLS反调试

TLS用途2:
在安全领域中,TLS常被用来处理诸如反调试、抢占执行等操作。

既然我们知道了TLS是最先执行的,那么我们在TLS回调函数中加上判断是否被调试的API,若被调试直接在OEP之前终止程序,即可做到反调试。

#include<Windows.h>
#include<iostream>
#pragma comment(linker,"/INCLUDE:__tls_used")
void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	if (Reason == DLL_PROCESS_ATTACH)
	{
		BOOL result = FALSE;
		HANDLE hNewHandle = 0;
		DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hNewHandle, NULL, NULL, DUPLICATE_SAME_ACCESS);
		CheckRemoteDebuggerPresent(hNewHandle, &result);//微软提供的API 判断该文件有没有被调试
		if (result)
		{
			MessageBoxA(0, "程序被调试了!", "警告", MB_OK);
			ExitProcess(0);
		}
	}
	return;
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1,0 }; 
#pragma data_seg()

int main()
{
	printf("main函数执行了");
	system("pause");
	return 0;
}

rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft

TLS表 IMAGE_TLS_DIRECTORY32

这其中的地址都是VA,而不是RVA。
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft
rvatofoa,PE文件结构,免杀前置课,windows,服务器,c++,microsoft文章来源地址https://www.toymoban.com/news/detail-792011.html

到了这里,关于【免杀前置课——PE文件结构】十八、数据目录表及其内容详解——数据目录表(导出表、导入表、IAT表、TLS表)详解;如何在程序在被调试之前反击?TLS反调试(附代码)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构和算法 - 前置扫盲

    1、数据结构分类 1.1 逻辑结构:线性与非线性 tip: 逻辑结构揭示了数据元素之间的逻辑关系 。 线性数据结构 :元素间存在明确的顺序关系。 数据按照一定顺序排列,其中元素之间存在一个对应关系,使得它们按照线性顺序排列。 每个元素都有且仅有一个前驱元素和一个后

    2024年02月04日
    浏览(38)
  • Linux对一个目录及其子目录所有文件添加权限

    chmod是一个改变用户拥有指定文件的权限的命令.r:只读,w:写,x执行.也可以用数字 -rw------- (600) -- 只有属主有读写权限。   -rw-r--r-- (644) -- 只有属主有读写权限;而属组用户和其他用户只有读权限。   -rwx------ (700) -- 只有属主有读、写、执行权限。   -rwxr-xr-x (755) -- 

    2024年02月02日
    浏览(89)
  • Linux——认识Linux的目录结构 & 常用命令 & vim命令 & 权限及其控制

    一切皆文件 文件分类 【安装】Linux环境下的 JDK的安装 安装配置 环境变量 1.进程kill -9 运行窗口退出 2.ctrl c退出 ls -a 查看所有文件(包含隐藏) ​ ls -la 查看所有文件详细信息 查看当前文件夹下的文件 在 Linux 系统中,ls 和 ll 命令都是用来列出目录内容的命令,它们的区别

    2024年02月16日
    浏览(56)
  • shell 脚本统计 http 文件服务器下指定目录及其子目录下所有文件的大小

    shell脚本如下: 首先 vi calculate_size.sh 写入下入内容 执行 sh calculate_size.sh http://example.com/some/dir/ 即可统计 http 文件服务器http://example.com/some/dir/ 中 dir 目录及其子目录下所有文件的大小。

    2024年02月15日
    浏览(55)
  • 【数据结构一】初始Java集合框架(前置知识)

           Java语言在设计之初有一个非常重要的理念便是:write once,run anywhere!所以Java中的数据结构是已经被设计者封装好的了,我们只需要实例化出想使用的对象,便可以操作相应的数据结构了,本篇文章中我会向大家简单 介绍一下什么是数据结构 ,以及 对Java中常用的数

    2024年02月04日
    浏览(46)
  • 【数据结构前置知识】初识集合框架和时间,空间复杂度

    Java 集合框架 Java Collection Framework ,又被称为容器 container ,是定义在 java.util 包下的一组接口 interfaces 和其实现类 classes 。 其主要表现为将多个元素 element 置于一个单元中,用于对这些元素进行快速、便捷的存储 store 、检索 retrieve 、管理 manipulate ,即平时我们俗称的增删查

    2024年02月09日
    浏览(36)
  • Hadoop3教程(二十八):(生产调优篇)NN、DN的多目录配置及磁盘间数据均衡

    NN多目录的意思是,本地目录可以配置成多个,且每个目录存放内容相同,这样的目的是增加可靠性。比如说下图这样: 但其实生产中不常用哈, 生产中要增加NN的可靠性的话,一般会开启NN的高可用,即在不同节点上开启多个NN,靠zookeeper来协调 。 所以本节就 了解一下即可

    2024年02月08日
    浏览(42)
  • 文件目录:FCB、索引节点、目录结构

    FCB(文件控制块): 存放控制文件需要的各种信息的数据结构,以实现按名存取。FCB的有序集合称为文件目录,一个FCB就是一个文件目录项。 FCB包含了文件的基本信息、存取控制信息、使用信息。最重要还是文件名信息和物理存放外存地址的信息,实现了文件名和文件之间

    2024年02月10日
    浏览(48)
  • 文件管理---索引文件结构、位示图、树形目录结构

    操作系统中对软件的管理就是对文件的管理(基本单位) 逻辑结构:主要是概念当中的一些结构层次,主要有流式文件和记录式文件; 物理结构: 顺序文件结构:按照文件的逻辑顺序存储到物理结构当中,相当于给文件分配一个连续的存储空间,逻辑上是什么形式,物理上

    2024年01月19日
    浏览(33)
  • 数据结构(超详细讲解!!)第十八节 串(堆串)

    假设以一维数组heap MAXSIZE 表示可供字符串进行动态分配的存储空间,并设 int start 指向heap 中未分配区域的开始地址(初始化时start =0) 。在程序执行过程中,当生成一个新串时,就从start指示的位置起,为新串分配一个所需大小的存储空间,同时建立该串的描述。这种存储

    2024年02月05日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包