7.1 实现进程内存块枚举

news2025/1/17 0:16:18

Windows操作系统中,每个进程的虚拟地址空间都被划分为若干内存块,每个内存块都具有一些属性,如内存大小、保护模式、类型等。这些属性可以通过VirtualQueryEx函数查询得到。

该函数可用于查询进程虚拟地址空间中的内存信息的函数。它的作用类似于Windows操作系统中的Task Manager中的进程选项卡,可以显示出一个进程的内存使用情况、模块列表等信息。使用VirtualQueryEx函数,可以枚举一个进程的所有内存块。该函数需要传入要查询的进程的句柄、基地址和一个MEMORY_BASIC_INFORMATION结构体指针。它会返回当前内存块的基地址、大小、状态(free/commit/reserve)、保护模式等信息。

在实现对内存块的枚举之前,我们先通过ReadProcessMemory函数实现一个内存远程内存读取功能,如下代码所示,首先,通过OpenProcess函数打开进程句柄,获得当前进程的操作权限。然后,调用EnumMemory函数,传入进程句柄以及起始地址和终止地址参数,依次读取每一页内存,通过循环打印其内存数据。

#include <iostream>
#include <windows.h>

// 枚举内存实现
void EnumMemory(HANDLE Process, DWORD BeginAddr, DWORD EndAddr)
{
  // 每次读入长度
  const DWORD pageSize = 1024;

  BYTE page[pageSize];
  DWORD tmpAddr = BeginAddr;
  while (tmpAddr <= EndAddr)
  {
    ReadProcessMemory(Process, (LPCVOID)tmpAddr, &page, pageSize, 0);
    for (int x = 0; x < pageSize; x++)
    {
      if (x % 15 == 0)
      {
        printf("| 0x%08X \n", tmpAddr + x);
      }
      printf("0x%02X ", page[x]);
    }
    tmpAddr += pageSize;
  }
}

int main(int argc, char* argv[])
{
  HANDLE process;

  // 打开当前进程
  process = OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessId());

  // 枚举内存 从0x401000 - 0x7FFFFFFF
  EnumMemory(process, 0x00401000, 0x7FFFFFFF);

  system("pause");
  return 0;
}

上述代码简单明了,易于理解,但并没有实现过滤特定内存属性的功能。如果需要对特定类型的内存进行分析,需要结合VirtualQueryEx函数实现内存属性的查询和过滤。

接着我们进入本章的重点,实现枚举进程内存块,要实现该功能首先读者必须要了解一个结构体_SYSTEM_INFO该结构体是系统信息结构,可用于存储系统硬件和系统配置信息,而我们所需要的内存块数据同样可以使用该结构进行存储。

根据具体需求,可以通过调用GetSystemInfo函数来获得_SYSTEM_INFO结构体的信息。GetSystemInfo函数可以返回系统的硬件信息,包括有多少个处理器,每个处理器有多少个核心,系统页大小等信息,该结构体的定义如下所示;

    typedef struct _SYSTEM_INFO {
      union {
      DWORD dwOemId;                          // 兼容性保留
      struct {
        WORD wProcessorArchitecture;          // 操作系统处理器体系结构
        WORD wReserved;                       // 保留
      } DUMMYSTRUCTNAME;
      } DUMMYUNIONNAME;
      DWORD     dwPageSize;                   // 页面大小和页面保护和承诺的粒度
      LPVOID    lpMinimumApplicationAddress;  // 指向应用程序和dll可访问的最低内存地址的指针
      LPVOID    lpMaximumApplicationAddress;  // 指向应用程序和dll可访问的最高内存地址的指针
      DWORD_PTR dwActiveProcessorMask;        // 处理器掩码
      DWORD     dwNumberOfProcessors;         // 当前组中逻辑处理器的数量
      DWORD     dwProcessorType;              // 处理器类型,兼容性保留
      DWORD     dwAllocationGranularity;      // 虚拟内存的起始地址的粒度
      WORD      wProcessorLevel;              // 处理器级别
      WORD      wProcessorRevision;           // 处理器修订
    } SYSTEM_INFO, *LPSYSTEM_INFO;

接着就是要查询内存块的状态了,我们可通过VirtualQueryEx函数实现查询进程虚拟地址空间中的内存信息,其原型定义如下:

