CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Native API编程 专栏说明
  • 第1章 原生API(Native API)开发入门
  • 第2章 原生API(Native API)基础
  • 第3章 原生应用程序(Native Applications)
  • 第4章:系统信息
  • 第5章:进程
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
    • 8.1 引言
      • 8.1.1 页状态(Page States)
      • 8.1.2 内存 API(Memory APIs)
    • 8.2 虚拟函数(The Virtual Functions)
      • MemExtendedParameterAddressRequirements
      • 其他参数类型
    • 8.3 内存查询(Querying Memory)
      • 8.3.1 MemoryBasicInformation
      • 8.3.2 MemoryWorkingSetInformation
      • 8.3.3 MemoryMappedFilenameInformation
      • 8.3.4:MemoryRegionInformation和MemoryRegionInformationEx
      • 8.3.5 扩展内存工作集信息(MemoryWorkingSetExInformation)
      • 8.3.6 内存共享提交信息(MemorySharedCommitInformation)
      • 8.3.7 内存镜像信息(MemoryImageInformation)
      • 8.3.8 特权内存基本信息(MemoryPrivilegedBasicInformation)
      • 8.3.9 内存飞地镜像信息(MemoryEnclaveImageInformation)
      • 8.3.10 受限内存基本信息(MemoryBasicInformationCapped)
      • 8.3.11 内存物理连续性信息(MemoryPhysicalContiguityInformation)
      • 8.3.12 内存损坏信息(MemoryBadInformation)和所有进程内存损坏信息(MemoryBadInformationAllProcesses)
    • 8.4 读取和写入(Reading and Writing)
      • 8.4.1 通过远程线程注入DLL
    • 8.5 其他虚拟内存 API(Other Virtual APIs)
    • 8.6 堆(Heaps)
      • 8.6.1 基本堆管理(Basic Heap Management)
      • 8.6.2 创建堆(Creating Heaps)
      • 8.6.3 其他堆函数(Other Heap Functions)
      • 8.6.3.1 堆分配大小(Heap Allocation Size)
      • 8.6.3.2 保护(Protection)
      • 8.6.3.3 锁定(Locking)
      • 8.6.3.4 用户数据(User Data)
      • 8.6.3.5 多次分配(More Allocations)
    • 8.7 堆信息(Heap Information)
      • 8.7.1 堆遍历(Heap Walking)
      • 8.7.2 更多堆信息
      • 8.7.3 堆兼容性信息(HeapCompatibilityInformation,0)
      • 8.7.4 堆损坏时终止进程(HeapEnableTerminationOnCorruption)
      • 8.7.5 堆扩展信息(HeapExtendedInformation)
      • 8.7.5.1 回调函数(Callback)
      • 8.7.6 堆资源优化(HeapOptimizeResources)
      • 8.7.7 堆内存限制(HeapMemoryLimit)
    • 8.8 总结
  • 第9章:I/O
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
目录

第 8 章:内存(第一部分)

# 第 8 章:内存(第一部分)

内存(Memory)对于任何计算机系统而言显然都至关重要。Windows 执行体中的内存管理器组件(Memory Manager component)负责管理一系列页表(page tables),使 CPU 能够正确地将虚拟地址(virtual addresses)转换为物理地址(physical addresses,即内存(RAM)地址)。从 CPU 和内存管理器的角度来看,内存始终以“页(pages)”为单位进行管理。

本章将涵盖以下内容:

  • 引言
  • 虚拟函数
  • 内存查询
  • 读写操作
  • 其他虚拟 API
  • 堆
  • 堆信息

# 8.1 引言

表 8-1 列出了 Windows 支持的各架构对应的页大小(page sizes)。默认页大小为 4KB。

表 8-1:页大小(Page Sizes)

架构(Architecture) 普通(小)页(Normal (Small)) 大页(Large) 巨页(Huge) 备注(Remarks)
x86 4KB 2MB 不支持(N/A) Windows 11 及以上版本不支持
x64 4KB 2MB 1GB 无
ARM 4KB 4MB 不支持(N/A) Windows 11 及以上版本不支持
ARM64 4KB 2MB 1GB 无

简单来说,虚拟内存(Virtual Memory)的含义如下:

  • 虚拟地址到物理地址存在某种映射关系,处理器会透明地遵循该映射。代码仅通过虚拟地址访问内存。
  • 页(page)可能根本不在物理内存中,此时 CPU 会触发页错误异常(page fault exception),由内存管理器处理。如果该页位于某个文件(如页面文件(page file))中,内存管理器会分配一个空闲的物理页,读取数据到该物理页,修正页表,并通知处理器重试。这一过程对调用代码完全透明。

# 8.1.1 页状态(Page States)

进程或内核的虚拟地址空间(virtual address space)中的每个页都可能处于以下状态:

  • 空闲(Free):该页无任何内容,无映射关系。访问此类页会引发访问冲突异常(Access Violation exception)。
  • 已提交(Committed):与空闲状态相反。该页必定可访问,尽管它在不同时刻可能驻留在文件中。但由于页保护冲突(page protection conflicts),它仍可能无法访问。
  • 已保留(Reserved):从访问角度来看与空闲状态相同——该页无任何内容。但该页已被预留用于未来使用,不会被考虑用于新的内存分配。

# 8.1.2 内存 API(Memory APIs)

用户模式(user-mode)中有多个层级的内存 API。最底层是所谓的“虚拟函数(Virtual functions)”,它们功能最强,因为最接近内存管理器。这些函数提供了内存管理器的全部功能,例如提交(committing)和保留(reserving)内存、修改保护属性(changing protection)、使用大页(using large pages)等。其缺点是始终以页为单位工作。例如,若使用这些函数之一分配 100 字节,实际会获得 4KB(一个页的大小);若分配 5KB,实际会返回 8KB。

API 层级如图 8-1 所示。

图 8-1:内存 API 层级(Memory API Layers)

虚拟 API(Virtual API)之上是堆 API(Heap API)。该 API 的目的是高效管理小型或非页大小的内存块。它们会在需要时调用虚拟 API。最后,最高层级的 API 是 C/C++ 运行时的一部分——例如 malloc、free、operator new 等函数。其实现依赖于编译器,但微软的编译器目前使用堆 API(与默认进程堆(default process heap)配合工作)。

# 8.2 虚拟函数(The Virtual Functions)

Windows API 变体包括 VirtualAlloc(Ex)、VirtualFree、VirtualProtect 等。本节将介绍底层的原生 API(native APIs),首先从基本分配函数 NtAllocateVirtualMemory 开始:

NTSTATUS NtAllocateVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _Inout_ PVOID *BaseAddress,
    _In_ ULONG_PTR ZeroBits,
    _Inout_ PSIZE_T RegionSize,
    _In_ ULONG AllocationType,
    _In_ ULONG Protect);
1
2
3
4
5
6
7

该函数在 Windows 驱动程序开发工具包(WDK)中有实际文档说明,其大多数参数与 Windows API 中的 VirtualAllocEx 对应的参数一致。以下是简要说明:

  • ProcessHandle:要在其中进行内存分配的进程的句柄。典型情况是 NtCurrentProcess,但如果该句柄具有 PROCESS_VM_OPERATION 访问掩码(access mask),也可以在其他进程中进行分配。
  • BaseAddress:提供要使用的地址。对于新分配,*BaseAddress 通常设为 NULL,表示告知内存管理器无首选地址。如果相关内存区域已被保留(reserved),则 *BaseAddress 可指定该区域内的某个地址(例如用于提交内存(committing memory))。函数成功返回时,*BaseAddress 指示实际使用的地址——该地址可能会从输入值向下舍入到最近的页边界;若输入时指定为 NULL,则返回内存管理器选择的实际地址。
  • ZeroBits:通常为 0,但可用于影响最终选择的地址。详见第 6 章的说明。
  • *RegionSize:受影响内存区域的大小(以字节为单位)。它可能会向上舍入(取决于 BaseAddress),以对齐到页边界。
  • AllocationType:一组标志(部分标志互斥),指示要执行的操作。最常见的标志如下:
    • MEM_COMMIT:提交内存区域。
    • MEM_RESERVE:保留内存区域。
    • MEM_COMMIT | MEM_RESERVED:同时保留和提交内存区域,用于新分配。
  • Protect:已分配页的保护属性(protection),例如 PAGE_READWRITE、PAGE_READONLY、PAGE_EXECUTE_READWRITE 等。

有关这些参数的更多详细信息,请参阅 Windows SDK 中 VirtualAlloc 的文档。

与 NtAllocateVirtualMemory 对应的逆函数是 NtFreeVirtualMemory:

NTSTATUS NtFreeVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _Inout_ PVOID *BaseAddress,
    _Inout_ PSIZE_T RegionSize,
    _In_ ULONG FreeType);
1
2
3
4
5

前三个参数与 NtAllocateVirtualMemory 相同。但 *BaseAddress 不能为 NULL——它必须指定一个有效的地址,用于解除提交(de-commit)或释放(release,与保留(reserve)相反)内存。地址和内存区域大小将分别向下和向上舍入,使该范围对齐到页边界。最后,最常见的标志如下:

  • MEM_DECOMMIT:解除提交页,使其变为保留状态。
  • MEM_RELEASE:释放页,使其变为空闲状态。内存区域大小必须为 0,且 *BaseAddress 必须是初始调用 NtAllocateVirtualMemory 分配时返回的基地址。

有关更多信息,请参阅 SDK 中 VirtualFree 的文档。

Windows 10 1803 版本及更高版本提供了一个扩展分配函数:

NTSTATUS NtAllocateVirtualMemoryEx(
    _In_ HANDLE ProcessHandle,
    _Inout_ PVOID* BaseAddress,
    _Inout_ PSIZE_T RegionSize,
    _In_ ULONG AllocationType,
    _In_ ULONG PageProtection,
    _Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
    _In_ ULONG ExtParameCount);
1
2
3
4
5
6
7
8

NtAllocateVirtualMemoryEx 是一个扩展函数,允许指定一组可变的扩展参数,这些参数在 WinNt.h 中声明:

typedef enum MEM_EXTENDED_PARAMETER_TYPE {
    MemExtendedParameterInvalidType = 0,
    MemExtendedParameterAddressRequirements,
    MemExtendedParameterNumaNode,
    MemExtendedParameterPartitionHandle,
    MemExtendedParameterUserPhysicalHandle,
    MemExtendedParameterAttributeFlags,
    MemExtendedParameterImageMachine,
    MemExtendedParameterMax
} MEM_EXTENDED_PARAMETER_TYPE, *PMEM_EXTENDED_PARAMETER_TYPE;

#define MEM_EXTENDED_PARAMETER_TYPE_BITS 8

typedef struct DECLSPEC_ALIGN(8) MEM_EXTENDED_PARAMETER {
    struct {
        DWORD64 Type : MEM_EXTENDED_PARAMETER_TYPE_BITS;
        DWORD64 Reserved : 64 - MEM_EXTENDED_PARAMETER_TYPE_BITS;
    };
    union {
        DWORD64 ULong64;
        PVOID Pointer;
        SIZE_T Size;
        HANDLE Handle;
        DWORD ULong;
    };
} MEM_EXTENDED_PARAMETER, *PMEM_EXTENDED_PARAMETER;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Windows API 中的 VirtualAlloc2 会调用 NtAllocateVirtualMemoryEx。

MEM_EXTENDED_PARAMETER_TYPE 中的每个值代表一种自定义类型,其中部分类型在 CreateFileMapping2 的文档中有说明。CreateFileMapping2 仅支持 MemSectionExtendedParameterUserPhysicalFlags 和 MemSectionExtendedParameterNumaNode 值。以下是当前可用的参数类型:

# MemExtendedParameterAddressRequirements

该类型要求提供一个指向以下结构的指针:

typedef struct _MEM_ADDRESS_REQUIREMENTS {
    PVOID LowestStartingAddress;
    PVOID HighestEndingAddress;
    SIZE_T Alignment;
} MEM_ADDRESS_REQUIREMENTS, *PMEM_ADDRESS_REQUIREMENTS;
1
2
3
4
5

这允许指定内存分配的最小和最大地址,以及最小对齐要求(alignment)。

# 其他参数类型

  • MemExtendedParameterNumaNode:指定所需的非统一内存访问节点(NUMA node,在 ULong 成员中)。
  • MemExtendedParameterPartitionHandle:指定所需的内存分区句柄(memory partition handle)(内存分区超出本章范围)。
  • MemExtendedParameterUserPhysicalHandle:指定另一个节对象(section object)的句柄,用于复制与地址窗口扩展(Address Windowing Extensions,AWE)相关的信息,AWE 始终标识物理映射的内存(超出本章范围)。
  • MemExtendedParameterAttributeFlags:指定一组附加标志,其中大多数在 WinNt.h 中定义,如表 8-2 所示:

表 8-2:扩展参数类型(Extended parameters types)

标志(MEM_EXTENDED_PARAMETER_) 描述(Description)
GRAPHICS(1) 分配/映射将用于与图形处理器(GPU)配合工作
NONPAGED(2) 使用始终非分页的 64KB 页
ZERO_PAGES_OPTIONAL(4) 提交内存时不需要零初始化页(Zero pages)
NONPAGED_LARGE(8) 分配/映射将使用大页(2MB)
NONPAGED_HUGE(0x10) 分配/映射将使用巨页(1GB)
SOFT_FAULT_PAGES(0x20) 立即执行软页错误(soft page faults),以减少后续发生的可能性
EC_CODE(0x40) 用于仿真的内存(ARM64 架构)
IMAGE_NO_HPAT(0x80) 用于映像(image)的热补丁(hot-patching)

