驱动开发:内核物理内存寻址读写

news2024/11/22 7:04:48

在某些时候我们需要读写的进程可能存在虚拟内存保护机制,在该机制下用户的CR3以及MDL读写将直接失效,从而导致无法读取到正确的数据,本章我们将继续研究如何实现物理级别的寻址读写。

首先,驱动中的物理页读写是指在驱动中直接读写物理内存页(而不是虚拟内存页)。这种方式的优点是它能够更快地访问内存,因为它避免了虚拟内存管理的开销,通过直接读写物理内存,驱动程序可以绕过虚拟内存的保护机制,获得对系统中内存的更高级别的访问权限。

想要实现物理页读写,第一步则是需要找到UserDirectoryTableBase的实际偏移地址,你一定会问这是个什么?别着急,听我来慢慢解释;

在操作系统中,每个进程都有一个KPROCESS结构体,它是进程的内部表示。该结构体中包含了一些重要的信息,包括UserDirectoryTableBase字段,它指向进程的页表目录表(Page Directory Table),也称为DirectoryTable页目录表。

Page Directory Table是一种数据结构,它在虚拟内存管理中起着重要的作用。它被用来存储将虚拟地址映射到物理地址的映射关系,其内部包含了一些指向页表的指针,每个页表中又包含了一些指向物理页面的指针。这些指针一起构成了一个树形结构,它被称为页表树(Page Table Tree)

kd> dt _KPROCESS
ntdll!_KPROCESS
+0x278 UserTime         : Uint4B
+0x27c ReadyTime        : Uint4B
+0x280 UserDirectoryTableBase : Uint8B
+0x288 AddressPolicy    : UChar
+0x289 Spare2           : [71] UChar

#define GetDirectoryTableOffset 0x280

UserDirectoryTableBase字段包含了进程的页表树的根节点的物理地址,通过它可以找到进程的页表树,从而实现虚拟内存的管理。在WinDbg中,通过输入dt _KPROCESS可以查看进程的KPROCESS结构体的定义,从而找到UserDirectoryTableBase字段的偏移量,这样可以获取该字段在内存中的地址,进而获取DirectoryTable的地址。不同操作系统的KPROCESS结构体定义可能会有所不同,因此它们的UserDirectoryTableBase字段的偏移量也会不同。

通过上述原理解释,我们可知要实现物理页读写需要实现一个转换函数,因为在应用层传入的还是一个虚拟地址,通过TransformationCR3函数即可实现将虚拟地址转换到物理地址,函数内部实现了从虚拟地址到物理地址的转换过程,并返回物理地址。

// 从用户层虚拟地址切换到物理页地址的函数
// 将 CR3 寄存器的末尾4个比特清零,这些比特是用于对齐的,不需要考虑
/*
    参数 cr3:物理地址。
    参数 VirtualAddress:虚拟地址。
*/
ULONG64 TransformationCR3(ULONG64 cr3, ULONG64 VirtualAddress)
{
  cr3 &= ~0xf;
  // 获取页面偏移量
  ULONG64 PAGE_OFFSET = VirtualAddress & ~(~0ul << 12);

  // 读取虚拟地址所在的三级页表项
  SIZE_T BytesTransferred = 0;
  ULONG64 a = 0, b = 0, c = 0;

  ReadPhysicalAddress((PVOID)(cr3 + 8 * ((VirtualAddress >> 39) & (0x1ffll))), &a, sizeof(a), &BytesTransferred);

  // 如果 P(存在位)为0,表示该页表项没有映射物理内存,返回0
  if (~a & 1)
  {
    return 0;
  }

  // 读取虚拟地址所在的二级页表项
  ReadPhysicalAddress((PVOID)((a & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 30) & (0x1ffll))), &b, sizeof(b), &BytesTransferred);

  // 如果 P 为0,表示该页表项没有映射物理内存,返回0
  if (~b & 1)
  {
    return 0;
  }

  // 如果 PS(页面大小)为1,表示该页表项映射的是1GB的物理内存,直接计算出物理地址并返回
  if (b & 0x80)
  {
    return (b & (~0ull << 42 >> 12)) + (VirtualAddress & ~(~0ull << 30));
  }

  // 读取虚拟地址所在的一级页表项
  ReadPhysicalAddress((PVOID)((b & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 21) & (0x1ffll))), &c, sizeof(c), &BytesTransferred);

  // 如果 P 为0,表示该页表项没有映射物理内存,返回0
  if (~c & 1)
  {
    return 0;
  }
  // 如果 PS 为1,表示该页表项映射的是2MB的物理内存,直接计算出物理地址并返回
  if (c & 0x80)
  {
    return (c & ((~0xfull << 8) & 0xfffffffffull)) + (VirtualAddress & ~(~0ull << 21));
  }
  // 读取虚拟地址所在的零级页表项,计算出物理地址并返回
  ULONG64 address = 0;
  ReadPhysicalAddress((PVOID)((c & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 12) & (0x1ffll))), &address, sizeof(address), &BytesTransferred);
  address &= ((~0xfull << 8) & 0xfffffffffull);
  if (!address)
  {
    return 0;
  }

  return address + PAGE_OFFSET;
}