SIZE_T VirtualQueryEx(
  HANDLE                    hProcess,
  LPCVOID                   lpAddress,
  PMEMORY_BASIC_INFORMATION lpBuffer,
  SIZE_T                    dwLength
);

参数说明:

  • hProcess:进程句柄。需要查询的进程的句柄
  • lpAddress:基地址。需要查询的内存块的基地址
  • lpBuffer:内存信息缓冲区。 PMEMORY_BASIC_INFORMATION 结构指针,用于存储查询结果。它包含了取得的内存块信息,如基地址、保护属性、状态、大小等
  • dwLength:缓冲区大小。缓冲区的大小,以字节为单位。如果缓冲区太小,则函数将返回指定的内存块信息长度存放到此处,不会写入完整的信息

该函数返回实际填充到缓冲区中的字节数。如果函数失败,则返回0。当我们需要了解特定进程的内存使用情况时,可以使用VirtualQueryEx()函数枚举进程内存中的所有内存块,并按需查询其中的属性值。

#include <iostream>
#include <windows.h>
#include <Psapi.h>

#pragma comment(lib,"psapi.lib")

// 枚举特定进程内存块信息
VOID ScanProcessMemory(HANDLE hProc)
{
  SIZE_T stSize = 0;
  PBYTE pAddress = (PBYTE)0;
  SYSTEM_INFO sysinfo;
  MEMORY_BASIC_INFORMATION mbi = { 0 };

  //获取页的大小
  ZeroMemory(&sysinfo, sizeof(SYSTEM_INFO));
  GetSystemInfo(&sysinfo);

  // 得到的镜像基地址
  pAddress = (PBYTE)sysinfo.lpMinimumApplicationAddress;

  printf("------------------------------------------------------------------------ \n");
  printf("开始地址 \t 结束地址 \t 大小 \t 状态 \t 内存类型 \t 页面属性 \n");
  printf("------------------------------------------------------------------------ \n");
  
  // 判断只要当前地址小于最大地址就循环
  while (pAddress < (PBYTE)sysinfo.lpMaximumApplicationAddress)
  {
    // 对结构体进行初始化
    ZeroMemory(&mbi, sizeof(MEMORY_BASIC_INFORMATION));

    // 查询内存属性
    stSize = VirtualQueryEx(hProc, pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION));

    if (stSize == 0)
    {
      pAddress += sysinfo.dwPageSize;
      continue;
    }

    // 输出查询结果
    printf("0x%08X \t 0x%08X \t %8d K \t ", mbi.BaseAddress, ((DWORD)mbi.BaseAddress + (DWORD)mbi.RegionSize), mbi.RegionSize >> 10);

    // 输出状态
    switch (mbi.State)
    {
      case MEM_FREE: printf("空闲 \t"); break;
      case MEM_RESERVE: printf("保留 \t"); break;
      case MEM_COMMIT: printf("提交 \t"); break;
      default: printf("未知 \t"); break;
    }

    // 输出类型
    switch (mbi.Type)
    {
      case MEM_PRIVATE:   printf("私有  \t"); break;
      case MEM_MAPPED:    printf("映射  \t"); break;
      case MEM_IMAGE:     printf("镜像  \t"); break;
      default: printf("未知  \t"); break;
    }

    if (mbi.Protect == 0)
    {
      printf("---");
    }
    else if (mbi.Protect & PAGE_EXECUTE)
    {
      printf("E--");
    }
    else if (mbi.Protect & PAGE_EXECUTE_READ)
    {
      printf("ER-");
    }
    else if (mbi.Protect & PAGE_EXECUTE_READWRITE)
    {
      printf("ERW");
    }
    else if (mbi.Protect & PAGE_READONLY)
    {
      printf("-R-");
    }
    else if (mbi.Protect & PAGE_READWRITE)
    {
      printf("-RW");
    }
    else if (mbi.Protect & PAGE_WRITECOPY)
    {
      printf("WCOPY");
    }
    else if (mbi.Protect & PAGE_EXECUTE_WRITECOPY)
    {
      printf("EWCOPY");
    }

    printf("\n");

    // 每次循环累加内存块的位置
    pAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize;
  }
}