# 8.3 内存查询(Querying Memory)

NtQueryVirtualMemory API 提供指定进程中虚拟地址空间区域的相关信息:

typedef enum _MEMORY_INFORMATION_CLASS {
    MemoryBasicInformation,                 // MEMORY_BASIC_INFORMATION
    MemoryWorkingSetInformation,            // MEMORY_WORKING_SET_INFORMATION
    MemoryMappedFilenameInformation,        // UNICODE_STRING
    MemoryRegionInformation,                // MEMORY_REGION_INFORMATION
    MemoryWorkingSetExInformation,          // MEMORY_WORKING_SET_EX_INFORMATION
    MemorySharedCommitInformation,          // MEMORY_SHARED_COMMIT_INFORMATION
    MemoryImageInformation,                 // MEMORY_IMAGE_INFORMATION
    MemoryRegionInformationEx,              // MEMORY_REGION_INFORMATION
    MemoryPrivilegedBasicInformation,       // MEMORY_BASIC_INFORMATION
    MemoryEnclaveImageInformation,          // MEMORY_ENCLAVE_IMAGE_INFORMATION
    MemoryBasicInformationCapped,           // 仅 ARM64 架构支持
    MemoryPhysicalContiguityInformation,    // MEMORY_PHYSICAL_CONTIGUITY_INFORMATION
    MemoryBadInformation,                   // MEMORY_BAD_IDENTITY_INFORMATION
    MemoryBadInformationAllProcesses,       // MEMORY_BAD_IDENTITY_INFORMATION
    MaxMemoryInfoClass
} MEMORY_INFORMATION_CLASS;

NTSTATUS NtQueryVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _In_opt_ PVOID BaseAddress,
    _In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
    _Out_writes_bytes_(MemoryInformationLength) PVOID MemoryInformation,
    _In_ SIZE_T MemoryInformationLength,
    _Out_opt_ PSIZE_T ReturnLength);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

如你所见,该函数支持多种信息类,以下将对其进行描述。

# 8.3.1 MemoryBasicInformation

缓冲区类型为MEMORY_BASIC_INFORMATION,在Windows SDK中有相关文档说明。实际上,使用此信息类等同于Windows应用程序编程接口(API)的VirtualQueryEx。它返回地址空间中某一区域的信息,该区域在页面状态(page state)、映射类型(mapping type)和页面保护(page protection)方面具有相同属性。进程句柄所需的访问掩码为PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION。有关详细信息,请参阅VirtualQueryEx的文档。

在撰写本文时,微软的文档指出需要PROCESS_QUERY_INFORMATION访问掩码,但这是不正确的。

这是Sysinternals工具VMMap的基本功能。

以下示例展示了如何扫描指定进程的地址空间:

void QueryVM(HANDLE hProcess)
{
    MEMORY_BASIC_INFORMATION mbi;
    BYTE* address = nullptr;

    while (NT_SUCCESS(NtQueryVirtualMemory(hProcess, address,
        MemoryBasicInformation, &mbi, sizeof(mbi), nullptr)))
    {
        DisplayMemoryInfo(mbi);
        address += mbi.RegionSize;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

当地址超出用户模式地址空间时,NtQueryVirtualMemory最终会执行失败。DisplayMemoryInfo是一个本地函数,用于显示MEMORY_BASIC_INFORMATION结构的详细信息:

void DisplayMemoryInfo(MEMORY_BASIC_INFORMATION const& mi)
{
    printf("%p  %16zX  %-10s  %-8s  %-15s  %-15s\n ",
        mi.BaseAddress, mi.RegionSize,
        StateToString(mi.State), TypeToString(mi.Type),
        ProtectionToString(mi.Protect).c_str(),
        ProtectionToString(mi.AllocationProtect).c_str());
}
1
2
3
4
5
6
7
8

辅助函数将数值转换为字符串:

const char* StateToString(DWORD state)
{
    switch (state)
    {
        case MEM_FREE:     return "Free";
        case MEM_COMMIT:   return "Committed";
        case MEM_RESERVE:  return "Reserved";
    }
    return "";
}

const char* TypeToString(DWORD type)
{
    switch (type)
    {
        case MEM_IMAGE:    return "Image";
        case MEM_MAPPED:   return "Mapped";
        case MEM_PRIVATE:  return "Private";
    }
    return "";
}

std::string ProtectionToString(DWORD protect)
{
    std::string text;
    if (protect & PAGE_GUARD)
    {
        text += "Guard/";
        protect &= ~PAGE_GUARD;
    }
    if (protect & PAGE_WRITECOMBINE)
    {
        text += "Write  Combine/";
        protect &= ~PAGE_WRITECOMBINE;
    }

    switch (protect)
    {
        case 0:                         break;
        case PAGE_NOACCESS:             text += "No  Access"; break;
        case PAGE_READONLY:             text += "RO"; break;
        case PAGE_READWRITE:            text += "RW"; break;
        case PAGE_EXECUTE_READWRITE:    text += "RWX"; break;
        case PAGE_WRITECOPY:            text += "Write  Copy"; break;
        case PAGE_EXECUTE:              text += "Execute"; break;
        case PAGE_EXECUTE_READ:         text += "RX"; break;
        case PAGE_EXECUTE_WRITECOPY:    text += "Execute/Write  Copy"; break;
        default:                        text += "<other>";
    }
    return text;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

以下是我系统上Explorer.exe进程的(经过大幅精简的)输出:

地址(BaseAddress) 区域大小(RegionSize) 状态(State) 类型(Type) 保护属性(Protect) 分配保护属性(AllocationProtect)
0000000000000000 380000 Free No Access
0000000000380000 10000 Committed Mapped RW RW
0000000000390000 3000 Committed Mapped RO RO
0000000000393000 D000 Free No Access
00000000003A0000 1F000 Committed Mapped RO RO
00000000003BF000 1000 Free No Access
00000000003C0000 4000 Committed Mapped RO RO
00000000003C4000 C000 Free No Access
00000000003D0000 3000 Committed Mapped RO RO
00000000003D3000 D000 Free No Access
00000000003E0000 2000 Committed Private RW RW
00000000003E2000 E000 Free No Access
00000000003F0000 3000 Committed Mapped RO RO
00000000003F3000 D000 Free No Access
0000000000400000 2000 Committed Private RW RW
0000000000402000 1E000 Reserved Private RW
... ... ... ... ... ...
0000000000600000 6F000 Reserved Private RW
000000000066F000 3000 Committed Private Guard/RW RW
0000000000672000 E000 Committed Private RW RW
0000000000680000 11000 Committed Mapped RO RO
0000000000691000 F000 Free No Access
00000000006A0000 11000 Committed Mapped RO RO
... ... ... ... ... ...
00007FF8A392F000 14000 Committed Image RO Execute/Write Copy
00007FF8A3943000 1000 Committed Image RW Execute/Write Copy
00007FF8A3944000 5000 Committed Image RO Execute/Write Copy
00007FFFDB164000 5000 Committed Image RO Execute/Write Copy
00007FFFDB169000 1000 Committed Image RW Execute/Write Copy
00007FFFDB16A000 4000 Committed Image RO Execute/Write Copy
00007FFFDB16E000 1B742000 Free No Access
00007FFFF68B0000 1000 Committed Image RO Execute/Write Copy
00007FFFF68B1000 4000 Committed Image RX Execute/Write Copy
00007FFFF68B5000 2000 Committed Image RO Execute/Write Copy
00007FFFF68B7000 1000 Committed Image RW Execute/Write Copy
00007FFFF68B8000 3000 Committed Image RO Execute/Write Copy
00007FFFF68BB000 1A5000 Free No Access
00007FFFF6A60000 1000 Committed Image RO Execute/Write Copy
00007FFFF6A61000 5000 Committed Image RX Execute/Write Copy
00007FFFF6A66000 3000 Committed Image RO Execute/Write Copy
00007FFFF6A69000 1000 Committed Image RW Execute/Write Copy
00007FFFF6A6A000 4000 Committed Image RO Execute/Write Copy
00007FFFF6A6E000 9582000 Free No Access

完整代码位于QueryVM项目中。

# 8.3.2 MemoryWorkingSetInformation

此信息类与进程的工作集(Working Set)相关——无需产生页面错误(page fault)即可访问的内存。进程句柄必须具有PROCESS_QUERY_INFORMATION访问掩码。

相关数据结构如下所示:

typedef struct _MEMORY_WORKING_SET_BLOCK
{
    ULONG_PTR Protection  : 5;
    ULONG_PTR ShareCount  : 3;
    ULONG_PTR Shared      : 1;
    ULONG_PTR Node        : 3;
#ifdef _WIN64
    ULONG_PTR VirtualPage : 52;
#else
    ULONG     VirtualPage : 20;
#endif
} MEMORY_WORKING_SET_BLOCK, *PMEMORY_WORKING_SET_BLOCK;

typedef struct _MEMORY_WORKING_SET_INFORMATION
{
    ULONG_PTR NumberOfEntries;
    MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1];
} MEMORY_WORKING_SET_INFORMATION, *PMEMORY_WORKING_SET_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

主结构仅作为容器,存储描述给定进程中工作集范围的条目。BaseAddress值无实际作用。这也意味着提供的数据大小不固定,可能会因STATUS_INFO_LENGTH_MISMATCH而执行失败,此时应扩大缓冲区并重新调用。

MEMORY_WORKING_SET_BLOCK的成员说明如下:

• Protection描述区域保护属性。其值与上一小节中看到的标准保护常量不同。 • ShareCount存储此区域的共享计数(如果是共享区域);最大值为7。 • Shared若设置,则表示此区域可与其他进程共享。 • Node是该物理内存页所在的非统一内存访问(NUMA,Non-Uniform Memory Access)节点。 • VirtualPage是虚拟页编号(左移12位可得到实际地址)。

此信息类提供的功能与Windows应用程序编程接口(API)QueryWorkingSet相同。有关数据成员的更多信息,请参阅SDK文档。

以下示例展示了如何根据进程句柄输出所有工作集页面:

bool QueryWS(HANDLE hProcess)
{
    SIZE_T size = 1 << 17;           //  任意值
    std::unique_ptr<BYTE[]> buffer;
    NTSTATUS status;

    do
    {
        buffer = std::make_unique<BYTE[]>(size);
        status = NtQueryVirtualMemory(hProcess, nullptr,
            MemoryWorkingSetInformation, buffer.get(), size, nullptr);
        if (NT_SUCCESS(status))
            break;
        size *= 2;    //  大小不足(size  too  small)
    } while (status == STATUS_INFO_LENGTH_MISMATCH);

    if (!NT_SUCCESS(status))
        return false;

    auto ws = (MEMORY_WORKING_SET_INFORMATION*)buffer.get();
    for (ULONG_PTR i = 0; i < ws->NumberOfEntries; i++)
    {
        //
        //  获取单个条目(get  single  item)
        //
        auto& info = ws->WorkingSetInfo[i];

        //
        //  显示部分详细信息(show  some  details)
        //
        printf("%16zX  Prot:  %-17s  Share:  %d  Shareable:  %s  Node:  %d\n ",
            info.VirtualPage << 12, ProtectionToString(info.Protection),
            (int)info.ShareCount, (int)info.Shared ? "Y" : "N", (int)info.Node);
    }
    return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

完整代码位于QueryWS项目中。

# 8.3.3 MemoryMappedFilenameInformation

此信息类返回给定已提交内存区域(若有)中映射文件的UNICODE_STRING。以下示例返回给定地址中映射文件的std::wstring:

std::wstring Details(HANDLE hProcess, void* address)
{
    BYTE buffer[1 << 10];
    if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, address,
        MemoryMappedFilenameInformation, buffer, sizeof(buffer), nullptr)))
    {
        auto us = (PUNICODE_STRING)buffer;
        return std::wstring(us->Buffer, us->Length / sizeof(WCHAR));
    }
    return L"";
}
1
2
3
4
5
6
7
8
9
10
11

进程句柄必须具有PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION访问掩码。所使用的地址不能包含私有内存(private memory),这意味着该地址所指向的页面类型必须是MEM_MAPPED或MEM_IMAGE。查询MEM_PRIVATE或未提交(non-committed)页面会执行失败,因此最好节省时间,避免查询此类区域。

完整示例位于QueryVM样本的Details函数中。

# 8.3.4:MemoryRegionInformation和MemoryRegionInformationEx

这些信息类提供给定分配范围的额外详细信息。两者均返回以下定义的MEMORY_REGION_INFORMATION结构:

typedef struct _MEMORY_REGION_INFORMATION
{
    PVOID      AllocationBase;
    ULONG      AllocationProtect;
    union
    {
        ULONG  RegionType;
        struct
        {
            ULONG  Private              : 1;
            ULONG  MappedDataFile       : 1;
            ULONG  MappedImage          : 1;
            ULONG  MappedPageFile       : 1;
            ULONG  MappedPhysical       : 1;
            ULONG  DirectMapped         : 1;
            ULONG  SoftwareEnclave      : 1;
            ULONG  PageSize64K          : 1;
            ULONG  PlaceholderReservation : 1;
            ULONG  MappedWriteWatch     : 1;
            ULONG  PageSizeLarge        : 1;
            ULONG  PageSizeHuge         : 1;

            ULONG  Reserved             : 19;
        };
    };
    SIZE_T     RegionSize;
    SIZE_T     CommitSize;
    ULONG_PTR  PartitionId;
    ULONG_PTR  NodePreference;
} MEMORY_REGION_INFORMATION, *PMEMORY_REGION_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

调用MemoryRegionInformation时使用的结构可不含最后两个成员;最好使用扩展版本。

各成员说明如下:

• AllocationBase是初始分配的基地址。如果调用中的BaseAddress是分配块的起始地址,则AllocationBase可能与此不同。 • AllocationProtect是初始分配时赋予内存块的保护属性。请注意,当前保护属性可能不同——实际上,此分配区域的每个页面的保护属性都可能不同。

这也解释了为什么MEMORY_BASIC_INFORMATION同时具有AllocationProtect成员(整个区域的初始保护属性)和Protect成员(该区域(可能是子集)的当前保护属性)。

与RegionType相关联的标志如下:

• Private若设置,则表示该内存区域是进程私有的(不共享)。 • MappedDataFile若设置,则表示该区域将文件映射为数据(而非可移植可执行文件(PE)镜像)。 • MappedImage若设置,则表示该区域映射到可移植可执行文件(PE)镜像。 • MappedPageFile若设置,则表示该区域映射到页面文件(page file)。 • MappedPhysical若设置,则表示该区域映射到非分页内存(例如大页面(large pages))。 • DirectMapped若设置,则表示该区域由内核驱动程序映射到设备内存(device memory)。 • SoftwareEnclave若设置,则表示该区域映射到内存隔离区(例如基于虚拟化的安全(VBS,Virtualization Based Security)保护的隔离区)。 • PageSize64K若设置,则表示该区域以64KB页面大小映射(使用区段对象(section objects)时可能出现)。 • PlaceholderReservation若设置,则表示该区域由内核保留(reserved by the kernel)。 • MappedWriteWatch若设置,则表示该内存已注册写入监视(请参阅Windows应用程序编程接口(API)InitializeProcessForWsWatch或NtSetInformationProcess(指定ProcessWorkingSetWatch))。 • PageSizeLarge若设置,则表示该区域使用大页面(large pages)映射。 • PageSizeHuge若设置,则表示该区域使用超大页面(huge pages)映射。

其余成员如下:

• RegionSize是所讨论区域的大小。 • CommitSize是该区域已提交部分的大小。 • PartitionId是该区域所属的内存分区(通常为0)。 • NodePreference是该区域的非统一内存访问(NUMA)节点优先级。若无优先级,则为(ULONG)-1。

所需的进程句柄必须具有PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION访问掩码位。

QueryVM样本已扩展为包含区域信息,如下所示(Details函数):

if   (mbi .State  ==  MEM_COMMIT)
{
    MEMORY_REGION_INFORMATION  ri;
    if   (NT_SUCCESS(NtQueryVirtualMemory(hProcess,  mbi .BaseAddress,
        MemoryRegionInformationEx,  &ri,  sizeof(ri),  nullptr)))
    {
        details  +=  MemoryRegionFlagsToString(ri .RegionType);
    }
}
1
2
3
4
5
6
7
8
9

辅助函数MemoryRegionFlagsToString根据设置的标志构建字符串:

std::wstring  MemoryRegionFlagsToString(ULONG  flags)
{
    std::wstring  result;
    static  PCWSTR  text[]  =
    {
        L"Private" ,  L"Data  File" ,  L"Image" ,  L"Page  File" ,
        L"Physical" ,  L"Direct" ,  L"Enclave" ,  L"64KB  Page" ,
        L"Placeholder  Reserve" ,  L"Write  Watch" ,
        L"Large  Page" ,  L"Huge  Page" ,
    };

    for(int  i  =  0;  i  <  _countof(text);  i++)
        if  (flags  &   (1  <<  i))
        {
            result  +=  text[i];
            result  +=  L",  " ;
        }

    if  ( ! result.empty())
        return  result.substr(0,  result.length()  -  2);
    return  L"" ;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 8.3.5 扩展内存工作集信息(MemoryWorkingSetExInformation)

该信息类在返回的详情方面是内存工作集信息(MemoryWorkingSetInformation)的扩展版本,但使用方式不同。它不会提供整个进程的工作集信息,而是由调用者选择感兴趣的虚拟地址(可仅为一个),并将其作为输出缓冲区的一部分传入。缓冲区会更新为所提供地址的属性。每个条目均为扩展内存工作集信息(MEMORY_WORKING_SET_EX_INFORMATION)类型,该类型包含一个扩展内存工作集块(MEMORY_WORKING_SET_EX_BLOCK):

typedef struct _MEMORY_WORKING_SET_EX_BLOCK {
    union {
        struct {
            ULONG_PTR Valid : 1;
            ULONG_PTR ShareCount : 3;
            ULONG_PTR Win32Protection : 11;
            ULONG_PTR Shared : 1;
            ULONG_PTR Node : 6;
            ULONG_PTR Locked : 1;
            ULONG_PTR LargePage : 1;
            ULONG_PTR Priority : 3;
            ULONG_PTR Reserved : 3;
            ULONG_PTR SharedOriginal : 1;
            ULONG_PTR Bad : 1;
#ifdef _WIN64
            ULONG_PTR Win32GraphicsProtection : 4;
            ULONG_PTR ReservedUlong : 28;
#endif
        };
        struct {
            ULONG_PTR Valid : 1;
            ULONG_PTR Reserved0 : 14;
            ULONG_PTR Shared : 1;
            ULONG_PTR Reserved1 : 5;
            ULONG_PTR PageTable : 1;
            ULONG_PTR Location : 2;
            ULONG_PTR Priority : 3;
            ULONG_PTR ModifiedList : 1;
            ULONG_PTR Reserved2 : 2;
            ULONG_PTR SharedOriginal : 1;
            ULONG_PTR Bad : 1;
#ifdef _WIN64
            ULONG_PTR ReservedUlong : 32;
#endif
        } Invalid;
    };
} MEMORY_WORKING_SET_EX_BLOCK, *PMEMORY_WORKING_SET_EX_BLOCK;

typedef struct _MEMORY_WORKING_SET_EX_INFORMATION {
    PVOID VirtualAddress;
    union {
        MEMORY_WORKING_SET_EX_BLOCK VirtualAttributes;
        ULONG_PTR Long;
    } u1;
} MEMORY_WORKING_SET_EX_INFORMATION, *PMEMORY_WORKING_SET_EX_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

虚拟地址(VirtualAddress)是输入地址,必须在调用前设置。相邻的虚拟属性(VirtualAttributes)是返回结果。虚拟属性(VirtualAttributes)包含一个联合(union)的两个部分:

  • 如果地址有效(已提交且存在于内存(RAM)中),应参考第一个结构(有效(Valid)标志为1)。
  • 如果地址无效,应参考第二个结构(无效(Invalid))。此时,无效(Invalid)结构中的有效(Valid)标志为0。

部分标志已在内存工作集块(MEMORY_WORKING_SET_BLOCK)中存在,以下是新增标志的描述:

  • 有效(Valid):虚拟地址有效时置1,否则置0。
  • Win32保护(Win32Protection):该地址的保护权限,使用标准保护常量(如PAGE_READWRITE、PAGE_READONLY等)表示。
  • 锁定(Locked):地址被锁定在物理内存中时置1。
  • 优先级(Priority):页面优先级(0到7,7为最高),决定当内存(RAM)不足时该页面保留在内存(RAM)中的可能性。更多详情请参见《Windows内部原理》(Windows Internals)一书的第5章。
  • 原始共享(SharedOriginal):页面由原型页表项(prototype PTE)支持时置1。
  • 损坏(Bad):页面物理损坏或属于飞地(enclave)时置1。
  • 页表(PageTable):该页面存储页表时置1(不适用于用户模式地址)。
  • 位置(Location):设置为三个值之一:0(无效页面)、1(驻留)、2(页面文件)。
  • 修改列表(ModifiedList):页面位于内存管理器(Memory Manager)维护的修改页面列表(Modified page list)中时置1。
  • Win32图形保护(Win32GraphicsProtection):如果该内存供显卡使用,则设置Win32保护标志。

使用扩展内存工作集信息(MemoryWorkingSetExInformation)要求调用者的令牌具有单进程分析权限(SeProfileSingleProcessPrivilege),该权限通常授予管理员。

QueryVM项目包含相关代码,若调用者拥有所需权限,可显示大部分此类信息(参见详情函数(Details function))。

# 8.3.6 内存共享提交信息(MemorySharedCommitInformation)

该信息类返回指定进程中共享提交内存的大小(类型为SIZE_T),以页面(4KB块)为单位。以下示例显示共享提交内存的大小(以KB为单位):

SIZE_T committed;
if (NT_SUCCESS(NtQueryVirtualMemory(hProcess, nullptr,
    MemorySharedCommitInformation, &committed, sizeof(committed), nullptr))) {
    printf("Shared commit: %llu KB\n", committed << 2);
}
1
2
3
4
5

该信息类要求句柄具有有限进程查询权限(PROCSS_QUERY_LIMITED_INFORMATION)或进程查询权限(PROCESS_QUERY_INFORMATION)。基地址(BaseAddress)参数未被使用。

# 8.3.7 内存镜像信息(MemoryImageInformation)

该信息类提供映射到指定地址的镜像(若存在)的详情。返回的结构如下:

typedef struct _MEMORY_IMAGE_INFORMATION {
    PVOID ImageBase;
    SIZE_T SizeOfImage;
    union {
        ULONG ImageFlags;
        struct {
            ULONG ImagePartialMap : 1;
            ULONG ImageNotExecutable : 1;
            ULONG ImageSigningLevel : 4;
            ULONG Reserved : 26;
        };
    };
} MEMORY_IMAGE_INFORMATION, *PMEMORY_IMAGE_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13

镜像基址(ImageBase):镜像在进程中映射的虚拟地址。镜像大小(SizeOfImage):镜像的字节大小。部分映射镜像(ImagePartialMap):仅该页面内的部分镜像被映射时置1。不可执行镜像(ImageNotExecutable):映射不包含执行权限时置1。

最后,镜像签名级别(ImageSigningLevel)是<WinNt.h>中定义的以下值之一:

#define SE_SIGNING_LEVEL_UNCHECKED               0x00000000
#define SE_SIGNING_LEVEL_UNSIGNED                 0x00000001
#define SE_SIGNING_LEVEL_ENTERPRISE             0x00000002
#define SE_SIGNING_LEVEL_CUSTOM_1                 0x00000003
#define SE_SIGNING_LEVEL_DEVELOPER                SE_SIGNING_LEVEL_CUSTOM_1
#define SE_SIGNING_LEVEL_AUTHENTICODE         0x00000004
#define SE_SIGNING_LEVEL_CUSTOM_2                 0x00000005
#define SE_SIGNING_LEVEL_STORE                         0x00000006
#define SE_SIGNING_LEVEL_CUSTOM_3                 0x00000007

#define SE_SIGNING_LEVEL_ANTIMALWARE           SE_SIGNING_LEVEL_CUSTOM_3
#define SE_SIGNING_LEVEL_MICROSOFT               0x00000008
#define SE_SIGNING_LEVEL_CUSTOM_4                 0x00000009
#define SE_SIGNING_LEVEL_CUSTOM_5                 0x0000000A
#define SE_SIGNING_LEVEL_DYNAMIC_CODEGEN  0x0000000B
#define SE_SIGNING_LEVEL_WINDOWS                    0x0000000C
#define SE_SIGNING_LEVEL_CUSTOM_7                 0x0000000D
#define SE_SIGNING_LEVEL_WINDOWS_TCB           0x0000000E
#define SE_SIGNING_LEVEL_CUSTOM_6                 0x0000000F
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 8.3.8 特权内存基本信息(MemoryPrivilegedBasicInformation)

该信息类提供与内存基本信息(MemoryBasicInformation)相同的详情,但适用于受基于虚拟化的安全性(Virtualization Based Security,VBS)保护的“安全”(隔离用户模式 - Isolated User Mode,IUM)进程,例如LsaIso.exe(如果系统上运行了凭据保护(Credential Guard))。结果由安全内核提供,完全由其自行决定。

对于普通进程,其行为与内存基本信息(MemoryBasicInformation)相同。

# 8.3.9 内存飞地镜像信息(MemoryEnclaveImageInformation)

该信息类提供存储在飞地(enclaves)中的镜像的相关信息。用户模式似乎无法访问该信息类。

# 8.3.10 受限内存基本信息(MemoryBasicInformationCapped)

该信息类与内存基本信息(MemoryBasicInformation)相同,但仅支持ARM64架构。除了所提供的区域大小会决定最大地址范围(即使发生向下取整)外,它似乎与内存基本信息(MemoryBasicInformation)没有太大区别。

# 8.3.11 内存物理连续性信息(MemoryPhysicalContiguityInformation)

该信息类返回地址范围的物理内存信息,以物理内存连续性信息(MEMORY_PHYSICAL_CONTIGUITY_INFORMATION)结构形式呈现:

typedef enum _MEMORY_PHYSICAL_CONTIGUITY_UNIT_STATE {
    MemoryNotContiguous,
    MemoryAlignedAndContiguous,
    MemoryNotResident,
    MemoryNotEligibleToMakeContiguous,
} MEMORY_PHYSICAL_CONTIGUITY_UNIT_STATE;

typedef struct _MEMORY_PHYSICAL_CONTIGUITY_UNIT_INFORMATION {
    union {
        ULONG AllInformation;
        struct {
            ULONG State : 2;         // MEMORY_PHYSICAL_CONTIGUITY_UNIT_STATE
            ULONG Reserved : 30;
        };
    };
} MEMORY_PHYSICAL_CONTIGUITY_UNIT_INFORMATION;

typedef struct _MEMORY_PHYSICAL_CONTIGUITY_INFORMATION {
    PVOID VirtualAddress;
    ULONG_PTR Size;
    ULONG_PTR ContiguityUnitSize;
    ULONG Flags;
    PMEMORY_PHYSICAL_CONTIGUITY_UNIT_INFORMATION ContiguityUnitInformation;
} MEMORY_PHYSICAL_CONTIGUITY_INFORMATION, *PMEMORY_PHYSICAL_CONTIGUITY_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

调用该信息类要求调用者的令牌具有单进程分析权限(SeProfileSingleProcessPrivilege)。

输入时,虚拟地址(VirtualAddress)必须设置为要查询的地址(必须与连续性单元大小(ContiguityUnitSize)对齐——通常为一个页面),大小(Size)必须设置为地址范围的大小(必须是连续性单元大小(ContiguityUnitSize)的倍数)。返回时,状态(State)成员会被设置为相应的值。标志(Flags)可以是0或1(若为1,则仅查询有效页面)。

遗憾的是,我未能成功调用该信息类——始终返回状态无效参数(STATUS_INVALID_PARAMETER)。

# 8.3.12 内存损坏信息(MemoryBadInformation)和所有进程内存损坏信息(MemoryBadInformationAllProcesses)

内存损坏信息(MemoryBadInformation)返回进程的损坏页面列表——要么是物理损坏的页面,要么是用于内存飞地(memory enclaves)的页面。基地址(BaseAddress)必须设置为NULL,返回的缓冲区包含以下结构的数组:

typedef struct _MEMORY_BAD_IDENTITY_INFORMATION {
    union {
        PVOID VirtualAddress;             // 虚拟内存
        ULONG_PTR PageFrameIndex;    // 物理内存
    };

    union {
        struct {
            ULONG_PTR Poisoned : 1;      // 不应访问
            ULONG_PTR Physical : 1;
        };
        ULONG_PTR AllInformation;
    };
} MEMORY_BAD_IDENTITY_INFORMATION, *PMEMORY_BAD_IDENTITY_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

物理(Physical)标志指示应使用页面帧索引(PageFrameIndex)(置1时)还是虚拟地址(VirtualAddress)(置0时)。

所有进程内存损坏信息(MemoryBadInformationAllProcesses)提供所有进程的类似信息,但用户模式无法访问。

# 8.4 读取和写入(Reading and Writing)

读取和写入当前进程的内存不需要任何特殊API——只需使用C风格的内存函数(如memcpy、memset)和常规代码即可完成读写操作。

读取和写入其他进程的地址空间需要以下API:

NTSTATUS NtReadVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _In_opt_ PVOID BaseAddress,
    _Out_writes_bytes_(BufferSize) PVOID Buffer,
    _In_ SIZE_T BufferSize,
    _Out_opt_ PSIZE_T NumberOfBytesRead);