这段代码将输入的CR3值和虚拟地址作为参数,并将CR3值和虚拟地址的偏移量进行一系列计算,最终得出物理地址。

其中,CR3是存储页表的物理地址,它保存了虚拟地址到物理地址的映射关系。该函数通过读取CR3中存储的页表信息,逐级访问页表,直到找到对应的物理地址。

该函数使用虚拟地址的高9位确定页表的索引,然后通过读取对应的页表项,得到下一级页表的物理地址。该过程重复执行,直到读取到页表的最后一级,得到物理地址。

最后,该函数将物理地址的低12位与虚拟地址的偏移量进行OR运算,得到最终的物理地址,并将其返回。

需要注意的是,该函数还会进行一些错误处理,例如在读取页表项时,如果该项没有被设置为有效,函数将返回0,表示无法访问对应的物理地址。

此时用户已经获取到了物理地址,那么读写就变得很容易了,当需要读取数据时调用ReadPhysicalAddress函数,其内部直接使用MmCopyMemory对内存进行拷贝即可,而对于写入数据而言,需要通过调用MmMapIoSpace先将物理地址转换为一个用户空间的虚拟地址,然后再通过RtlCopyMemory向内部拷贝数据即可实现写入,这三段代码的封装如下所示;

#include <ntifs.h>
#include <windef.h>

#define GetDirectoryTableOffset 0x280
#define bit64 0x28
#define bit32 0x18

// 读取物理内存封装
// 这段代码实现了将物理地址映射到内核空间,然后将物理地址对应的数据读取到指定的缓冲区中。
/*
    address:需要读取的物理地址;
    buffer:读取到的数据需要保存到的缓冲区;
    size:需要读取的数据大小;
    BytesTransferred:实际读取到的数据大小。
*/
NTSTATUS ReadPhysicalAddress(PVOID address, PVOID buffer, SIZE_T size, SIZE_T* BytesTransferred)
{
  MM_COPY_ADDRESS Read = { 0 };
  Read.PhysicalAddress.QuadPart = (LONG64)address;
  return MmCopyMemory(buffer, Read, size, MM_COPY_MEMORY_PHYSICAL, BytesTransferred);
}

// 写入物理内存
// 这段代码实现了将数据写入物理地址的功能
/*
    参数 address:要写入的物理地址。
    参数 buffer:要写入的数据缓冲区。
    参数 size:要写入的数据长度。
    参数 BytesTransferred:实际写入的数据长度。
*/
NTSTATUS WritePhysicalAddress(PVOID address, PVOID buffer, SIZE_T size, SIZE_T* BytesTransferred)
{
  if (!address)
  {
    return STATUS_UNSUCCESSFUL;
  }

  PHYSICAL_ADDRESS Write = { 0 };
  Write.QuadPart = (LONG64)address;
    
    // 将物理空间映射为虚拟空间
  PVOID map = MmMapIoSpace(Write, size, (MEMORY_CACHING_TYPE)PAGE_READWRITE);

  if (!map)
  {
    return STATUS_UNSUCCESSFUL;
  }

    // 开始拷贝数据
  RtlCopyMemory(map, buffer, size);
  *BytesTransferred = size;
  MmUnmapIoSpace(map, size);
  return STATUS_SUCCESS;
}