int main(int argc, char* argv[])
{
  // 打开进程
  HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());

  // 开始枚举
  ScanProcessMemory(hProc);
  CloseHandle(hProc);

  system("pause");
  return 0;
}

运行上述代码片段则首先通过GetCurrentProcessId()得到自身进程的PID号,接着通过调用ScanProcessMemory函数实现对自身进程内存块的枚举功能,最终输出如下图所示的效果;

当然了虽然上述代码可以实现对内存块的枚举功能,但是在实际的开发场景中我们还是需要将枚举结果存储起来以便后期调用,此时我们可以考虑在全局定义vector容器,容器的属性为每一个内存块的MEMORY_BASIC_INFORMATION属性,当需要查询时只需要枚举这个容器并循环输出该容器内的数据即可,改进后的代码如下所示;

#include <Windows.h>
#include <vector>
#include <iostream>
#include <assert.h>

using namespace std;

// 枚举指定进程所有内存块
static bool ScanProcessMemory(HANDLE hProcess, OUT vector<MEMORY_BASIC_INFORMATION>& memories)
{
  // 如果 hProcess 为空则结束运行
  assert(hProcess != nullptr);

  // 初始化容器并设置容量
  memories.clear();
  memories.reserve(200);

  // 获取 PageSize 和地址粒度
  SYSTEM_INFO sysInfo = { 0 };
  GetSystemInfo(&sysInfo);

  // 定义基本的内存结构
  const char* p = (const char*)sysInfo.lpMinimumApplicationAddress;
  MEMORY_BASIC_INFORMATION memInfo = { 0 };

  // 开始遍历内存
  while (p < sysInfo.lpMaximumApplicationAddress)
  {
    // 获取进程虚拟内存块缓冲区字节数
    size_t size = VirtualQueryEx(
      hProcess,                             // 进程句柄
      p,                                    // 要查询内存块的基地址指针
      &memInfo,                             // 接收内存块信息的 MEMORY_BASIC_INFORMATION 对象
      sizeof(MEMORY_BASIC_INFORMATION32)    // 缓冲区大小
      );

    if (size != sizeof(MEMORY_BASIC_INFORMATION32))
    {
      break;
    }

    // 将内存块信息追加到容器内
    memories.push_back(memInfo);

    // 移动指针
    p += memInfo.RegionSize;
  }

  // 容器大于0则返回
  return memories.size() > 0;
}

int main(int argc, char* argv[])
{
  // 存放进程内存块的数组
  vector<MEMORY_BASIC_INFORMATION> vec;

  // 打开自身进程
  HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());

  // 遍历该进程的内存
  if (ScanProcessMemory(handle, vec))
  {
    printf("------------------------------------------------------------------------ \n");
    printf("开始地址 \t 结束地址 \t 大小 \t 状态 \t 内存类型 \t 页面属性 \n");
    printf("------------------------------------------------------------------------ \n");
    
    // 此处循环遍历结构
    for (int i = 0; i < vec.size(); i++)
    {
      printf("0x%08X \t 0x%08X \t %8d K \t ", vec[i].BaseAddress, ((DWORD)vec[i].BaseAddress + (DWORD)vec[i].RegionSize), vec[i].RegionSize >> 10);

      switch (vec[i].State)
      {
      case MEM_FREE: printf("空闲 \t"); break;
      case MEM_RESERVE: printf("保留 \t"); break;
      case MEM_COMMIT: printf("提交 \t"); break;
      default: printf("未知 \t"); break;
      }

      switch (vec[i].Type)
      {
      case MEM_PRIVATE: printf("私有  \t"); break;
      case MEM_MAPPED: printf("映射  \t"); break;
      case MEM_IMAGE: printf("镜像  \t"); break;
      default: printf("未知  \t"); break;
      }

      if (vec[i].Protect == 0)
      {
        printf("---");
      }
      else if (vec[i].Protect & PAGE_EXECUTE)
      {
        printf("E--");
      }
      else if (vec[i].Protect & PAGE_EXECUTE_READ)
      {
        printf("ER-");
      }
      else if (vec[i].Protect & PAGE_EXECUTE_READWRITE)
      {
        printf("ERW");
      }
      else if (vec[i].Protect & PAGE_READONLY)
      {
        printf("-R-");
      }
      else if (vec[i].Protect & PAGE_READWRITE)
      {
        printf("-RW");
      }
      else if (vec[i].Protect & PAGE_WRITECOPY)
      {
        printf("WCOPY");
      }
      else if (vec[i].Protect & PAGE_EXECUTE_WRITECOPY)
      {
        printf("EWCOPY");
      }
      printf("\n");
    }
  }
  system("pause");
  return 0;
}