NTSTATUS NtWriteVirtualMemory(
    _In_ HANDLE ProcessHandle,
    _In_opt_ PVOID BaseAddress,
    _In_reads_bytes_(BufferSize) PVOID Buffer,
    _In_ SIZE_T BufferSize,
    _Out_opt_ PSIZE_T NumberOfBytesWritten);
1
2
3
4
5
6
7
8
9
10
11
12
13

这些函数是Windows API中的读取进程内存函数(ReadProcessMemory)和写入进程内存函数(WriteProcessMemory)的底层调用函数。参数含义大部分不言自明:基地址(BaseAddress)是目标进程中的虚拟地址。进程句柄(ProcessHandle)必须具有进程虚拟内存读取权限(PROCESS_VM_READ)(用于读取虚拟内存函数(NtReadVirtualMemory))或进程虚拟内存写入权限(PROCESS_VM_WRITE)(用于写入虚拟内存函数(NtWriteVirtualMemory))。

# 8.4.1 通过远程线程注入DLL

向其他进程内存写入数据的一个“经典”示例是,通过在目标进程中创建线程,并将线程指向该进程中的加载库函数(LoadLibrary function),从而向目标进程注入动态链接库(DLL)。以下是使用Windows API实现该功能的示例(为简洁起见,省略了错误处理):