// 从用户层虚拟地址切换到物理页地址的函数
// 将 CR3 寄存器的末尾4个比特清零,这些比特是用于对齐的,不需要考虑
/*
    参数 cr3:物理地址。
    参数 VirtualAddress:虚拟地址。
*/
ULONG64 TransformationCR3(ULONG64 cr3, ULONG64 VirtualAddress)
{
  cr3 &= ~0xf;
  // 获取页面偏移量
  ULONG64 PAGE_OFFSET = VirtualAddress & ~(~0ul << 12);

  // 读取虚拟地址所在的三级页表项
  SIZE_T BytesTransferred = 0;
  ULONG64 a = 0, b = 0, c = 0;

  ReadPhysicalAddress((PVOID)(cr3 + 8 * ((VirtualAddress >> 39) & (0x1ffll))), &a, sizeof(a), &BytesTransferred);

  // 如果 P(存在位)为0,表示该页表项没有映射物理内存,返回0
  if (~a & 1)
  {
    return 0;
  }

  // 读取虚拟地址所在的二级页表项
  ReadPhysicalAddress((PVOID)((a & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 30) & (0x1ffll))), &b, sizeof(b), &BytesTransferred);

  // 如果 P 为0,表示该页表项没有映射物理内存,返回0
  if (~b & 1)
  {
    return 0;
  }

  // 如果 PS(页面大小)为1,表示该页表项映射的是1GB的物理内存,直接计算出物理地址并返回
  if (b & 0x80)
  {
    return (b & (~0ull << 42 >> 12)) + (VirtualAddress & ~(~0ull << 30));
  }

  // 读取虚拟地址所在的一级页表项
  ReadPhysicalAddress((PVOID)((b & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 21) & (0x1ffll))), &c, sizeof(c), &BytesTransferred);

  // 如果 P 为0,表示该页表项没有映射物理内存,返回0
  if (~c & 1)
  {
    return 0;
  }
  // 如果 PS 为1,表示该页表项映射的是2MB的物理内存,直接计算出物理地址并返回
  if (c & 0x80)
  {
    return (c & ((~0xfull << 8) & 0xfffffffffull)) + (VirtualAddress & ~(~0ull << 21));
  }
  // 读取虚拟地址所在的零级页表项,计算出物理地址并返回
  ULONG64 address = 0;
  ReadPhysicalAddress((PVOID)((c & ((~0xfull << 8) & 0xfffffffffull)) + 8 * ((VirtualAddress >> 12) & (0x1ffll))), &address, sizeof(address), &BytesTransferred);
  address &= ((~0xfull << 8) & 0xfffffffffull);
  if (!address)
  {
    return 0;
  }

  return address + PAGE_OFFSET;
}

有了如上封装,那么我们就可以实现驱动读写了,首先我们实现驱动读取功能,如下这段代码是Windows驱动程序的入口函数DriverEntry,主要功能是读取指定进程的虚拟地址空间中指定地址处的4个字节数据。

代码首先通过 PsLookupProcessByProcessId 函数获取指定进程的 EPROCESS 结构体指针。然后获取该进程的 CR3值,用于将虚拟地址转换为物理地址。接下来,循环读取指定地址处的 4 个字节数据,每次读取 PAGE_SIZE 大小的物理内存数据。最后输出读取到的数据,并关闭对 EPROCESS 结构体指针的引用。

需要注意的是,该代码并没有进行有效性检查,如没有检查读取的地址是否合法、读取的数据是否在用户空间,因此存在潜在的风险。另外,该代码也没有考虑内核模式下访问用户空间数据的问题,因此也需要进行进一步的检查和处理。

// 驱动卸载例程
extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
  UNREFERENCED_PARAMETER(pDriver);
  DbgPrint("Uninstall Driver \n");
}