读者可编译并自行运行上述代码,观察输出效果其与第一个案例中的效果保持一致,此处仅仅只是通过容器中转了参数传递,输出效果图如下所示;

对于内存块中的范围区间同样可实现继续查询,例如在开始地址0x5DF00000-0x5DF01000这个内存区间内,可能灰灰划分为更多的子块,当Basicinfo.State内存属性中的子块属性为MEM_COMMIT时,我们还可以继续调用VirtualQuery函数对这个大内存块内的子内存块进行更加细致的解析效果,这段代码如下所示;

#include <iostream>
#include <Windows.h>

int main(int argc, char* argv[])
{
  DWORD Addres = 0, Size = 0;
  MEMORY_BASIC_INFORMATION Basicinfo = {};

  // 遍历进程所有分页, 输出内容
  while (VirtualQuery((LPCVOID)Addres, &Basicinfo, sizeof(MEMORY_BASIC_INFORMATION)))
  {
    Size = Basicinfo.RegionSize;

    printf("[+] 开始地址: 0x%08X \t 结束地址: 0x%08X \t 大小: %7d K \t 类型: ",
      Basicinfo.BaseAddress, ((DWORD)Basicinfo.BaseAddress + (DWORD)Basicinfo.RegionSize), Basicinfo.RegionSize >> 10);

    switch (Basicinfo.Type)
    {
    case MEM_PRIVATE:   printf("私有  \t"); break;
    case MEM_MAPPED:    printf("映射  \t"); break;
    case MEM_IMAGE:     printf("镜像  \t"); break;
    default:      printf("未知  \t"); break;
    }

    printf(" \t 状态: ");
    switch (Basicinfo.State)
    {
    case MEM_FREE:      printf("空闲 \n"); break;
    case MEM_RESERVE:   printf("保留 \n"); break;
    case MEM_COMMIT:    printf("提交 \n"); break;
    default:      printf("未知 \n"); break;
    }

    // 如果是提交状态的内存区域,那么遍历所有块中的信息
    if (Basicinfo.State == MEM_COMMIT)
    {
      // 遍历所有基址是 Address
      LPVOID BaseBlockAddress = (LPVOID)Addres;
      DWORD BlockAddress = Addres;
      DWORD dwBlockSize = 0;

      // 遍历大内存块中的小内存块
      while (VirtualQuery((LPVOID)BlockAddress, &Basicinfo, sizeof(Basicinfo)))
      {
        if (BaseBlockAddress != Basicinfo.AllocationBase)
        {
          break;
        }
        printf("[*] ---> 块地址: 0x%08X \t ", BlockAddress);
        // 查看内存状态,映射方式
        switch (Basicinfo.Type)
        {
        case MEM_PRIVATE:   printf("私有  \t "); break;
        case MEM_MAPPED:    printf("映射  \t "); break;
        case MEM_IMAGE:     printf("镜像  \t "); break;
        default:      printf("未知  \t "); break;
        }

        if (Basicinfo.Protect == 0)
          printf("---");
        else if (Basicinfo.Protect & PAGE_EXECUTE)
          printf("E--");
        else if (Basicinfo.Protect & PAGE_EXECUTE_READ)
          printf("ER-");
        else if (Basicinfo.Protect & PAGE_EXECUTE_READWRITE)
          printf("ERW");
        else if (Basicinfo.Protect & PAGE_READONLY)
          printf("-R-");
        else if (Basicinfo.Protect & PAGE_READWRITE)
          printf("-RW");
        else if (Basicinfo.Protect & PAGE_WRITECOPY)
          printf("WCOPY");
        else if (Basicinfo.Protect & PAGE_EXECUTE_WRITECOPY)
          printf("EWCOPY");
        printf("\n");

        // 计算所有相同块大小
        dwBlockSize += Basicinfo.RegionSize;
        // 累加内存块的位置
        BlockAddress += Basicinfo.RegionSize;
      }
      // 有可能大小为空
      Size = dwBlockSize ? dwBlockSize : Basicinfo.RegionSize;
    }
    // 下一个区域内存信息
    Addres += Size;
  }

  system("pause");
  return 0;
}