int main(int argc, const char* argv[]) {
    if (argc < 3) {
        printf("Usage: Injector <pid> <dllpath>\n");
        return 0;
    }

    auto pid = atoi(argv[1]);
    HANDLE hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
        PROCESS_CREATE_THREAD, FALSE, pid);
    auto p = VirtualAllocEx(hProcess, nullptr, 1 << 12, MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE);
    WriteProcessMemory(hProcess, p, argv[2], strlen(argv[2]) + 1, nullptr);
    auto hThread = CreateRemoteThread(hProcess, nullptr, 0,
        (LPTHREAD_START_ROUTINE)GetProcAddress(
            GetModuleHandle(L"kernel32"), "LoadLibraryA"),
        p, 0, nullptr);

    // 等待远程线程退出
    WaitForSingleObject(hThread, INFINITE);

    // (可选)清理:释放目标进程中的内存
    VirtualFreeEx(hProcess, p, 0, MEM_RELEASE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

下面我们看看如何实现该技术的“原生”版本。首先,与Windows API版本一样,接收进程ID和动态链接库(DLL)路径,但切换为Unicode编码:

int wmain(int argc, const wchar_t* argv[]) {
    if (argc < 3) {
        printf("Usage: InjectDllRemoteThread <pid> <dllpath>\n");
        return 0;
    }
1
2
3
4
5

这并非强制要求,但我们将使用宽字符加载库函数(LoadLibraryW)作为远程线程要执行的目标函数,这样可以简化代码。

下一步是打开目标进程的句柄,并获取所需的访问权限:

HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(wcstol(argv[1], nullptr, 0)) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
auto status = NtOpenProcess(&hProcess,
    PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
    &procAttr, &cid);
if(!NT_SUCCESS(status)) {
    printf("Error opening process (status: 0x%X)\n", status);
    return status;
}
1
2
3
4
5
6
7
8
9
10

接下来,我们需要在目标进程中分配内存,并将完整的 DLL 路径写入该内存。首先是内存分配操作:

PVOID  buffer  =  nullptr;
SIZE_T  size  =  1  <<  12;    // 4 KB 应该足够
status  =  NtAllocateVirtualMemory(hProcess,  &buffer,  0 ,  &size, MEM_COMMIT   |    MEM_RESERVE,   PAGE_READWRITE);

if   ( !NT_SUCCESS(status))
{
    printf("Error  allocating  memory  (status:  0x%X)\n " ,  status);
    return  status;
}
1
2
3
4
5
6
7
8
9

接下来,将待加载的 DLL 路径复制到已分配的远程缓冲区中:

status  =  NtWriteVirtualMemory(hProcess,  buffer,
    (PVOID)argv[2],  sizeof(WCHAR)  *  (wcslen(argv[2])  +  1),  nullptr);

if   ( !NT_SUCCESS(status))
{
    printf("Error  writing  to  process  (status:  0x%X)\n " ,  status);
    return  status;
}
1
2
3
4
5
6
7
8

现在,我们需要获取 kernel32 的模块句柄(module handle),以便定位 LoadLibraryW 函数:

UNICODE_STRING  kernel32Name;
RtlInitUnicodeString(&kernel32Name,  L"kernel32");
PVOID  hK32Dll;

status  =  LdrGetDllHandle(nullptr ,  nullptr ,  &kernel32Name,  &hK32Dll);

if   ( !NT_SUCCESS(status))
{
    printf("Error  getting  kernel32  module  (status:  0x%X)\n " ,  status);
    return  status;
}
1
2
3
4
5
6
7
8
9
10
11

LdrGetDllHandle 是 Windows API 中 GetModuleHandle 函数的近似等效原生函数。现在我们可以定位 LoadLibraryW 了:

ANSI_STRING  fname;
RtlInitAnsiString(&fname,  "LoadLibraryW");
PVOID  pLoadLibrary;

status  =  LdrGetProcedureAddress(hK32Dll,  &fname,  0 ,  &pLoadLibrary);

if   ( !NT_SUCCESS(status))
{
    printf("Error  locating  LoadLibraryW  (status:  0x%X)\n " ,  status);
    return  status;
}
1
2
3
4
5
6
7
8
9
10
11

你可能会疑惑,为什么我们不“完全使用原生 API”,而是用 LdrLoadDll(LoadLibrary 的原生等效函数)。原因是线程函数(thread function)只能接受一个参数,且该参数必须是 DLL 路径。但遗憾的是,LdrLoadDll 的 DLL 路径通过第三个参数传入,我们没有合适的方式为其提供该参数:

NTSTATUS  LdrLoadDll(
    _In_opt_  PWSTR  DllPath,
    _In_opt_  PULONG  DllCharacteristics,
    _In_  PUNICODE_STRING  DllName,       // DLL 路径
    _Out_  PVOID  *DllHandle);
1
2
3
4
5

因此,我们暂时选择 LoadLibraryW——它仅接受一个参数,即待加载的 DLL 路径。

最后一步是创建实际的线程,以执行 LoadLibraryW 并将目标进程中已分配的缓冲区作为参数传入:

HANDLE  hThread;
status  =  RtlCreateUserThread(hProcess,  nullptr ,  FALSE,  0 ,  0 ,  0 ,
    (PUSER_THREAD_START_ROUTINE)pLoadLibrary,  buffer,
    &hThread,  nullptr);

if   ( !NT_SUCCESS(status))
{
    printf("Error  creating  thread  (status:  0x%X)\n " ,  status);
    return  status;
}

printf("Success!\n " );
1
2
3
4
5
6
7
8
9
10
11
12

如果此操作成功,线程将开始执行,并根据传入的路径加载 DLL。当然,如果路径无效(例如路径错误),加载可能会失败。从注入器(injector)的角度来看,后续只需执行清理操作:

NtWaitForSingleObject(hThread,  FALSE,  nullptr);
size  =  0;
NtFreeVirtualMemory(hProcess,  &buffer,  &size,  MEM_RELEASE);

NtClose(hThread);
NtClose(hProcess);
1
2
3
4
5
6

以下是完整的 main 函数(来自 InjectDllRemoteThread 项目),已移除错误处理:

int  wmain(int  argc,  const  wchar_t*  argv[])
{
    if   (argc  <  3)
    {
        printf("Usage:  InjectDllRemoteThread  <pid>  <dllpath>\n " );
        return  0;
    }

    HANDLE  hProcess;
    CLIENT_ID  cid{  ULongToHandle(wcstol(argv[1],  nullptr ,  0))  };
    OBJECT_ATTRIBUTES  procAttr  =  RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr ,  0);
    auto  status  =  NtOpenProcess(&hProcess,
        PROCESS_VM_WRITE   |    PROCESS_VM_OPERATION   |    PROCESS_CREATE_THREAD,
        &procAttr,  &cid);

    PVOID  buffer  =  nullptr;
    SIZE_T  size  =  1  <<  12;    // 4 KB 应该足够
    status  =  NtAllocateVirtualMemory(hProcess,  &buffer,  0 ,  &size,
        MEM_COMMIT   |    MEM_RESERVE,   PAGE_READWRITE);

    status  =  NtWriteVirtualMemory(hProcess,  buffer,
        (PVOID)argv[2],  sizeof(WCHAR)  *  (wcslen(argv[2])  +  1),  nullptr);

    UNICODE_STRING  kernel32Name;
    RtlInitUnicodeString(&kernel32Name,  L"kernel32");
    PVOID  hK32Dll;
    status  =  LdrGetDllHandle(nullptr ,  nullptr ,  &kernel32Name,  &hK32Dll);

    ANSI_STRING  fname;
    RtlInitAnsiString(&fname,  "LoadLibraryW");
    PVOID  pLoadLibrary;
    status  =  LdrGetProcedureAddress(hK32Dll,  &fname,  0 ,  &pLoadLibrary);

    HANDLE  hThread;
    status  =  RtlCreateUserThread(hProcess,  nullptr ,  FALSE,  0 ,  0 ,  0 ,
        (PUSER_THREAD_START_ROUTINE)pLoadLibrary,  buffer,
        &hThread,  nullptr);

    printf("Success!\n " );

    NtWaitForSingleObject(hThread,  FALSE,  nullptr);
    size  =  0;
    NtFreeVirtualMemory(hProcess,  &buffer,  &size,  MEM_RELEASE);

    NtClose(hThread);
    NtClose(hProcess);
    return  0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 8.5 其他虚拟内存 API(Other Virtual APIs)

本节将介绍“虚拟内存”(Virtual)系列中的其他一些 API。

新分配内存区域的初始页面保护属性(page protection)在调用 NtAllocateVirtualMemory 时设置。如果需要修改内存保护属性,可以使用 NtProtectVirtualMemory:

NTSTATUS  NtProtectVirtualMemory(
    _In_  HANDLE  ProcessHandle,
    _Inout_  PVOID  *BaseAddress,
    _Inout_  PSIZE_T  RegionSize,
    _In_  ULONG  NewProtect,
    _Out_  PULONG  OldProtect);
1
2
3
4
5
6

大多数参数到目前为止应该已经很熟悉了。ProcessHandle 可以是 NtCurrentProcess(),用于修改调用进程自身的内存保护属性(此场景下调用始终成功);如果操作目标是其他进程,则该句柄必须具备 PROCESS_VM_OPERATION 访问权限掩码。NewProtect 是请求的保护属性,使用我们之前遇到的与 Windows API 相同的常量(如 PAGE_READWRITE、PAGE_READONLY 等)。返回的 OldProtect 包含该内存区域第一页的原有保护属性。

Windows API 中的 VirtualProtect(Ex) 系列 API 底层会调用此函数。

内存可以被“锁定”(locked)到物理内存中,防止其在“解锁”(unlocked)前被换出(paged out)。可使用以下 API 实现该功能:

NTSTATUS  NtLockVirtualMemory(
    _In_  HANDLE  ProcessHandle,
    _Inout_  PVOID  *BaseAddress,
    _Inout_  PSIZE_T  RegionSize,
    _In_  ULONG  MapType);

NTSTATUS  NtUnlockVirtualMemory(
    _In_  HANDLE  ProcessHandle,
    _Inout_  PVOID  *BaseAddress,
    _Inout_  PSIZE_T  RegionSize,
    _In_  ULONG  MapType);
1
2
3
4
5
6
7
8
9
10
11

MapType 只能取以下两个值之一:MAP_PROCESS(1)或 MAP_SYSTEM(2)。如果指定 MAP_SYSTEM,则调用方的令牌(token)中必须具备 SeLockMemoryPrivilege 权限。这两个标志在操作逻辑上似乎没有区别,但必须指定其中一个。

Windows API 中的 VirtualLock 和 VirtualUnlock 使用 MAP_PROCESS 标志。

锁定内存会告知内存管理器(memory manager):只要进程中有线程在运行(而非等待状态),就将这些页面保留在进程的工作集(working set)中。

# 8.6 堆(Heaps)

“虚拟内存”(Virtual)系列 API 始终以页面(4 KB)为单位操作,这并不满足大多数应用程序的需求。堆(heaps)在虚拟内存 API 的基础上,提供了细粒度的内存管理功能。堆管理器(Heap Manager)是负责管理堆内内存的核心组件。

Windows API 提供了一系列与堆相关的函数,例如 HeapAlloc、HeapFree 等。这些函数都是原生堆 API 的轻量级包装层。

每个进程启动时都会拥有一个默认堆(Process Default Heap),该堆由 NtDll 在进程初始化过程中创建。可以通过 RtlProcessHeap 获取此堆:

#define  RtlProcessHeap()  (NtCurrentPeb()->ProcessHeap)
1

进程还可以创建更多堆,后续将简要说明创建额外堆的一些原因。

堆管理器同样在核内实现,我们接下来要介绍的部分函数在 WDK(Windows Driver Kit)中有文档说明。

# 8.6.1 基本堆管理(Basic Heap Management)

从堆中分配内存可通过 RtlAllocateHeap 实现:

PVOID  RtlAllocateHeap(
    _In_  PVOID  HeapHandle,
    _In_opt_  ULONG  Flags,
    _In_  SIZE_T  Size);
1
2
3
4

HeapHandle 是堆的句柄(RtlProcessHeap() 是有效的句柄值)。Flags 可以是 0,也可以是以下值的组合:

  • HEAP_NO_SERIALIZE(1):分配操作无需与可能访问同一堆的其他线程同步。
  • HEAP_ZERO_MEMORY(8):返回的缓冲区会被初始化为零(未指定此标志时,缓冲区内容保持不变)。
  • HEAP_GENERATE_EXCEPTION(4):分配失败时,默认返回 NULL;指定此标志后,会抛出 STATUS_NO_MEMORY 异常。
  • HEAP_SETTABLE_USER_VALUE(0x100):允许为分配的内存块设置用户定义的值(详情见下一节)。

“未指定”某个标志并不意味着其对立面,而是表示使用堆的默认行为。例如,未指定 HEAP_NO_SERIALIZE 时,如果堆本身是通过该标志创建的,那么分配操作仍可能不同步。

进程默认堆是同步的。

Size 是分配的内存大小(以字节为单位)。返回值是分配的内存块指针;分配失败时返回 NULL(除非 Flags 中指定了 HEAP_GENERATE_EXCEPTION,此时失败会抛出异常)。

当内存不再需要时,可调用 RtlFreeHeap 释放该内存块:

BOOLEAN  RtlFreeHeap(
    _In_  PVOID  HeapHandle,
    _In_opt_  ULONG  Flags,
    _Frees_ptr_opt_  PVOID  BaseAddress);
1
2
3
4

唯一有效的 Flags 是 HEAP_NO_SERIALIZE。BaseAddress 必须是之前通过 RtlAllocateHeap 返回的地址。

如果需要调整已分配内存块的大小,可以使用 RtlReAllocateHeap:

PVOID  RtlReAllocateHeap(
    _In_  PVOID  HeapHandle,
    _In_  ULONG  Flags,
    _Frees_ptr_opt_  PVOID  BaseAddress,
    _In_  SIZE_T  Size);
1
2
3
4
5

新内存块的大小可以大于或小于 BaseAddress 所指向的现有内存块。如果请求的内存块更大且无法在现有块空间内扩展,数据会被复制到新的内存块中。即使是缩小内存块,也不保证返回原内存块(即返回值才是指向最终内存块的指针,无论是否移动过)。

Flags 支持的取值与 RtlAllocateHeap 相同。

# 8.6.2 创建堆(Creating Heaps)

进程中可以创建多个堆。以下是不使用默认进程堆、而是创建新堆的一些可能原因:

  • 默认进程堆是同步的,如果堆不存在竞争(contention),同步机制会降低性能。创建独立堆可以控制同步行为;总体而言,创建堆能提供更多自定义空间。
  • 如果应用程序频繁请求不同大小的内存块,堆可能会随着时间推移产生碎片(fragmented)。如果应用程序中常见固定大小的内存分配,一种解决方案是创建专门用于固定大小分配的堆。
  • 如果堆访问存在错误(如堆损坏,heap corruptions),若这些错误发生在特定堆上,会更容易排查。
  • 有时一次性销毁整个堆,比逐个释放所有分配的内存块更简便。

Windows 支持两种类型的堆:第一种是 NT 堆(NT Heap),是最初实现的堆管理方案;Windows 8 引入了一种新的堆类型——段堆(Segment Heap),在内存块管理和安全性方面提供了更优的实现。但由于兼容性考虑,普通进程默认仍使用 NT 堆。UWP 应用程序和某些系统进程使用段堆。

使用段堆的系统进程镜像名称包括:Svchost.exe、Smss.exe、Csrss.exe、Lsass.exe、Services.exe、RuntimeBroker.exe、dwm.exe、WinInit.exe、WinLogon.exe、MsMpEng.exe、NisSrv.exe、SiHost.exe 等。

可执行文件可以选择启用段堆:在注册表项 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\exename 中添加一个名为 FrontEndHeapDebugOptions 的值,设为 8 表示使用段堆,设为 4 表示禁用段堆。

RtlHeapCreate 是堆创建函数,在 WDK 中有文档说明(带有内核相关偏向):

PVOID  RtlCreateHeap(
    _In_  ULONG  Flags,
    _In_opt_  PVOID  HeapBase,
    _In_opt_  SIZE_T  ReserveSize,
    _In_opt_  SIZE_T  CommitSize,
    _In_opt_  PVOID  Lock,
    _In_opt_  PRTL_HEAP_PARAMETERS  Parameters);
1
2
3
4
5
6
7

Flags 可以是 0,也可以是以下值的组合:

  • HEAP_NO_SERIALIZE(1):表示堆默认不同步。
  • HEAP_GENERATE_EXCEPTION(4):分配函数失败时抛出 STATUS_NO_MEMORY 异常,而非返回 NULL。
  • HEAP_GROWABLE(2):表示堆可以增长到任意大小,受进程地址空间和可用内存限制。指定此标志时,HeapBase 必须设为 NULL。
  • HEAP_REALLOC_IN_PLACE_ONLY(0x10):仅当无需分配新内存块时,重新分配(reallocations)操作才能成功。
  • HEAP_TAIL_CHECKING_ENABLED(0x20):堆函数会执行额外检查,确保进程未使用超出分配额度的字节。具体实现是在分配请求的内存块之后填充几个额外字节(使用已知填充模式),并在释放内存块或查询其大小时验证该模式。这是一种调试辅助功能,用于在堆损坏发生前发现问题。
  • HEAP_FREE_CHECKING_ENABLED(0x40):另一种堆调试功能,旨在确保释放操作(free operations)作用于正确的地址。
  • HEAP_DISABLE_COALESCE_ON_FREE(0x80):不尝试自动合并(coalesce)空闲内存区域。可通过 RtlCompactHeap 强制合并空闲区域。

HEAP_TAIL_CHECKING_ENABLED、HEAP_FREE_CHECKING_ENABLED 和 HEAP_DISABLE_COALESCE_ON_FREE 也可以通过 NT 全局标志(NT Global Flags)指定;若通过全局标志指定,无论创建堆时是否显式设置这些标志,它们都会自动应用于新创建的堆。

  • HEAP_CREATE_ALIGN_16(0x10000):表示内存块应按 16 字节对齐。
  • HEAP_CREATE_ENABLE_TRACING(0x20000):启用堆跟踪(heap tracing)。
  • HEAP_CREATE_ENABLE_EXECUTE(0x40000):表示分配的内存具备 PAGE_EXECUTE_READWRITE 保护属性,即代码可以从堆中执行而不会引发异常。
  • HEAP_CREATE_SEGMENT_HEAP(0x100):表示创建段堆(Segment Heap)而非 NT 堆。创建段堆时,必须同时指定 HEAP_GROWABLE 标志;此外,HeapBase、ReservedSize、CommitSize 和 Lock 必须设为 0 或 NULL。

各种“调试”标志(如 HEAP_TAIL_CHECKING_ENABLED、HEAP_FREE_CHECKING_ENABLED)会导致分配函数变慢,仅用于调试目的。

HeapBase 可以是 NULL(此时必须指定 HEAP_GROWABLE 标志),这种情况下堆管理器会分配新的内存区域来承载堆;如果 HeapBase 非 NULL,则使用该地址作为新堆的基础地址——该地址必须是进程之前已分配的(例如通过 NtAllocateVirtualMemory 分配)。

如果 ReservedSize 非 0,表示堆的初始保留内存大小(向上取整到页面单位);CommitSize 表示堆的初始提交内存大小。ReservedSize 和 CommittedSize 的关系如表 8-3 所示。

表 8-3:CommitSize 与 ReservedSize 的关系

ReservedSize CommitSize 结果(Result)
0 0 堆初始保留 64 个页面,提交 1 个页面
0 非 0 ReservedSize 设为 CommitSize,并向上取整到最近的 64KB 大小
非 0 0 堆初始提交 1 个页面
非 0 非 0 若 CommitSize 大于 ReservedSize,则将 CommitSize 缩减为 ReservedSize

接下来,Lock 参数是可选指针,指向已初始化的 CRITICAL_SECTION 结构,用于同步堆访问。如果指定了 HEAP_NO_SERIALIZE 标志,则 Lock 必须设为 NULL(因为该标志的目的是禁用堆同步)。

最后,Parameters 是一个可选指针,指向包含更多自定义选项的结构,其定义如下:

typedef  NTSTATUS   (NTAPI  *PRTL_HEAP_COMMIT_ROUTINE)( _In_  PVOID  Base,
_Inout_  PVOID  *CommitAddress,
_Inout_  PSIZE_T  CommitSize);

typedef  struct  _RTL_HEAP_PARAMETERS  {
ULONG  Length;      //  sizeof(RTL_HEAP_PARAMATERS)
SIZE_T  SegmentReserve;
SIZE_T  SegmentCommit;
SIZE_T  DeCommitFreeBlockThreshold;
SIZE_T  DeCommitTotalFreeThreshold;
SIZE_T  MaximumAllocationSize;
SIZE_T  VirtualMemoryThreshold;
SIZE_T  InitialCommit;
SIZE_T  InitialReserve;
PRTL_HEAP_COMMIT_ROUTINE  CommitRoutine;
SIZE_T  Reserved[2];
}  RTL_HEAP_PARAMETERS,  *PRTL_HEAP_PARAMETERS;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

表 8-4 总结了 RTL_HEAP_PARAMETERS 结构各成员的作用。

表 8-4:“RTL_HEAP_PARAMETERS”结构

成员 描述 指定为零时的值
Length 使用前必须设置为该结构的大小 必须指定
SegmentReserve 堆扩展时的保留段大小 PEB->HeapSegmentReserve
SegmentCommit 需要更多已提交内存时的提交段大小 PEB->HeapSegmentCommit
DeCommitFreeBlockThreshold 释放内存时触发释放提交(decommit)操作的块阈值 PEB->HeapDeCommitFreeBlockThreshold
DeCommitTotalFreeThreshold 触发释放所有堆内存提交的阈值 PEB->DeCommitTotalFreeThreshold
MaximumAllocationSize 单次分配允许的最大大小 比进程地址空间小一页
VirtualMemoryThreshold 用于决定内部创建数组的大小 0x7f000
InitialCommit 与 CommitSize 参数含义相同,非零时覆盖该参数 忽略
InitialReserve 与 ReservedSize 参数含义相同,非零时覆盖该参数 忽略
CommitRoutine 用于执行分配操作的自定义例程 忽略
Reserved 保留字段 必须清零

CommitRoutine 允许自定义块的分配过程。仅当堆不可扩展且 BaseAddress 不为 NULL 时,该例程才有效。在这种情况下,堆管理器(Heap Manager)会使用以下参数调用该例程:

  • Base 是堆的基地址(即 RtlCreateHeap 函数中的 BaseAddress 参数)。
  • CommitAddress 是存储分配地址的位置。
  • CommitSize 是输入时的请求大小,该例程可将其修改为实际分配的字节数。

RtlCreateHeap 函数的返回值是一个堆句柄(heap handle)。它并非内核对象(kernel object)意义上的句柄,而是一个指向未公开结构的不透明指针(opaque pointer)。该指针可能指向两种结构之一:NT 堆(NT heap)对应的 HEAP 结构,或段堆(Segment heap)对应的 SEGMENT_HEAP 结构。尽管这些结构未公开,但可通过微软的公共符号(public Microsoft symbols)获取其定义。

以下是 WinDbg 调试会话的输出,该会话附加到一个用户模式进程(任意用户模式进程均可),展示了这两种结构(为简洁起见进行了截断):

0:000>  dt  ntdll!_HEAP
+0x000  Segment                       :  _HEAP_SEGMENT
+0x000  Entry                          :  _HEAP_ENTRY
+0x010  SegmentSignature   :  Uint4B
+0x014  SegmentFlags            :  Uint4B
+0x018  SegmentListEntry   :  _LIST_ENTRY
+0x028  Heap                              :  Ptr64  _HEAP
+0x030  BaseAddress              :  Ptr64  Void
+0x038  NumberOfPages         :  Uint4B
+0x040  FirstEntry                :  Ptr64  _HEAP_ENTRY
+0x048  LastValidEntry       :  Ptr64  _HEAP_ENTRY
+0x050  NumberOfUnCommittedPages   :  Uint4B
+0x054  NumberOfUnCommittedRanges   :  Uint4B
+0x058  SegmentAllocatorBackTraceIndex   :  Uint2B
+0x05a  Reserved                  :  Uint2B
+0x060  UCRSegmentList       :  _LIST_ENTRY
...
+0x238  Counters                     :  _HEAP_COUNTERS
+0x2b0  TuningParameters   :  _HEAP_TUNING_PARAMETERS

0:000>  dt  ntdll!_SEGMENT_HEAP
+0x000  EnvHandle                  :  RTL_HP_ENV_HANDLE
+0x010  Signature                  :  Uint4B
+0x014  GlobalFlags              :  Uint4B
+0x018  Interceptor           :  Uint4B
+0x01c  ProcessHeapListIndex   :  Uint2B
+0x01e  AllocatedFromMetadata   :  Pos  0,  1  Bit
+0x020  CommitLimitData     :  _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020  ReservedMustBeZero1   :  Uint8B
+0x028  UserContext            :  Ptr64  Void
+0x030  ReservedMustBeZero2   :  Uint8B
+0x038  Spare                            :  Ptr64  Void
+0x040  LargeMetadataLock   :  Uint8B
+0x048  LargeAllocMetadata   :  _RTL_RB_TREE
+0x058  LargeReservedPages   :  Uint8B
+0x060  LargeCommittedPages   :  Uint8B
+0x068  Tag                                :  Uint8B
+0x070  StackTraceInitVar   :  _RTL_RUN_ONCE
+0x080  MemStats                     :  _HEAP_RUNTIME_MEMORY_STATS
+0x0d8  GlobalLockCount     :  Uint2B
+0x0dc  GlobalLockOwner     :  Uint4B
+0x0e0  ContextExtendLock   :  Uint8B
+0x0e8  AllocatedBase        :  Ptr64  UChar
+0x0f0  UncommittedBase     :  Ptr64  UChar
+0x0f8  ReservedLimit        :  Ptr64  UChar
+0x100  ReservedRegionEnd   :  Ptr64  UChar
+0x108  CallbacksEncoded   :  _RTL_HP_HEAP_VA_CALLBACKS_ENCODED
+0x140  SegContexts              :   [2]  _HEAP_SEG_CONTEXT
+0x2c0  VsContext                  :  _HEAP_VS_CONTEXT
+0x380  LfhContext               :  _HEAP_LFH_CONTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

这意味着,如果需要,你可以将堆句柄强制转换为上述两种结构之一,并查看其成员。对于一个并非由你创建的堆,如何判断其类型呢?你可以将其强制转换为 SEGMENT_HEAP 结构并查看 Signature 成员。如果该成员的值为 0xddeeddee,则表明这是一个段堆(Segment heap)。以下是一个示例,假设上述结构已转换为对应的 C 语言声明:

void  PrintHeapType(PVOID  heap)
{
    auto  segHeap  =   (SEGMENT_HEAP*)heap;
    if(segHeap->Signature  ==  0xddeeddee)
        printf("Segment  Heap");
    else
    {
        auto  ntHeap  =   (HEAP*)heap;
        printf("NT  Heap,  Base  Address:  0x%p",  ntHeap->BaseAddress);
    }
}
1
2
3
4
5
6
7
8
9
10
11

使用 WinDbg 时,可以通过 !heap 命令获取进程中所有堆或特定目标堆的详细信息。

当堆不再需要时,应将其销毁:

PVOID  RtlDestroyHeap(_In_  _Post_invalid_  PVOID  HeapHandle);
1

如果堆上仍有未释放的分配内存,在堆被完全销毁时,这些内存会被“释放”(released)。销毁成功时,返回值为 NULL;否则,返回值为 HeapHandle。

以下示例创建了一个新堆,在该堆上执行分配操作、释放分配的内存,最后销毁堆。显然,在实际场景中,创建的堆会用于多次分配操作。

auto  heap  =  RtlCreateHeap(HEAP_GROWABLE   |    HEAP_NO_SERIALIZE, nullptr ,  0 ,  0 ,  nullptr ,  nullptr);
auto  buffer  =  RtlAllocateHeap(heap,  HEAP_ZERO_MEMORY,  30);    //  30  bytes

//  use  buffer . . .

RtlFreeHeap(heap,  0,  buffer);
RtlDestroyHeap(heap);
1
2
3
4
5
6
7

# 8.6.3 其他堆函数(Other Heap Functions)

本节将介绍其他堆相关函数。

# 8.6.3.1 堆分配大小(Heap Allocation Size)

可以查询堆分配的大小:

SIZE_T  RtlSizeHeap(
_In_  PVOID  HeapHandle,
_In_  ULONG  Flags,
_In_  PVOID  BaseAddress);
1
2
3
4

返回的大小(以字节为单位)是指定的原始分配大小,不包括任何填充字节(padding bytes)或元数据字节(metadata bytes)。

有时出于安全考虑,需要将已释放的块清零,因为通常情况下,这些数据会一直存在,直到被覆盖为止。RtlZeroHeap 函数的作用就是实现这一功能:

# 8.6.3.2 保护(Protection)

NTSTATUS  RtlZeroHeap(
_In_  PVOID  HeapHandle,
_In_  ULONG  Flags);
1
2
3

堆的内存通常受 PAGE_READWRITE 或 PAGE_EXECUTE_READWRITE 保护(如果在创建堆时指定了 HEAP_CREATE_ENABLE_EXECUTE 标志)。可以通过 RtlProtectHeap 函数将堆内存的保护属性修改为只读(read-only):

VOID  RtlProtectHeap(
_In_  PVOID  HeapHandle,
_In_  BOOLEAN  MakeReadOnly);
1
2
3

之后,若要恢复原有保护属性,可再次调用该函数,并将 MakeReadOnly 参数设置为 FALSE。

# 8.6.3.3 锁定(Locking)

可以将堆的访问权限锁定到当前线程,防止其他线程访问该堆。与多次调用(每次调用内部都会进行锁定/解锁操作)相比,这种方式可能具有更高的性能。此外,如果要遍历堆(heap walking),锁定堆也是必要的,因为其他线程的任何修改都会破坏遍历的状态。以下函数用于锁定和解锁堆:

BOOLEAN  RtlLockHeap(_In_  PVOID  HeapHandle);
BOOLEAN  RtlUnlockHeap(_In_  PVOID  HeapHandle);
1
2

这些函数支持递归调用,这意味着多次调用 Lock 函数后,需要调用相同次数的 Unlock 函数,才能真正解锁堆,允许正常访问。

# 8.6.3.4 用户数据(User Data)

可以为堆条目(heap entries)附加用户定义的值。要实现这一功能,分配内存时必须指定 HEAP_SETTABLE_USER_VALUE 标志:

BOOLEAN  RtlSetUserValueHeap(
_In_  PVOID HeapHandle,
_In_  ULONG Flags,
_In_  PVOID BaseAddress,
_In_  PVOID UserValue);
1
2
3
4
5

理论上,还可以通过调用 RtlSetUserFlagsHeap 函数设置 2 位标志,但这种方式非常不稳定,因为它既不适用于段堆(Segment heap),也不适用于低碎片堆(Low Fragmentation Heap,LFH)层处于活跃状态的情况。

若要从堆分配中检索回附加的用户定义值,可调用 RtlGetUserInfoHeap 函数,该函数会返回用户定义的值和/或标志:

BOOLEAN  RtlGetUserInfoHeap(
_In_  PVOID  HeapHandle,
_In_  ULONG  Flags,
_In_  PVOID  BaseAddress,
_Out_opt_  PVOID  *UserValue,
_Out_opt_  PULONG  UserFlags);
1
2
3
4
5
6

以下示例展示了如何将 0x1234 设为用户定义值,并将其检索回来:

auto  buffer  =  RtlAllocateHeap(heap,  HEAP_SETTABLE_USER_VALUE,  40);
auto  ok  =  RtlSetUserValueHeap(heap,  0,  buffer,  (PVOID)0x1234);
ok  =  RtlSetUserFlagsHeap(heap,  0 ,  buffer,  0,  RTL_HEAP_SETTABLE_FLAG1);

//  read  back

PVOID  value{  nullptr  };
ok  =  RtlGetUserInfoHeap(heap,  0 ,  buffer,  &value,  nullptr);
assert(value  ==   (PVOID)0x1234);
1
2
3
4
5
6
7
8
9

# 8.6.3.5 多次分配(More Allocations)

可以通过一次调用完成多个相同大小的内存分配:

ULONG  RtlMultipleAllocateHeap(
_In_  PVOID  HeapHandle,
_In_  ULONG  Flags,
_In_  SIZE_T  Size,
_In_  ULONG  Count,
_Out_  PVOID*  Array);
1
2
3
4
5
6

其中,Size 是每次分配的字节大小,Count 是分配次数,Array 是返回的指针数组。调用成功时,返回值为 Count;若返回值小于 Count,则表示成功分配的次数。

正如你所预期的,也可以一次性释放多个分配的内存:

ULONG  RtlMultipleFreeHeap(
_In_  PVOID  HeapHandle,
_In_  ULONG  Flags,
_In_  ULONG  Count,
_In_  PVOID  *Array);
1
2
3
4
5

# 8.7 堆信息(Heap Information)

存在多个原生 API(native APIs)用于获取堆的相关信息。首先,我们来看如何获取调用进程(calling process)中的所有堆。有两种实现方式:一种是调用 RtlGetProcessHeaps 函数获取堆句柄数组,另一种是使用 RtlEnumProcessHeaps 函数通过回调枚举堆:

ULONG  RtlGetProcessHeaps(
_In_  ULONG  NumberOfHeaps,
_Out_  PVOID  *ProcessHeaps);

typedef  NTSTATUS   (NTAPI  *PRTL_ENUM_HEAPS_ROUTINE)(
_In_  PVOID  HeapHandle,
_In_  PVOID  Parameter);

NTSTATUS  RtlEnumProcessHeaps(
_In_  PRTL_ENUM_HEAPS_ROUTINE  EnumRoutine,
_In_  PVOID  Parameter);
1
2
3
4
5
6
7
8
9
10
11

RtlGetProcessHeaps 函数接受两个参数:要检索的堆句柄数量,以及用于存储结果的数组。该函数返回进程中实际的堆数量。这意味着,如果返回值大于 NumberOfHeaps,则表示未检索到所有堆句柄。

RtlEnumProcessHeaps 函数会为进程中的每个堆调用一次回调函数(EnumRoutine),这种方式可能更便捷,因为无需提前分配内存,也无需“猜测”堆的数量。

# 8.7.1 堆遍历(Heap Walking)

给定一个堆句柄,可以对堆进行“遍历”(walk),即检索堆所包含的各个块,这在调试场景中非常有用。堆遍历需使用 RtlWalkHeap 函数,返回的条目以 RTL_HEAP_WALK_ENTRY 结构表示:

#define 宏定义 数值
#define RTL_HEAP_BUSY (USHORT)0x0001
#define RTL_HEAP_SEGMENT (USHORT)0x0002
#define RTL_HEAP_SETTABLE_VALUE (USHORT)0x0010
#define RTL_HEAP_SETTABLE_FLAG1 (USHORT)0x0020
#define RTL_HEAP_SETTABLE_FLAG2 (USHORT)0x0040
#define RTL_HEAP_SETTABLE_FLAG3 (USHORT)0x0080
#define RTL_HEAP_SETTABLE_FLAGS (USHORT)0x00e0
#define RTL_HEAP_UNCOMMITTED_RANGE (USHORT)0x1000
#define RTL_HEAP_PROTECTED_ENTRY (USHORT)0x2000
#define RTL_HEAP_LARGE_ALLOC (USHORT)0x4000
#define RTL_HEAP_LFH_ALLOC (USHORT)0x8000
typedef  struct  _RTL_HEAP_WALK_ENTRY
{
    PVOID  DataAddress;
    SIZE_T  DataSize;
    UCHAR  OverheadBytes;
    UCHAR  SegmentIndex;
    USHORT  Flags;       //  see  flags  above
    union
    {
        struct
        {
            SIZE_T  Settable;
            USHORT  TagIndex;
            USHORT  AllocatorBackTraceIndex;
            ULONG  Reserved[2];
        }  Block;
        struct
        {
            ULONG  CommittedSize;
            ULONG  UnCommittedSize;
            PVOID  FirstEntry;
            PVOID  LastEntry;
        }  Segment;
    };
}  RTL_HEAP_WALK_ENTRY,  *PRTL_HEAP_WALK_ENTRY;

NTSTATUS  RtlWalkHeap(
_In_  PVOID  HeapHandle,
_Inout_  PRTL_HEAP_WALK_ENTRY  Entry);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

堆遍历的起始条件是:RTL_HEAP_WALK_ENTRY 结构的 DataAddress 字段设置为 NULL。后续调用应使用上一次返回的地址来获取下一个条目。根据 Flags 成员中的 RTL_HEAP_SEGMENT 标志,返回的每个条目要么是一个块(block),要么是一个段(segment)。段(Segment)是一个管理实体,包含多个块,每个块代表一块内存(注意不要与段堆(Segment Heap)混淆)。

以下是对默认进程堆(default process heap)进行简单遍历的示例:

RTL_HEAP_WALK_ENTRY  entry{};
while  (NT_SUCCESS(RtlWalkHeap(RtlProcessHeap(),  &entry)))
{
    printf("Addr:  0x%p  Size:  0x%08X  Flags:  0x%04X\n " ,
    entry.DataAddress,  entry.DataSize,  (int)entry.Flags);
}
1
2
3
4
5
6

DataSize 是块或段的大小。对于块(block),如果其状态为忙碌(已使用,RTL_HEAP_BUSY),则 DataSize 是提供给客户端的分配大小。OverheadBytes 是维护该分配所需的额外字节数。SegmentIndex 是该堆中的段索引,从 0 开始。

对于段(设置了 RTL_HEAP_SEGMENT 标志),联合体(union)中的 Segment 部分包含以下成员:

  • CommittedSize:段中已提交的字节数(始终是页大小的整数倍)。
  • UncommittedSize:当前未提交的字节数(始终是页大小的整数倍)。
  • FirstEntry:段中的第一个条目。
  • LastEntry:段中最后一个有效的条目。

对于块(未设置 RTL_HEAP_SEGMENT 标志),联合体(union)中的 Block 部分包含以下成员:

  • Settable:附加到该块的用户定义值(如果设置了 RTL_HEAP_SETTABLE_VALUE 标志)。
  • TagIndex:如果块带有标签(tagged),则为该块的标签索引。
  • AllocatorBackTraceIndex:用于调试目的(本章暂不涉及)。

# 8.7.2 更多堆信息

可以通过 RtlQueryHeapInformation 函数检索堆信息,也可以通过 RtlSetHeapInformation 函数修改某些堆的详细配置:

NTSTATUS  RtlQueryHeapInformation(
_In_  PVOID  HeapHandle,
_In_  HEAP_INFORMATION_CLASS  HeapInformationClass,
_Out_opt_  PVOID  HeapInformation,
_In_opt_    SIZE_T  HeapInformationLength,
_Out_opt_  PSIZE_T  ReturnLength);

NTSTATUS  RtlSetHeapInformation(
_In_  PVOID  HeapHandle,
_In_  HEAP_INFORMATION_CLASS  HeapInformationClass,
_In_opt_  PVOID  HeapInformation,
_In_opt_  SIZE_T  HeapInformationLength);
1
2
3
4
5
6
7
8
9
10
11
12

这些函数的使用模式与我们之前见过的其他查询/设置函数非常相似。以下是支持的信息类(部分定义在 <WinNt.h> 头文件中):

typedef enum _HEAP_INFORMATION_CLASS
{
    HeapCompatibilityInformation          = 0,
    HeapEnableTerminationOnCorruption     = 1,
    HeapExtendedInformation               = 2,
    HeapOptimizeResources                 = 3,
    HeapTaggingInformation                = 4,
    HeapStackDatabase                     = 5,
    HeapMemoryLimit                       = 6,
    HeapTag                               = 7,
    HeapDetailedFailureInformation        = (int)0x80000001,
    HeapSetDebuggingInformation           = (int)0x80000002
} HEAP_INFORMATION_CLASS;
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.7.3 堆兼容性信息(HeapCompatibilityInformation,0)

使用该信息类(information class)查询时,会返回一个无符号长整数(ULONG),用于描述所查询堆(heap)的某些特性。对于段堆(segment heap),该值始终为 2,意味着低碎片堆(Low Fragmentation Heap,LFH)层处于激活状态。对于标准堆(standard heap),该值可能为 0(未应用 LFH)或 2(已应用 LFH)。

在设置操作中,仅可对标准堆启用 LFH。这还要求堆具备可增长性(growable),且创建时未设置 HEAP_NO_SERIALIZE 标志。通常无需手动启用 LFH,因为堆管理器(Heap Manager)会在判断启用 LFH 有益时自动启用它。可使用来自 phnt 的以下枚举(enumeration):

typedef enum _HEAP_COMPATIBILITY_MODE {
    HEAP_COMPATIBILITY_STANDARD = 0UL,
    HEAP_COMPATIBILITY_LAL = 1UL,       // 不适用(N/A)
    HEAP_COMPATIBILITY_LFH = 2UL,
} HEAP_COMPATIBILITY_MODE;
1
2
3
4
5

# 8.7.4 堆损坏时终止进程(HeapEnableTerminationOnCorruption)

该信息类支持查询或设置一项选项:若检测到堆损坏(heap corruption),进程应立即终止(terminate),而非抛出可能被以某种方式处理但通常无法有效解决问题的异常(exception)。

预期的数据类型为无符号长整数(ULONG),非零值表示启用“堆损坏时终止进程”功能。目前,所有堆默认启用此功能。

# 8.7.5 堆扩展信息(HeapExtendedInformation)

该信息类通过 HEAP_EXTENDED_INFORMATION 结构体(structure)及其相关结构体,返回所查询堆或进程中所有堆的详细信息:

typedef struct _PROCESS_HEAP_INFORMATION {
    ULONG_PTR ReserveSize;
    ULONG_PTR CommitSize;
    ULONG NumberOfHeaps;
    ULONG_PTR FirstHeapInformationOffset;
} PROCESS_HEAP_INFORMATION, *PPROCESS_HEAP_INFORMATION;

typedef struct _HEAP_INFORMATION {
    ULONG_PTR Address;
    ULONG Mode;
    ULONG_PTR ReserveSize;
    ULONG_PTR CommitSize;
    ULONG_PTR FirstRegionInformationOffset;
    ULONG_PTR NextHeapInformationOffset;
} HEAP_INFORMATION, *PHEAP_INFORMATION;

typedef struct _HEAP_EXTENDED_INFORMATION {
    HANDLE Process;
    ULONG_PTR Heap;
    ULONG Level;
    PVOID CallbackRoutine;
    PVOID CallbackContext;
    union {
        PROCESS_HEAP_INFORMATION ProcessHeapInformation;
        HEAP_INFORMATION HeapInformation;
    };
} HEAP_EXTENDED_INFORMATION, *PHEAP_EXTENDED_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

输入时,RtlQueryHeapInformation 函数的堆句柄(heap handle)参数不会被使用。相反,HEAP_EXTENDED_INFORMATION 的前五个成员决定了要请求的信息。该信息类的一个强大功能是能够从其他进程获取堆详情——通过 Process 成员传入目标进程的句柄(handle)即可。若调用进程(calling process)即为目标进程,Process 成员可设为 NtCurrentProcess()。若为跨进程查询,目标进程的句柄需具备较高权限,包括 PROCESS_QUERY_INFORMATION、PROCESS_CREATE_THREAD、PROCESS_VM_OPERATION、PROCESS_VM_READ、PROCESS_DUP_HANDLE 和 PROCESS_VM_WRITE。有时,仅这些权限仍不足,可能还需要 PROCESS_SET_QUOTA 和 PROCESS_SET_INFORMATION 权限。这是因为跨进程查询时,会在目标进程中创建一个线程(thread)来获取信息,并与目标进程共享一个区段对象(Section object)。

我发现,对于此类查询,请求 PROCESS_ALL_ACCESS 权限或使用 MAXIMUM_ALLOWED 权限会更便捷,可尝试这些权限是否能满足访问需求。

Level 成员指定了调用所需的详情级别。最通用的级别是 HEAP_INFORMATION_LEVEL_PROCESS(值为 1),该级别会填充 ProcessHeapInformation 成员。以下是示例代码(省略了错误处理):

#define HEAP_INFORMATION_LEVEL_PROCESS 1

void DisplayProcessHeapsTotals(HANDLE hProcess) {
    HEAP_EXTENDED_INFORMATION info{};
    info.Process = hProcess;
    info.Level = HEAP_INFORMATION_LEVEL_PROCESS;

    RtlQueryHeapInformation(nullptr, HeapExtendedInformation,
        &info, sizeof(info), nullptr);
    printf("总堆数(Total heaps): %u  已提交大小(Commit): 0x%zX  已保留大小(Reserved): 0x%zX\n",
        info.ProcessHeapInformation.NumberOfHeaps,
        info.ProcessHeapInformation.CommitSize,
        info.ProcessHeapInformation.ReserveSize);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

phnt 头文件(header)中,HeapExtendedInformation 等常量以宏(macro)形式定义,而非枚举(enum)——因为 <WinNt.h> 中虽存在相关枚举,但未包含完整列表。我简化了代码,未添加向 HEAP_INFORMATION_CLASS 的强制类型转换(hard cast),尽管我更倾向于将该转换包含在宏中,或定义一个略有不同的枚举名称。这部分内容留给感兴趣的读者自行实现。

下一个级别是 HEAP_INFORMATION_LEVEL_HEAP(值为 2),可用于获取进程中每个堆的详情,同时也包含上述进程堆的总体信息。若需同时获取这两类信息,建议通过一次调用完成,而非两次单独调用。返回结果中,ProcessHeapInformation 成员的填充方式与前一个级别相同,但 NextHeapInformationOffset 会存储第一个堆详情的偏移量(在 HEAP_INFORMATION_LEVEL_PROCESS 级别下,该值为 0)。

通过该偏移量,后续数据均为 HEAP_INFORMATION 结构体,每个结构体描述一个堆,其中 NextHeapInformationOffset 指向下一个堆的信息。以下代码片段通过一次调用获取进程的总体详情及所有堆的详情:

// hProcess 是目标进程的句柄
// 为简化起见,假设堆数不超过 128 个
auto size = 128 * sizeof(HEAP_INFORMATION) + sizeof(HEAP_EXTENDED_INFORMATION);
auto buffer = std::make_unique<BYTE[]>(size);
auto info = (HEAP_EXTENDED_INFORMATION*)buffer.get();
info->Process = hProcess;
info->Level = HEAP_INFORMATION_LEVEL_HEAP;
RtlQueryHeapInformation(nullptr, HeapExtendedInformation, info, size, nullptr);

// 定位到第一个堆
auto hi = (HEAP_INFORMATION*)(buffer.get() +
    info->ProcessHeapInformation.FirstHeapInformationOffset);
auto heaps = info->ProcessHeapInformation.NumberOfHeaps;
printf("总堆数(Total heaps): %u  已提交大小(Commit): 0x%zX  已保留大小(Reserved): 0x%zX\n",
    heaps, info->ProcessHeapInformation.CommitSize,
    info->ProcessHeapInformation.ReserveSize);

for (ULONG i = 0; i < heaps; i++) {
    // 输出堆详情
    printf("堆(Heap) %2d: 地址(Addr): 0x%p  已提交大小(Commit): 0x%08zX  已保留大小(Reserve): 0x%08zX  %s\n",
        i + 1, (PVOID)hi->Address, hi->CommitSize, hi->ReserveSize,
        hi->Mode ? "(LFH)" : "");

    // 移动到下一个堆
    hi = (HEAP_INFORMATION*)(buffer.get() + hi->NextHeapInformationOffset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

以下是上述代码的示例输出(完整源代码包含在 Heaps 示例中):

总堆数(Total) 16 已提交大小(Commit): 0x1DE20000 已保留大小(Reserved): 0x1F418000
堆(Heap)1 地址(Addr): 0x0000000000C50000 已提交大小(Commit): 0x1C287000 已保留大小(Reserve): 0x1CE01000 (LFH)
堆(Heap)2 地址(Addr): 0x0000000000810000 已提交大小(Commit): 0x00001000 已保留大小(Reserve): 0x00010000
堆(Heap)3 地址(Addr): 0x0000000001210000 已提交大小(Commit): 0x01A01000 已保留大小(Reserve): 0x01F9D000 (LFH)
堆(Heap)4 地址(Addr): 0x00000000011F0000 已提交大小(Commit): 0x00051000 已保留大小(Reserve): 0x001D1000 (LFH)
堆(Heap)5 地址(Addr): 0x000000000C030000 已提交大小(Commit): 0x0004A000 已保留大小(Reserve): 0x001D1000 (LFH)
堆(Heap)6 地址(Addr): 0x000000000CBA0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)7 地址(Addr): 0x0000000016AB0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)8 地址(Addr): 0x0000000016AD0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)9 地址(Addr): 0x000000001CBD0000 已提交大小(Commit): 0x000D0000 已保留大小(Reserve): 0x001D1000 (LFH)
堆(Heap)10 地址(Addr): 0x000000001C7F0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)11 地址(Addr): 0x000000001C810000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)12 地址(Addr): 0x000000001A880000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)13 地址(Addr): 0x000000001A8A0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)14 地址(Addr): 0x00000000196A0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)15 地址(Addr): 0x00000000196C0000 已提交大小(Commit): 0x00004000 已保留大小(Reserve): 0x00004000
堆(Heap)16 地址(Addr): 0x0000000014E00000 已提交大小(Commit): 0x00008000 已保留大小(Reserve): 0x000D3000 (LFH)