// 驱动入口地址
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
{
  DbgPrint("Hello LyShark \n");

  // 通过进程ID获取eprocess
  PEPROCESS pEProcess = NULL;
  NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)4116, &pEProcess);

  if (NT_SUCCESS(Status) && pEProcess != NULL)
  {

    ULONG64 TargetAddress = 0x401000;
    SIZE_T TargetSize = 4;
    SIZE_T read = 0;

    // 分配读取空间
    BYTE* ReadBuffer = (BYTE *)ExAllocatePool(NonPagedPool, 1024);

    // 获取CR3用于转换
    PUCHAR Var = reinterpret_cast<PUCHAR>(pEProcess);
    ULONG64 CR3 = *(ULONG64*)(Var + bit64);
    if (!CR3)
    {
      CR3 = *(ULONG64*)(Var + GetDirectoryTableOffset);
    }

    DbgPrint("[CR3] 寄存器地址 = 0x%p \n", CR3);

    while (TargetSize)
    {
      // 开始循环切换到CR3
      ULONG64 PhysicalAddress = TransformationCR3(CR3, TargetAddress + read);
      if (!PhysicalAddress)
      {
        break;
      }

      // 读取物理内存
      ULONG64 ReadSize = min(PAGE_SIZE - (PhysicalAddress & 0xfff), TargetSize);
      SIZE_T BytesTransferred = 0;

      // reinterpret_cast 强制转为PVOID类型
      Status = ReadPhysicalAddress(reinterpret_cast<PVOID>(PhysicalAddress), reinterpret_cast<PVOID>((PVOID *)ReadBuffer + read), ReadSize, &BytesTransferred);
      TargetSize -= BytesTransferred;
      read += BytesTransferred;

      if (!NT_SUCCESS(Status))
      {
        break;
      }

      if (!BytesTransferred)
      {
        break;
      }
    }

    // 关闭引用
    ObDereferenceObject(pEProcess);

    // 输出读取字节
    for (size_t i = 0; i < 4; i++)
    {
      DbgPrint("[读入字节 [%d] ] => 0x%02X \n", i, ReadBuffer[i]);
    }
  }

  // 关闭引用
  UNREFERENCED_PARAMETER(path);

  // 卸载驱动
  pDriver->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}

编译并运行上述代码片段,则会读取进程ID为41160x401000处的地址数据,并以字节的方式输出前四位,输出效果图如下所示;

写出数据与读取数据基本一致,只是调用方法从ReadPhysicalAddress变为了WritePhysicalAddress其他的照旧,但需要注意的是读者再使用写出时需要自行填充一段堆用于存储需要写出的字节集。

// 驱动卸载例程
extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
  UNREFERENCED_PARAMETER(pDriver);
  DbgPrint("Uninstall Driver \n");
}

// 驱动入口地址
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
{
  DbgPrint("Hello LyShark \n");

  // 物理页写
  PEPROCESS pEProcess = NULL;
  NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)4116, &pEProcess);

  // 判断pEProcess是否有效
  if (NT_SUCCESS(Status) && pEProcess != NULL)
  {
    ULONG64 TargetAddress = 0x401000;
    SIZE_T TargetSize = 4;
    SIZE_T read = 0;

    // 申请空间并填充写出字节0x90
    BYTE* ReadBuffer = (BYTE *)ExAllocatePool(NonPagedPool, 1024);

    for (size_t i = 0; i < 4; i++)
    {
      ReadBuffer[i] = 0x90;
    }

    // 获取CR3用于转换
    PUCHAR Var = reinterpret_cast<PUCHAR>(pEProcess);
    ULONG64 CR3 = *(ULONG64*)(Var + bit64);
    if (!CR3)
    {
      CR3 = *(ULONG64*)(Var + GetDirectoryTableOffset);
      // DbgPrint("[CR3] 寄存器地址 = 0x%p \n", CR3);
    }

    while (TargetSize)
    {
      // 开始循环切换到CR3
      ULONG64 PhysicalAddress = TransformationCR3(CR3, TargetAddress + read);
      if (!PhysicalAddress)
      {
        break;
      }

      // 写入物理内存
      ULONG64 WriteSize = min(PAGE_SIZE - (PhysicalAddress & 0xfff), TargetSize);
      SIZE_T BytesTransferred = 0;
      Status = WritePhysicalAddress(reinterpret_cast<PVOID>(PhysicalAddress), reinterpret_cast<PVOID>(ReadBuffer + read), WriteSize, &BytesTransferred);
      TargetSize -= BytesTransferred;
      read += BytesTransferred;

      // DbgPrint("[写出数据] => %d | %0x02X \n", WriteSize, ReadBuffer + read);
      if (!NT_SUCCESS(Status))
      {
        break;
      }

      if (!BytesTransferred)
      {
        break;
      }
    }

    // 关闭引用
    ObDereferenceObject(pEProcess);
  }

  // 关闭引用
  UNREFERENCED_PARAMETER(path);

  // 卸载驱动
  pDriver->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}