当上述代码运行后,我们就可以获取到当前内存中有多少个内存块,以及每一个内存块的属性信息,如下图所示;

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/c09766a2.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

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

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

相关文章

【C++】STL之适配器---用deque实现栈和队列

目录 前言 一、deque 1、deque 的原理介绍 2、deque 的底层结构 3、deque 的迭代器 4、deque 的优缺点 4.1、优点 4.2、缺点 二、stack 的介绍和使用 1、stack 的介绍 2、stack 的使用 3、stack 的模拟实现 三、queue 的介绍和使用 1、queue 的介绍 2、queue 的使用 3、qu…

​专业图像处理软件 Photoshop 2023 mac版本更新(ps2023中文)

​Photoshop 2023 mac是一款图像编辑和图形设计软件&#xff0c;广泛应用于专业人士和爱好者。它提供了许多工具和功能&#xff0c;用于创建、编辑和增强数字图像&#xff0c;包括图层、蒙版、滤镜和各种选择工具。Photoshop还支持多种文件格式&#xff0c;包括psD、JPEG、PNG和…

APP开发者如何运用积分墙广告,提升APP应用下载和用户留存?

“积分墙”移动广告通过在应用内展示各种积分任务&#xff0c;鼓励用户完成任务以获得积分奖励&#xff0c;从而增加应用的曝光度和下载量。 一、什么是积分墙&#xff1f; 积分墙是一种第三方移动广告平台。开发者可以在这类平台上发布任务&#xff08;如下载安装 App、注册…

JavaScript - canvas - 放大镜

效果 示例 项目结构&#xff1a; 源码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>放大镜</title><style type"text/css">div {width: 200px;height: 200px;display: inline-bl…

Java集成支付宝沙箱支付,详细教程(SpringBoot完整版)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、开发前准备&#xff1f;二、使用步骤1、引入库2、配置在 application.yml 里面进行配置&#xff1a;3、alipay的java配置&#xff1a;AplipayConfig.java4、支付…

不要再滥用可选链运算符(?.)啦!

可选链运算符(?.)&#xff0c;大家都很熟悉了&#xff0c;直接看个例子&#xff1a; const result obj?.a?.b?.c?.d 很简单例子&#xff0c;上面代码?前面的属性如果是空值&#xff08;null或undefined&#xff09;&#xff0c;则result值是undefined&#xff0c;反之…

企业架构LNMP学习笔记58

开始学习Tomcat&#xff1a; 学习目标和内容&#xff1a; 1&#xff09;能够描述Tomcat的使用场景&#xff1b; 2&#xff09;能够简单描述Tomcat的工作原理&#xff1b; 3&#xff09;能够实现部署安装Tomcat&#xff1b; 4&#xff09;能够实现和配置Tomcat的Server服务…

如何判断自己的服务器是不是中了勒索病毒,勒索病毒解密数据恢复

勒索病毒日益横行&#xff0c;给企业的生产经营和个人数据造成了极大的影响。但是对单一的企业和个人而言&#xff0c;遭受勒索病毒攻击毕竟是低频率事件&#xff0c;这也导致很多人中招之后&#xff0c;无法判断出具体的问题&#xff0c;进而没有及时采取应对措施&#xff0c;…

【谢希尔 计算机网络】第3章 数据链路层

数据链路层 数据链路层的地位 网络中的主机、路由器等都必须实现数据链路层局域网中的主机、交换机等都必须实现数据链路层不同链路层可能采用不同的数据链路层协议 数据链路层信道类型 点对点信道 使用一对一的点对点通信方式广播通信 必须使用专用的共享系电脑协议来协调这些…

【Java 基础篇】Java多线程实现文件上传详解