HEAP_EXTENDED_INFORMATION 的 Heap 成员可用于获取特定堆(而非进程中所有堆)的详情(若该成员值为 0,则查询所有堆)。该值为目标进程上下文中的堆句柄。

更细致的详情级别涉及堆内的内存区域(memory region),对应的结构体如下:

#define HEAP_INFORMATION_LEVEL_REGION 3

typedef struct _HEAP_REGION_INFORMATION {
    ULONG_PTR Address;
    SIZE_T ReserveSize;
    SIZE_T CommitSize;
    ULONG_PTR FirstRangeInformationOffset;
    ULONG_PTR NextRegionInformationOffset;
} HEAP_REGION_INFORMATION, *PHEAP_REGION_INFORMATION;
1
2
3
4
5
6
7
8
9

HEAP_INFORMATION 结构体的 FirstRegionInformationOffset 成员指向该堆的第一个区域描述符(region descriptor),偏移量基于原始缓冲区(original buffer)计算。以下是输出堆的区域信息的示例(完整代码见 Heaps 示例):

void DisplayRegions(PBYTE buffer, HEAP_INFORMATION* hi) {
    if (hi->FirstRegionInformationOffset == 0)    // 无区域信息(no region info)
        return;

    auto region = (HEAP_REGION_INFORMATION*)(buffer +
        hi->FirstRegionInformationOffset);
    for (;;) {
        printf("  区域地址(Region Addr): 0x%p  已提交大小(Commit): 0x%08zX  已保留大小(Reserve): 0x%08zX\n",
            (PVOID)region->Address, region->CommitSize, region->ReserveSize);
        if (region->NextRegionInformationOffset == 0)       // 无更多区域(no more regions)
            break;
        region = (HEAP_REGION_INFORMATION*)(buffer +
            region->NextRegionInformationOffset);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

再下一级别的详情是内存范围(range),对应以下级别值和结构体:

#define HEAP_INFORMATION_LEVEL_RANGE 4

#define HEAP_RANGE_TYPE_COMMITTED 1
#define HEAP_RANGE_TYPE_RESERVED    2

typedef struct _HEAP_RANGE_INFORMATION {
    ULONG_PTR Address;
    SIZE_T Size;
    ULONG Type;
    ULONG Protection;
    ULONG_PTR FirstBlockInformationOffset;
    ULONG_PTR NextRangeInformationOffset;
} HEAP_RANGE_INFORMATION, *PHEAP_RANGE_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13

以下是枚举内存范围的示例:

void DisplayRanges(PBYTE buffer, HEAP_REGION_INFORMATION* region) {
    if (region->FirstRangeInformationOffset == 0)
        return;

    auto range = (HEAP_RANGE_INFORMATION*)(buffer + region->FirstRangeInformationOffset);
    for (;;) {
        printf("    地址(Addr): 0x%p  大小(Size): 0x%08zX  类型(Type): %s  保护属性(Prot): %s\n",
            (PVOID)range->Address, range->Size,
            RangeTypeToString(range->Type),
            ProtectionToString(range->Protection).c_str());
        if (range->NextRangeInformationOffset == 0)
            break;
        range = (HEAP_RANGE_INFORMATION*)(buffer +
            range->NextRangeInformationOffset);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

RangeTypeToString 函数根据范围类型返回“已提交(Committed)”或“已保留(Reserved)”。ProtectionToString 是前文提及的函数,用于将保护属性常量(如 PAGE_READWRITE)转换为字符串。

最细致的详情级别(位于内存范围内)是内存块(block),对应以下级别值和结构体:

#define HEAP_INFORMATION_LEVEL_BLOCK 5

#define HEAP_BLOCK_BUSY                               1
#define HEAP_BLOCK_EXTRA_INFORMATION  2
#define HEAP_BLOCK_LARGE_BLOCK                4
#define HEAP_BLOCK_LFH_BLOCK                    8

typedef struct _HEAP_BLOCK_INFORMATION {
    ULONG_PTR Address;
    ULONG Flags;        // 参见上述标志(see flags above)
    SIZE_T DataSize;
    SIZE_T OverheadSize;
    ULONG_PTR NextBlockInformationOffset;
} HEAP_BLOCK_INFORMATION, *PHEAP_BLOCK_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

其数据结构模式与前文一致:一个内存范围通过 FirstBlockInformationOffset 指向其第一个内存块。以下是根据内存范围枚举内存块的示例:

void DisplayBlocks(PBYTE buffer, HEAP_RANGE_INFORMATION* range) {
    if (range->FirstBlockInformationOffset == 0)
        return;

    auto block = (HEAP_BLOCK_INFORMATION*)(buffer +
        range->FirstBlockInformationOffset);
    for (;;) {
        printf("         块地址(Block Addr): 0x%p  数据大小(Size): 0x%04zX  开销大小(Overhead): %2zd  %s\n",
            (PVOID)block->Address, block->DataSize, block->OverheadSize,
            BlockFlagsToString(block->Flags).c_str());
        if (block->NextBlockInformationOffset == 0)
            break;

        block = (HEAP_BLOCK_INFORMATION*)(buffer +
            block->NextBlockInformationOffset);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

BlockFlagsToString 函数用于将标志转换为字符串:

std::string BlockFlagsToString(ULONG flags) {
    static const struct {
        ULONG flag;
        PCSTR text;
    } data[] = {
        { HEAP_BLOCK_BUSY, "忙碌(Busy)" },
        { HEAP_BLOCK_EXTRA_INFORMATION, "额外信息(Extra)" },
        { HEAP_BLOCK_LARGE_BLOCK, "大块(Large)" },
        { HEAP_BLOCK_LFH_BLOCK, "LFH 块(LFH)" },
    };

    std::string text;
    for (auto& d : data) {
        if ((d.flag & flags) == d.flag)
            (text += d.text) += ", ";
    }
    return text.empty() ? "" : text.substr(0, text.length() - 2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

内存块级别是最细致的详情级别,但如果设置了 HEAP_BLOCK_EXTRA_INFORMATION 标志,则表示该内存块存在额外信息,且这些信息紧跟在 HEAP_BLOCK_INFORMATION 结构体之后。其头部(header)定义如下:

typedef struct _HEAP_BLOCK_EXTRA_INFORMATION {
    BOOLEAN Next;
    ULONG Type;
    SIZE_T Size;
    // 后续为数据(data follows)
} HEAP_BLOCK_EXTRA_INFORMATION, *PHEAP_BLOCK_EXTRA_INFORMATION;
1
2
3
4
5
6

Next 成员指示当前额外信息块之后是否还有其他额外信息块。Type 为额外信息类型,目前仅支持值为 1。额外信息结构体本身定义如下:

typedef struct _HEAP_BLOCK_SETTABLE_INFORMATION {
    SIZE_T Settable;
    USHORT TagIndex;
    USHORT AllocatorBackTraceIndex;
} HEAP_BLOCK_SETTABLE_INFORMATION, *PHEAP_BLOCK_SETTABLE_INFORMATION;
1
2
3
4
5

前文讨论 RtlWalkHeap 函数时曾提及类似信息。

# 8.7.5.1 回调函数(Callback)

HEAP_EXTENDED_INFORMATION 包含 CallbackRoutine 和 CallbackContext 成员。若 CallbackRoutine 不为空,则它指向一个回调函数(callback),该函数会在枚举每个项时被调用。这意味着,无论 Level 值如何,提供给 RtlQueryHeapInformation 的缓冲区大小只需为 sizeof(HEAP_EXTENDED_INFORMATION),无需更大。无需额外分配内存,因为回调函数会根据 Level 值,为每个请求的项(堆、范围、区域、块)被调用一次。

回调函数的原型(prototype)及关联结构体如下:

typedef struct _HEAP_INFORMATION_ITEM {
    ULONG Level;         // 报告的级别(reported level)
    SIZE_T Size;
    union {
        PROCESS_HEAP_INFORMATION ProcessHeapInformation;
        HEAP_INFORMATION HeapInformation;
        HEAP_REGION_INFORMATION HeapRegionInformation;
        HEAP_RANGE_INFORMATION HeapRangeInformation;
        HEAP_BLOCK_INFORMATION HeapBlockInformation;
        ULONG_PTR DynamicStart;
    };
} HEAP_INFORMATION_ITEM, *PHEAP_INFORMATION_ITEM;

NTSTATUS HeapInformationCallback(
    _In_ PHEAP_INFORMATION_ITEM item,
    _In_ PVOID context);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

回调函数返回 STATUS_SUCCESS 会继续枚举,返回失败状态(failure status)则会中止后续调用,且该失败状态会从 RtlQueryHeapInformation 函数返回。

# 8.7.6 堆资源优化(HeapOptimizeResources)

该信息类仅对 RtlSetHeapInformation 函数有效,且需使用以下结构体:

typedef struct _HEAP_OPTIMIZE_RESOURCES_INFORMATION {
    ULONG Version;
    ULONG Flags;
} HEAP_OPTIMIZE_RESOURCES_INFORMATION, *PHEAP_OPTIMIZE_RESOURCES_INFORMATION;
1
2
3
4

目前,Version 必须设为 1,Flags 必须设为 0。

该调用会尝试通过释放(decommit)当前未使用的区域来优化堆。若 RtlSetHeapInformation 函数的堆句柄参数为 NULL,则会优化进程中的所有堆;若指定了特定堆句柄,则仅优化该堆。

# 8.7.7 堆内存限制(HeapMemoryLimit)

该信息类可通过 RtlSetHeapInformation 函数为指定堆设置一些限制。对应的信息结构体如下:

typedef struct _RTL_HEAP_MEMORY_LIMIT_DATA {
    SIZE_T CommitLimitBytes;
    ULONG_PTR CommitLimitFailureCode;
    SIZE_T MaxAllocationSizeBytes;
    ULONG_PTR AllocationLimitFailureCode;
} RTL_HEAP_MEMORY_LIMIT_DATA, *PRTL_HEAP_MEMORY_LIMIT_DATA;

typedef struct _RTL_HEAP_MEMORY_LIMIT_INFO {
    ULONG Version;    // 必须设为 1
    RTL_HEAP_MEMORY_LIMIT_DATA Data;
} RTL_HEAP_MEMORY_LIMIT_INFO, *PRTL_HEAP_MEMORY_LIMIT_INFO;
1
2
3
4
5
6
7
8
9
10
11

上述成员的含义不言自明。大小参数必须以字节(byte)为单位,且为页面大小(page size)的整数倍。其余信息类要么无法使用,要么需要进一步研究。

# 8.8 总结

本章涵盖了与内存相关的大量内容,从虚拟内存 API(Virtual APIs)到堆。但这并非所有相关内容。在第 13 章中,我们将介绍更多与内存相关的 API,包括区段对象(Section object)、内存区(memory zone)和后备列表(lookaside list)。

第7章:对象与句柄
第9章:I/O

← 第7章:对象与句柄 第9章:I/O→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式