如上代码运行后,会向进程ID为41160x401000处写出4字节的0x90机器码,读者可通过第三方工具验证内存,输出效果如下所示;

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

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

相关文章

LiveGBS流媒体平台GB/T28181功能-海康大华宇视华为NVR等4G摄像头自带物联网卡注册国标平台后看不到设备的时候如何排查及抓包

LiveGBS流媒体平台GB/T28181功能-海康大华宇视华为NVR等4G摄像头自带物联网卡注册国标平台后看不到设备的时候如何排查及抓包 1、设备注册后查看不到1.1、是否是4G|5G摄像头1.2、关闭萤石云1.3、防火墙排查1.4、端口排查1.5、IP地址排查1.6、设备TCP/IP配置排查1.7、设备多网卡…

【Nexus】Maven从Nexus中下载jar包

目录 一、前言二、配置Apache Maven1、在Maven的settings.xml中添加一个镜像配置&#xff0c;并覆盖中央仓库的默认配置 二、创建Maven项目&#xff0c;配置pom文件拉取Nexus中的jar包1、确定配置的Maven的settings.xml是否是上一步修改的settings.xml文件&#xff0c;以及repo…

规划地类、用途分区、空间管制区代码对应表

规划地类、用途分区、空间管制区代码对应表 —the—end—

UE5 与 C++ 入门教程·第一课:角色与 Enhanced Input

本文主要围绕 UE5 新的输入系统&#xff0c;手把手从 0 搭建 Unreal 项目&#xff0c;实现角色的基础移动。 重要提示&#xff1a;众所周知&#xff0c;C 属于编译型语言&#xff0c;因此动态灵活性不足&#xff0c;不过执行效率高&#xff0c;而蓝图简单灵活&#xff0c;却执行…

探索TCC:释放高可用性和弹性事务的潜力

1、TCC简介 分布式事务是指在分布式系统中&#xff0c;多个服务之间需要保证数据的一致性和完整性的场景。传统的单机事务无法满足分布式系统的需求&#xff0c;因此需要引入一种新的事务模型来解决分布式事务问题。 TCC&#xff08;Try-Confirm-Cancel&#xff09;是一种基于…

MySQL的分库分表

分必要不要分库分表&#xff08;通过优化之后还明显影响业务再分&#xff0c;可以通过监控慢查询确定&#xff09; 分库分表的一般条件:单表数据量超过1000w&#xff08;阿里应该是说5000w&#xff09;或者单表数据文件(.ibd)超过20GB&#xff0c;这个很重要&#xff0c;&…

点云配准综述一篇综述《A comprehensive survey on point cloud registration》(翻译)

参照了 2021最新关于点云配准的全面综述 - 知乎&#xff0c;并且加了些自己翻译&#xff0c;全篇的内容可能稍有删减。主要作为个人笔记&#xff0c;阅读了几篇综述&#xff0c;发现这篇是质量较好的&#xff0c;值得花时间细读。 文章分类 文章将配准方法分为了同源配准和不…

JMeter三大重要组件——线程组、取样器、查看结果数(3)

JMeter三大重要组件 一、JMeter三大重要组件——线程组1、作用&#xff1a;JMeter主要通过线程组来运行用户脚本2、在取样器错误后要执行的动作&#xff1a;3、线程属性3、调度器4、setUp线程组和tearDown线程组 二、JMeter三大重要组件——取样器1、基本a、自动重定向和跟随重…

Obsidian多端同步插件LiveSync

网友 Leo 和 Paco反馈&#xff0c;群晖升级到 DSM7.2 &#xff0c;注册表可以搜索镜像&#xff0c;根据 Leo 贴的 /var/packages/Docker/etc/dockerd.json 的内容&#xff0c;DSM7.2 应该是使用了 https://docker.nju.edu.cn 作为注册表镜像&#xff0c;但老苏测试过下面几种情…

易基因:易基因近期染色质免疫共沉淀测序(ChIP-seq)研究成果|项目集锦

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 在生物学研究中&#xff0c;DNA与蛋白质之间的互作&#xff08;DNA-Protein Interactions&#xff0c;DPIs&#xff09;是至关重要的&#xff0c;参与基因的表达、调控、复制、重组和修复…