文件上传是Web应用程序中常见的功能之一&#xff0c;用户可以通过网页将文件从本地计算机上传到服务器。在处理大文件或多用户并发上传的情况下&#xff0c;为了提高性能和用户体验&#xff0c;常常使用多线程来实现文件上传功能。本文将详细介绍如何使用Java多线程实现文件上传…

【RabbitMQ实战】docker 安装RabbitMQ(bitnami)

一、搜索镜像 [rootlocalhost ~]# docker search rabbitmq NAME DESCRIPTION STARS OFFICIAL AUTOMATED rabbitmq RabbitMQ is an open source multi-pro…

携手走过四年,极智嘉(Geek+)赋能上海西门子开关智慧物流升级

日前&#xff0c;上海西门子开关有限公司迎来了工厂30周年庆典暨客户开放日活动&#xff0c;全球仓储机器人引领者极智嘉(Geek)荣幸作为优秀供应商代表受邀参会&#xff0c;见证其突破革新、数智飞跃的三十载辉煌历程。 客户开放日活动中&#xff0c;上海西门子开关有限公司开…

【SpringCloud】微服务技术栈入门2 - Nacos框架与Feign

目录 Nacos下载 Nacos 并运行配置 NacosNacos 集群Nacos 负载均衡Nacos 环境隔离Nacos 注册细节Nacos 更多配置项快速上手自动更新 Feign取代 RestTemplateFeign 自定义配置性能优化 Nacos 下载 Nacos 并运行 首先下载对应的 release 包&#xff0c;主要要选择已经打包编译好…

链表的回文判断

思路: 找中间节点–>逆置->比较 代码&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/struct ListNode* middleNode(struct ListNode* head) { struct ListNode*slowhead; struct ListNode*f…

maven多模块依赖包程序包xxx不存在

背景 rpc-common 被 rpc-server、rpc-client依赖 项目地址 https://github.com/pjmike/springboot-rpc-demo mvn clean install 打包时报错 报错信息 程序包xxxx不存在 找不到符号 原因分析 原因还不清楚&#xff0c;网友们帮解答一下 解决 主pom.xml 添加 <packaging…

23个销量最高的3D扫描仪【2023】

如果你可以 3D 扫描它&#xff0c;你就可以 3D 打印它。 市场上 3D 扫描仪的种类和质量非常丰富&#xff0c;机器尺寸、功能和价格各异。 这样的选择虽然本身是一件很棒的事情&#xff0c;但也会让从无用的东西中挑选出宝石成为一件苦差事。 推荐&#xff1a;用 NSDT编辑器 快速…

如何在linux定时备份opengauss数据库(linux核心至少在GLIBC_2.34及以上)

前提环境&#xff0c;linux的核心至少在GLIBC_2.34及以上才能使用。 查看linux的glibc版本的命令如下 strings /lib64/libc.so.6 | grep GLIBC 如下图 或者用ldd --version 如下图 在官网下载对应的依赖包&#xff0c; 只需要这个lib文件即可&#xff0c;将这个包放在lin…

拓世法宝|短视频带货风潮,数字人教育书单号成销售黑马

Z世代的爸妈&#xff0c;正在搞一种很新的育儿方式。 “躺平式”带娃、“用魔法打败魔法”等新时代育儿方式频频登上热搜&#xff0c;作为与互联网共同成长起来的一代&#xff0c;Z世代父母们更热衷于通过网络攻略获得和分享知识和经验&#xff0c;更注重个性的养育方式&#…

ChatGPT所引用的参考文献根本不存在?如何正确使用AI工具?

从ChatGPT推出以来&#xff0c;一直都是一个热点话题&#xff0c;尤其是在高等教育领域&#xff0c;大家展开了非常激烈的讨论。 巴斯大学对ChatGPT进行测试时发现&#xff0c;ChatGPT生成的论文&#xff0c;其参考文献的引用格式看起来很完美&#xff0c;有作者姓名和期刊名称…

文件、预处理、位运算

10.2 数据文件概述 10.2.1 ASCII文件与二进制文件 ASCII文件就是“将需要保存到文件的信息使用ASCII字符表示&#xff0c;然后按照顺序将每个字符的ASCII码存储到文件中”。ASCII文件的优点是编码方式公开&#xff0c;可以被其它的文本编辑器打开&#xff1b;其缺点是效率比较…