m 序列(最长线性反馈移位寄存器序列)详解

本专栏包含信息论与编码的核心知识&#xff0c;按知识点组织&#xff0c;可作为教学或学习的参考。markdown版本已归档至【Github仓库&#xff1a;https://github.com/timerring/information-theory 】或者公众号【AIShareLab】回复 信息论 获取。 文章目录 m 序列 (最长线性反…

Git进阶系列 | 8. 用Reflog恢复丢失的提交

Git是最流行的代码版本控制系统&#xff0c;这一系列文章介绍了一些Git的高阶使用方式&#xff0c;从而帮助我们可以更好的利用Git的能力。本系列一共8篇文章&#xff0c;这是最后一篇。原文&#xff1a;Using the Reflog to Restore Lost Commits[1] “Reflog”是Git不太为人所…

常见的未授权漏洞批量检测工具

常见的未授权漏洞检测 命令行版已放出支持多线程&#xff0c;批量扫描&#xff0c;指定服务扫描&#xff0c;命令行版地址https://github.com/xk11z/unauthorized_com GUI版unauthorizedV2已更新&#xff0c;可批量ip检测导出结果 项目包含 1 、FTP 未授权访问&#xff08…

SadTalker AI模型使用一张图片与一段音频便可以自动生成视频

SadTalker模型是一个使用图片与音频文件自动合成人物说话动画的开源模型,我们自己给模型一张图片以及一段音频文件,模型会根据音频文件把传递的图片进行人脸的相应动作,比如张嘴,眨眼,移动头部等动作。 SadTalker,它从音频中生成 3DMM 的 3D 运动系数(头部姿势、表情),…

webstorm配置vue开发环境

&#x1f333;&#x1f333;&#x1f333;前言&#xff1a;本文章针对于如何用IDE和webstorm运行一个别人的vue项目进行步骤记录。 &#x1f4d9;参考&#xff1a;(10条消息) idea配置vue开发环境_idea配置vue运行环境_drinkworld的博客-CSDN博客https://blog.csdn.net/drinkwo…

VSCode ssh ubuntu20显示图像界面

1、在vscode中安装 Remote X11(SSH) 2、在本地端安装MobaXterm 点击Settings-->Configurations-->X11&#xff0c;设置如下&#xff1a; 3、在服务端修改 ~/.bashrc文件&#xff0c;在末尾添加 export DISPLAY"192.168.0.201:0.0" 其中引号中内容为本地端IP地…

英伟达股价能否凭借AI进一步上涨到500美元?

来源&#xff1a; 猛兽财经 作者&#xff1a;猛兽财经 猛兽财经在之前的关于英伟达的分析中&#xff08;5月2日&#xff09;&#xff0c;就认为英伟达在人工智能方面的增长潜力还没有完全释放出来&#xff0c;并认为英伟达的股价将会很快涨到300美元&#xff0c;结果到了6月…

代码随想录二刷 day34 | 贪心之1005.K次取反后最大化的数组和 134. 加油站 135. 分发糖果

1005.K次取反后最大化的数组和 题目链接 解题思路&#xff1a; 两次贪心 如何可以让数组和最大呢&#xff1f; 局部最优&#xff1a;让绝对值大的负数变为正数&#xff0c;当前数值达到最大&#xff0c;整体最优&#xff1a;整个数组和达到最大 如何转变K次正负&#xff0c;让…

CASS打印地形图操作

1、打开地形图&#xff0c;如下&#xff1a; 2、在“工程应用”菜单栏中&#xff0c;选择“查询两点距离及方位”&#xff0c;如下&#xff1a; 3、量取地形图的大致范围大小&#xff0c;如下&#xff1a; 读取图上距离&#xff0c;用于设置纸张大小。 4、点击左上角打印图形&…

【算法题】神奇的斐波那契数列(Fibonacci sequence)、青蛙跳台阶问题、矩阵中的路径

神奇的斐波那契数列和青蛙跳台阶问题 一、神奇的斐波那契数列1.1、题目描述1.2、递归算法1.3、迭代法1.4、小结 二、青蛙跳台阶问题2.1、题目描述2.2、思路2.3、动态规划法2.4、小结 三、矩阵中的路径3.1、题目描述3.2、思路3.3、代码实现3.4、小结 总结 一、神奇的斐波那契数列…