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 章:内存(第一部分)
  • 第9章:I/O
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
    • 12.1 节
      • 12.1.1 创建节
      • 12.1.2 映射节
      • 12.1.3 示例:简单共享
      • 12.1.4 查询节信息
      • 12.1.5 其他节相关API
    • 12.2 内存区域
      • 12.2.1 示例:内存区域
    • 12.3 后备列表
    • 12.4 总结
  • 第13章 注册表
目录

第12章 内存(第二部分)

# 第12章 内存(第二部分)

第8章介绍了与内存相关的标准原生API(native APIs)。本章将重点讲解节内核对象(Section kernel objects),这类对象能够实现文件到内存的映射,以及进程间的内存共享。此外,还将描述NtDll.dll提供的不太为人熟知的内存管理对象——内存区域(memory zones)和后备列表(lookaside lists)。

本章内容包括:

  • 节(Sections)
  • 内存区域(Memory Zones)
  • 后备列表(Lookaside Lists)

# 12.1 节

节内核对象支持将文件映射到内存,同时允许多个进程共享该内存。节所使用的文件可以是页面文件(page file)——也就是说,内存由页面文件提供支持,这意味着一旦节对象被销毁,该内存中的数据也会丢失。

Windows API中将节对象称为“内存映射文件(Memory Mapped Files)”。

“页面文件(the page file)”这一术语看似表示只有一个页面文件,但实际上Windows最多支持16个页面文件。一个节可以使用任意页面文件的空间。

本节(无双关之意)将介绍与节对象相关的原生API。其中部分API在Windows驱动程序工具包(Windows Driver Kit,WDK)中有间接文档说明,我们会明确指出这一点。

# 12.1.1 创建节

节对象可以选择是否命名,与其他可命名对象类型(如互斥体(mutex)、信号量(semaphore)、事件(event))类似,既可以创建命名或未命名的节对象,也可以基于已存在的命名对象打开节对象。创建或打开节对象的简便函数是NtCreateSection:

NTSTATUS  NtCreateSection(
    _Out_  PHANDLE  SectionHandle,
    _In_  ACCESS_MASK  DesiredAccess,
    _In_opt_  POBJECT_ATTRIBUTES  ObjectAttributes,
    _In_opt_  PLARGE_INTEGER  MaximumSize,
    _In_  ULONG  SectionPageProtection,
    _In_  ULONG  AllocationAttributes,
    _In_opt_  HANDLE  FileHandle);
1
2
3
4
5
6
7
8

该函数在WDK中有文档说明(对应ZwCreateSection)。Windows API中的CreateFileMapping(Numa)函数会调用NtCreateSection。

SectionHandle是函数执行成功后返回的句柄。DesiredAccess是请求的访问权限,创建新节时通常指定为SECTION_ALL_ACCESS。其他节访问权限位在WinNt.h中定义(包括SECTION_MAP_READ、SECTION_MAP_WRITE、SECTION_MAP_EXECUTE、SECTION_QUERY和SECTION_EXTEND_SIZE)。

ObjectAttributes是常规的OBJECT_ATTRIBUTES结构,若指定该参数,可以为节设置名称。如果节已存在,NtCreateSection会执行失败,除非OBJECT_ATTRIBUTES中包含OBJ_OPENIF标志——在此情况下,只要调用者拥有请求的访问权限,函数就会返回一个句柄。MaximumSize指定最大内存大小:如果内存由页面文件提供支持,该参数直接指定最大内存大小;如果由文件提供支持,该参数则用于设置文件大小。SectionPageProtection是页面保护常量(page protection constants)之一,映射文件用于只读访问时通常指定为PAGE_READONLY,其他情况常指定为PAGE_READWRITE。

AllocationAttributes指定CreateFileMapping文档中说明的可选标志。若指定为0,则会被解释为SEC_COMMIT,这是一个合理的默认值,表示后续任何映射都会预先提交视图(commit the view upfront)。最后,FileHandle是可选的后备文件句柄,若为NULL,则表示没有显式的后备文件,将使用页面文件。

Windows 10 1809及更高版本支持扩展函数NtCreateSectionEx:

NTSTATUS  NtCreateSectionEx(
    _Out_  PHANDLE  SectionHandle,
    _In_  ACCESS_MASK  DesiredAccess,
    _In_opt_  POBJECT_ATTRIBUTES  ObjectAttributes,
    _In_opt_  PLARGE_INTEGER  MaximumSize,
    _In_  ULONG  SectionPageProtection,
    _In_  ULONG  AllocationAttributes,
    _In_opt_  HANDLE  FileHandle,
    _Inout_updates_opt_(ExtParamCount)  PMEM_EXTENDED_PARAMETER  ExtendedParameters,
    _In_  ULONG  ExtParamCount);
1
2
3
4
5
6
7
8
9
10

Windows API中的CreateFileMapping2函数会调用NtCreateSectionEx。

NtCreateSectionEx对NtCreateSection进行了泛化,通过MEM_EXTENDED_PARAMETER结构数组提供可扩展的自定义选项,该结构数组在第8章介绍NtAllocateVirtualMemoryEx时也曾遇到过,更多相关信息可参考第8章。

如果要通过名称查找节对象,可以使用NtOpenSection函数。如果找不到具有所需访问权限的对象,该函数会执行失败:

NTSTATUS  NtOpenSection(
    _Out_  PHANDLE  SectionHandle,
    _In_  ACCESS_MASK  DesiredAccess,
    _In_  POBJECT_ATTRIBUTES  ObjectAttributes);
1
2
3
4

与常规情况一致,名称必须在OBJECT_ATTRIBUTES结构中指定。

# 12.1.2 映射节

获取节句柄后,下一步是使用该句柄将节所表示的内存(全部或部分)映射到进程地址空间。这一功能由NtMapViewOfSection(Ex)函数实现:

NTSTATUS  NtMapViewOfSection(
    _In_  HANDLE  SectionHandle,
    _In_  HANDLE  ProcessHandle,
    _Inout_  PVOID  *BaseAddress,
    _In_  ULONG_PTR  ZeroBits,
    _In_  SIZE_T  CommitSize,
    _Inout_opt_  PLARGE_INTEGER  SectionOffset,
    _Inout_  PSIZE_T  ViewSize,
    _In_  SECTION_INHERIT  InheritDisposition,
    _In_  ULONG  AllocationType,
    _In_  ULONG  Win32Protect);

NTSTATUS  NtMapViewOfSectionEx(    // Windows 10 1803及以上版本
    _In_  HANDLE  SectionHandle,
    _In_  HANDLE  ProcessHandle,
    _Inout_  PVOID  *BaseAddress,
    _Inout_opt_  PLARGE_INTEGER  SectionOffset,
    _Inout_  PSIZE_T  ViewSize,
    _In_  ULONG  AllocationType,
    _In_  ULONG  PageProtection,
    _Inout_updates_opt_(ExtParamCount)  PMEM_EXTENDED_PARAMETER  ExtendedParameters,
    _In_  ULONG  ExtParamCount);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Windows API中的MapViewOfFile/Ex/Numa函数会调用NtMapViewOfSection。MapViewOfFile2和MapViewOfFileNuma2函数同样会调用NtMapViewOfSection。MapViewOfFile3函数则会调用NtMapViewOfSectionEx。

SectionHandle是表示映射的节句柄。ProcessHandle是要将内存映射到的目标进程句柄。通常情况下,目标进程是调用进程(可通过NtCurrentProcess获取),但也可以是其他进程——此时该句柄必须具有PROCESS_VM_OPERATION访问权限。

BaseAddress是输入/输出参数,指定目标进程中用于映射的地址。若指定为NULL,系统会在进程地址空间中查找合适的地址范围;否则,将使用指定的地址(向下取整到页面边界),如果该地址范围不可用,映射会执行失败。ZeroBits指定结果地址中必须为零的高位比特数(仅当*BaseAddress为NULL时有效)。通常指定为0,表示调用者没有特殊限制。

CommitSize是为该视图初始提交的内存大小(向上取整到页面边界),仅对页面文件支持的节有意义。通常指定为0,意味着内存会“按需提交”(即访问时才提交)。

SectionOffset是映射的起始偏移量(向下取整到页面边界)。若传入NULL,映射将从节的起始位置(偏移量0)开始。ViewSize是输入/输出参数,指定要映射的大小,函数返回时会给出实际映射的大小。若指定为0,视图将从SectionOffset映射到节的末尾;否则,大小会向上取整到页面边界。

InheritDisposition指定视图是否应被所有子进程继承(ViewShare表示继承,ViewUnmap表示不继承)。AllocationFlags通常指定为0,但也可以包含MEM_LARGE_PAGES、MEM_TOP_DOWN和MEM_RESERVE等标志,更多详细信息可参考VirtualAlloc的文档说明。

最后,Win32Protect是映射视图请求的页面保护权限(例如PAGE_READONLY、PAGE_READWRITE),该权限必须与节创建时的初始保护标志兼容。NtMapViewOfSection调用成功后,*BaseAddress中会返回映射后的地址,通过该地址可访问对应的内存。

使用NtMapViewOfSectionEx时,借助之前遇到的MEM_EXTENDED_PARAMETER数组可以实现更多自定义功能,更多信息可参考MapViewOfFile2的文档说明。

当视图不再需要时,应调用NtUnmapViewOfSection(Ex)函数解除映射:

NTSTATUS  NtUnmapViewOfSection(
    _In_  HANDLE  ProcessHandle,
    _In_opt_  PVOID  BaseAddress);

NTSTATUS  NtUnmapViewOfSectionEx(
    _In_  HANDLE  ProcessHandle,
    _In_opt_  PVOID  BaseAddress,
    _In_  ULONG  Flags);
1
2
3
4
5
6
7
8

函数的必填参数是进程句柄和NtMapViewOfSection(Ex)返回的基地址。NtUnmapViewOfSection函数会调用NtUnmapViewOfSectionEx,并将Flags参数设置为0。其他可用标志包括MEM_UNMAP_WITH_TRANSIENT_BOOST(值为1)和MEM_PRESERVE_PLACEHOLDER(值为2):其中MEM_UNMAP_WITH_TRANSIENT_BOOST表示由于调用者预计很快会重新映射这些页面,应对要解除映射的页面进行临时页面优先级提升;MEM_PRESERVE_PLACEHOLDER表示将页面恢复到“占位符”状态。更多信息可参考MapViewOfFile2的文档说明。

Windows API中的NtUnmapViewOfFile2函数会调用NtUnmapViewOfSectionEx。

# 12.1.3 示例:简单共享

以下示例项目使用前面介绍的主要节API,创建了一个简单的双进程“聊天”程序——两个进程通过共享内存传递字符串。

完整代码位于SharedMem项目中。

我们将使用一个事件对象(event object)来同步共享内存的读写操作:一个进程写入数据完成后,另一个进程读取数据,之后两者切换角色。

首先需要创建一个命名的节对象,以便于共享。该名称应在会话的命名空间(session’s namespace)中可见,因此需要先获取当前会话的基目录,代码如下:

#include  <string>
#include  <format>

std::wstring  GetBaseDirectory()
{
    ULONG  session;
    if  ( !NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(),
        ProcessSessionInformation,  &session,  sizeof(session),  nullptr)))
        return  L"" ;

    return  std::format(L"\\Sessions\\{}\\BaseNamedObjects\\ " ,  session);
}
1
2
3
4
5
6
7
8
9
10
11
12

调用NtQueryInformationProcess函数并指定ProcessSessionInformation参数,可获取会话ID(session ID),进而构建完整的路径字符串。也可以直接使用会话0的命名空间(\BaseNamedObjects),这在某些场景下可能更为合适。需要注意的是,在会话0的命名空间中创建节对象通常需要管理员权限。

上述函数使用了C++ 20的std::format函数来格式化名称,也可以使用swprintf_s等类似函数实现相同效果。

接下来,初始化OBJECT_ATTRIBUTES结构,为节对象设置自定义名称:

int  main()
{
    auto  baseDir  =  GetBaseDirectory();
    if  (baseDir.empty())
        return  1;

    HANDLE  hSection;
    OBJECT_ATTRIBUTES  secAttr;
    UNICODE_STRING  secName;
    auto  fullSecName  =  baseDir  +  L"MySharedMem" ;
    RtlInitUnicodeString(&secName,  fullSecName .c_str());
    InitializeObjectAttributes(&secAttr,  &secName,  OBJ_OPENIF,  nullptr ,  nullptr);
1
2
3
4
5
6
7
8
9
10
11
12

注意OBJ_OPENIF标志的作用:如果节名称已存在,则打开对应的句柄,而非执行失败。

现在可以创建/打开一个大小为8KB(可设置为任意大小)的节对象:

    LARGE_INTEGER  size;
    size.QuadPart  =  1  <<  13;        //  8KB

    auto  status  =  NtCreateSection(&hSection,  SECTION_ALL_ACCESS,
        &secAttr,  &size,  PAGE_READWRITE,  SEC_COMMIT,  nullptr);
    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  create/open  section  (0x%X)\n " ,  status);
        return  status;
    }
1
2
3
4
5
6
7
8
9
10

如果在调用NtCreateSection之前该对象已存在,则当前进程将以读取者身份启动,之后会等待通知:

    bool  wait  =  false;
    if   (status  ==  STATUS_OBJECT_NAME_EXISTS)
    {
        //
        //  该对象已由其他进程创建
        //
        wait  =  true;
    }
1
2
3
4
5
6
7
8

接下来,将节对象映射到当前进程的地址空间。由于节对象很小(仅8KB),我们将映射整个节:

    PVOID  address  =  nullptr;
    SIZE_T  viewSize  =  0;
    status  =  NtMapViewOfSection(hSection,  NtCurrentProcess(),  &address, 0 ,  0 ,  nullptr ,  &viewSize,  ViewUnmap,  0,  PAGE_READWRITE);
    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  map  section  (0x%X)\n " ,  status);
        return  status;
    }
1
2
3
4
5
6
7
8

然后,创建一个用于同步的事件对象,名称为“MySharedMemDataReady”:

    OBJECT_ATTRIBUTES  evtAttr;
    UNICODE_STRING  evtName;
    auto  fullEvtName  =  baseDir  +  L"MySharedMemDataReady" ;
    RtlInitUnicodeString(&evtName,  fullEvtName .c_str());
    InitializeObjectAttributes(&evtAttr,  &evtName,  OBJ_OPENIF,  nullptr ,  nullptr);
    HANDLE  hEvent;
    status  =  NtCreateEvent(&hEvent,  EVENT_ALL_ACCESS,  &evtAttr, SynchronizationEvent,  FALSE);
    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  create/open  event  (0x%X)\n " ,  status);
        return  status;
    }
1
2
3
4
5
6
7
8
9
10
11
12

至此,已准备就绪,可以通过读写共享内存交换信息:

    char  text[128];
    for(;;)
    {
        if   (wait)
        {
            printf("Waiting  for  data . . .\n " );
            NtWaitForSingleObject(hEvent,  FALSE,  nullptr);
            printf("%s\n " ,  (PCSTR)address);
        }
        else
        {
            printf(">  ");
            gets_s(text);
            strcpy_s((PSTR)address,  sizeof(text),  text);
            NtSetEvent(hEvent,  nullptr);
            if  (strcmp(text,  "quit")  ==  0)
                break;
        }
        wait  =   !wait;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

处于等待状态的进程会调用NtWaitForSingleObject函数,等待事件被触发(signaled),之后通过printf输出共享内存中的内容。其他情况下,程序会读取用户输入的文本并写入共享内存,随后触发事件(KeSetEvent)。

当用户输入“quit”时,循环退出,此时可以执行清理操作:

    NtUnmapViewOfSection(NtCurrentProcess(),  address);
    NtClose(hEvent);
    NtClose(hSection);
1
2
3

以下是完整的main函数,方便参考:

int  main()
{
    auto  baseDir  =  GetBaseDirectory();

    if  (baseDir.empty())
        return  1;

    HANDLE  hSection;
    OBJECT_ATTRIBUTES  secAttr;
    UNICODE_STRING  secName;
    auto  fullSecName  =  baseDir  +  L"MySharedMem" ;
    RtlInitUnicodeString(&secName,  fullSecName .c_str());
    InitializeObjectAttributes(&secAttr,  &secName,
        OBJ_OPENIF,  nullptr ,  nullptr);

    LARGE_INTEGER  size;
    size.QuadPart  =  1  <<  13;        //  8KB

    auto  status  =  NtCreateSection(&hSection,  SECTION_ALL_ACCESS,
        &secAttr,  &size,  PAGE_READWRITE,  SEC_COMMIT,  nullptr);
    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  create/open  section  (0x%X)\n " ,  status);
        return  status;
    }

    bool  wait  =  false;
    if   (status  ==  STATUS_OBJECT_NAME_EXISTS)
    {
        wait  =  true;
    }

    PVOID  address  =  nullptr;
    SIZE_T  viewSize  =  0;
    status  =  NtMapViewOfSection(hSection,  NtCurrentProcess(),  &address, 0 ,  0 ,  nullptr ,  &viewSize,  ViewUnmap,  0,  PAGE_READWRITE);

    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  map  section  (0x%X)\n " ,  status);
        return  status;
    }

    OBJECT_ATTRIBUTES  evtAttr;
    UNICODE_STRING  evtName;
    auto  fullEvtName  =  baseDir  +  L"MySharedMemDataReady" ;
    RtlInitUnicodeString(&evtName,  fullEvtName .c_str());
    InitializeObjectAttributes(&evtAttr,  &evtName,
        OBJ_OPENIF,  nullptr ,  nullptr);
    HANDLE  hEvent;
    status  =  NtCreateEvent(&hEvent,  EVENT_ALL_ACCESS,  &evtAttr, SynchronizationEvent,  FALSE);
    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  create/open  event  (0x%X)\n " ,  status);
        return  status;
    }

    char  text[128];
    for(;;)
    {
        if   (wait)
        {
            printf("Waiting  for  data . . .\n " );
            NtWaitForSingleObject(hEvent,  FALSE,  nullptr);
            printf("%s\n " ,  (PCSTR)address);
        }
        else
        {
            printf(">  ");
            gets_s(text);
            strcpy_s((PSTR)address,  sizeof(text),  text);
            NtSetEvent(hEvent,  nullptr);
            if  (strcmp(text,  "quit")  ==  0)
                break;
        }
        wait  =   !wait;
    }

    NtUnmapViewOfSection(NtCurrentProcess(),  address);
    NtClose(hEvent);
    NtClose(hSection);

    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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

# 12.1.4 查询节信息

NtQuerySection函数用于获取现有节对象的相关信息:

typedef  enum  _SECTION_INFORMATION_CLASS
{
    SectionBasicInformation,
    SectionImageInformation,
    SectionRelocationInformation,
    SectionOriginalBaseInformation,
    SectionInternalImageInformation,
    MaxSectionInfoClass
}  SECTION_INFORMATION_CLASS;

NTSTATUS  NtQuerySection(
    _In_  HANDLE  SectionHandle,
    _In_  SECTION_INFORMATION_CLASS  SectionInformationClass,
    _Out_writes_bytes_ (SectionInformationLength)  PVOID  SectionInformation,
    _In_  SIZE_T  SectionInformationLength,
    _Out_opt_  PSIZE_T  ReturnLength);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

要使该API正常工作,SectionHandle必须具有SECTION_QUERY访问权限。

当信息类别(SectionInformationClass)指定为SectionBasicInformation时,需要传入SECTION_BASIC_INFORMATION结构:

typedef  struct  _SECTION__BASIC__INFORMATION
{
    PVOID  BaseAddress;
    ULONG  AllocationAttributes;
    LARGE_INTEGER  MaximumSize;
}  SECTION_BASIC_INFORMATION,  *PSECTION_BASIC_INFORMATION;
1
2
3
4
5
6

BaseAddress是创建节时指定SEC_BASED分配属性的情况下所使用的基地址(即所有共享该节的进程中映射的地址)。AllocationAttributes是创建节时使用的标志(例如SEC_COMMIT、SEC_BASED)。最后,MaximumSize是该节可映射的最大内存大小。

当信息类别指定为SectionImageInformation时,函数返回SECTION_IMAGE_INFORMATION结构:

typedef  struct  _SECTION_IMAGE_INFORMATION
{
    PVOID  TransferAddress;
    ULONG  ZeroBits;
    SIZE_T  MaximumStackSize;
    SIZE_T  CommittedStackSize;
    ULONG  SubSystemType;
    union
    {
        struct
        {
            USHORT  SubSystemMinorVersion;
            USHORT  SubSystemMajorVersion;
        }  DUMMYSTRUCTNAME;
        ULONG  SubSystemVersion;
    };
    union
    {
        struct
        {
            USHORT  MajorOperatingSystemVersion;
            USHORT  MinorOperatingSystemVersion;
        };
        ULONG  OperatingSystemVersion;
    };
    USHORT  ImageCharacteristics;
    USHORT  DllCharacteristics;
    USHORT  Machine;
    BOOLEAN  ImageContainsCode;
    union
    {
        UCHAR  ImageFlags;
        struct
        {
            UCHAR  ComPlusNativeReady   :  1;
            UCHAR  ComPlusILOnly   :  1;
            UCHAR  ImageDynamicallyRelocated   :  1;
            UCHAR  ImageMappedFlat   :  1;
            UCHAR  BaseBelow4gb   :  1;
            UCHAR  ComPlusPrefer32bit   :  1;
            UCHAR  Reserved   :  2;
        };
    };
    ULONG  LoaderFlags;
    ULONG  ImageFileSize;
    ULONG  CheckSum;
}  SECTION_IMAGE_INFORMATION,  *PSECTION_IMAGE_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
46
47

该信息类别仅对以镜像文件(指定SEC_IMAGE属性)形式映射文件的节有效。

以下示例展示了如何映射镜像文件并读取其信息(省略了错误处理):

//  打开文件
HANDLE  hFile;
OBJECT_ATTRIBUTES  fileAttr;
UNICODE_STRING  fileName;
RtlInitUnicodeString(&fileName,  L"\\SystemRoot\\System32\\kernelbase.dll");
InitializeObjectAttributes(&fileAttr,  &fileName,  0 ,  nullptr ,  nullptr);
IO_STATUS_BLOCK  ioStatus;
NtOpenFile(&hFile,  FILE_READ_ACCESS,  &fileAttr,  &ioStatus,  FILE_SHARE_READ,  0);

//  基于hFile创建节
NtCreateSection(&hSection,  SECTION_ALL_ACCESS,
    nullptr ,  nullptr,  PAGE_READONLY,  SEC_IMAGE,  hFile);

SECTION_IMAGE_INFORMATION  sii;
NtQuerySection(hSection,  SectionImageInformation,  &sii,  sizeof(sii),  nullptr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

SectionInternalImageInformation信息类别对应一个扩展结构:

typedef  struct  _SECTION_INTERNAL_IMAGE_INFORMATION
{
    SECTION_IMAGE_INFORMATION  SectionInformation;
    union
    {
        ULONG  ExtendedFlags;
        struct
        {
            ULONG  ImageExportSuppressionEnabled:  1;
            ULONG  ImageCetShadowStacksReady:  1;
            ULONG  ImageXfgEnabled:  1;
            ULONG  ImageCetShadowStacksStrictMode:  1;
            ULONG  ImageCetSetContextIpValidationRelaxedMode:  1;
            ULONG  ImageCetDynamicApisAllowInProc:  1;
            ULONG  ImageCetDowngradeReserved1 :  1;
            ULONG  ImageCetDowngradeReserved2 :  1;
            ULONG  Reserved:  24;
        };
    };
}  SECTION_INTERNAL_IMAGE_INFORMATION,  *PSECTION_INTERNAL_IMAGE_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

遗憾的是,SectionInternalImageInformation无法正常使用——该信息仅在内核内部可用,外部无法访问。

最后两个信息类别(SectionRelocationInformation和SectionOriginalBaseInformation)同样仅适用于镜像文件。SectionRelocationInformation返回镜像文件的加载地址(SIZE_T类型),而SectionOriginalBaseInformation返回镜像文件的原始加载地址(PVOID类型)。

# 12.1.5 其他节相关API

NtExtendSection函数仅可扩展文件支持的节(非镜像映射形式)的大小:

NTSTATUS  NtExtendSection(
    _In_  HANDLE  SectionHandle,
    _Inout_  PLARGE_INTEGER  NewSectionSize);
1
2
3

节句柄必须具有SECTION_EXTEND_SIZE访问权限。如果传入的大小小于当前节的大小,该调用将被忽略。

最后,NtAreMappedFilesTheSame函数用于检查两个指定地址是否映射了同一个文件:

NTSTATUS  NtAreMappedFilesTheSame(
    _In_  PVOID  File1MappedAsAnImage,
    _In_  PVOID  File2MappedAsFile);
1
2
3

第一个地址必须位于以镜像形式映射文件的节中,不一定是节的起始地址。第二个地址必须位于映射文件(可以是镜像形式或非镜像形式)的节中。如果两个地址指向同一个文件,函数返回STATUS_SUCCESS;否则返回STATUS_NOT_SAME_DEVICE。

# 12.2 内存区域

内存区域(Memory Zone)是本地(当前进程)对象,用于管理已提交的内存缓冲区。可以在该缓冲区中分配内存,但无法显式释放单个分配的内存块——只能释放整个内存区域。内存区域是后备列表(详见下一节)的实现基础,但自身也具有独立的实用价值。

通过RtlCreateMemoryZone函数创建新的内存区域:

NTSTATUS  RtlCreateMemoryZone(
    _Out_  PVOID  *MemoryZone,
    _In_  SIZE_T  InitialSize,
    _Reserved_  ULONG  Flags);         //  目前无定义的标志
1
2
3
4

MemoryZone是返回的不透明指针(opaque pointer),指向分配的内存区域对象,该对象包含初始缓冲区大小(InitialSize字节)和管理结构本身。函数会分配(提交)InitialSize字节的内存,向上取整到下一个页面边界(同时考虑管理结构的大小)。

管理结构RTL_MEMORY_ZONE的定义可在PHNT库中找到,但为了应对未来Windows版本中可能的结构变更,最好将其视为不透明结构,避免直接操作内部成员。

创建内存区域后,可通过RtlAllocateMemoryZone函数分配内存:

NTSTATUS  RtlAllocateMemoryZone(
    _In_  PVOID  MemoryZone,
    _In_  SIZE_T  BlockSize,
    _Out_  PVOID  *Block);
1
2
3
4

BlockSize是请求分配的字节数。函数执行成功后,*Block中会返回分配的内存块指针。如果新增的分配请求超出当前内存区域的大小,分配将失败。此时,调用者可以先使用RtlExtendMemoryZone函数扩展内存区域的大小,再重新尝试分配:

NTSTATUS  RtlExtendMemoryZone(
    _In_  PVOID  MemoryZone,
    _In_  SIZE_T  Increment);
1
2
3

Increment是要添加到内存区域的字节数。

截至撰写本文时,PHNT头文件中未提供RtlExtendMemoryZone的声明。如需使用该函数,只需添加上述声明即可。

当内存区域不再需要时,可以通过以下函数销毁它:

NTSTATUS  RtlDestroyMemoryZone(_In_  _Post_invalid_  PVOID  MemoryZone);
1

该调用会释放内存区域分配的所有内存。如果需要重复使用内存区域,可以在不销毁它的情况下重置其内容(清空内存):

NTSTATUS  RtlResetMemoryZone(_In_  PVOID  MemoryZone);
1

目前,RtlResetMemoryZone似乎未包含在Visual Studio提供的Ntdll.lib导入库中。除非自行创建导入库,否则需要动态绑定该API,例如:

auto  const  pRtlResetMemoryZone  =  (decltype(RtlResetMemoryZone)*)GetProcAddress(
    GetModuleHandle(L"ntdll"),  "RtlResetMemoryZone");
1
2

最后,可以通过RtlLockMemoryZone函数将内存区域锁定到物理内存中,之后可通过RtlUnlockMemoryZone函数解锁。每次锁定/解锁操作会递增/递减内部的锁定计数:

NTSTATUS  RtlLockMemoryZone(_In_  PVOID  MemoryZone);
NTSTATUS  RtlUnlockMemoryZone(_In_  PVOID  MemoryZone);
1
2

# 12.2.1 示例:内存区域

以下示例展示了内存区域的使用。首先创建一个4KB大小的内存区域(可设置为任意大小):

int  main()
{
    PVOID  zone;
    auto  status  =  RtlCreateMemoryZone(&zone,  1  <<  12 ,  0);
    if   ( !NT_SUCCESS(status))
    {
        printf("Failed  to  create  memory  zone  (0x%X)\n " ,  status);
        return  status;
    }
1
2
3
4
5
6
7
8
9

接下来进行多次内存分配,如果某次分配失败,则扩展内存区域并重新尝试:

    PCHAR  buffers[100]{};

    for  (int  i  =  0;  i  <  _ARRAYSIZE(buffers);  i++)
    {
        PVOID  buffer;
        status  =  RtlAllocateMemoryZone(zone,  128  +  i  *  2 ,  &buffer);
        if   ( !NT_SUCCESS(status))
        {
            printf("Failed  to  allocate  block  %d  (0x%X) .  Extending . . .\n " ,
                i,  status);
            RtlExtendMemoryZone(zone,  256);
            i-- ;
            continue;
        }
        else
        {
            buffers[i]  =  (PCHAR)buffer;
            sprintf_s(buffers[i],  128 ,  "Data  stored  in  block  %d" ,  i);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

现在可以遍历所有分配的内存块并访问其内容:

    for  (int  i  =  0;  i  <  _ARRAYSIZE(buffers);  i++)
    {
        if   (buffers[i])
        {
            printf("%3d:  %s\n " ,  i,  buffers[i]);
        }
    }
1
2
3
4
5
6
7

最后销毁内存区域:

    RtlDestroyMemoryZone(zone);
    return  0;
}
1
2
3

完整源代码位于MemZones项目中。

# 12.3 后备列表

后备列表(Lookaside Lists,或称内存块后备列表Memory Block Lookaside Lists)是一类内存管理对象,通过不真正释放内存、仅将其标记为可用的方式,实现快速分配和释放。它们在底层使用内存区域,但与内存区域不同的是,后备列表支持“释放”单个分配的内存块。后备列表最适合用于固定大小的内存分配,但更一般地说,它支持指定最小和最大块大小的范围。

通过RtlCreateMemoryBlockLookaside函数创建后备列表:

NTSTATUS  RtlCreateMemoryBlockLookaside(
    _Out_  PVOID  *MemoryBlockLookaside,
    _Reserved_  ULONG  Flags,         //  目前无定义的标志
    _In_  ULONG  InitialSize,
    _In_  ULONG  MinimumBlockSize,
    _In_  ULONG  MaximumBlockSize);
1
2
3
4
5
6

后备列表管理对象通过第一个参数返回。InitialSize是预先为后备列表分配(提交)的内存大小。MinimumBlockSize和MaximumBlockSize指定了使用该后备列表进行有效分配的块大小范围。这些值会分别向下(最小值)和向上(最大值)取整到最接近的2的幂。

创建后备列表后,可通过RtlAllocateMemoryBlockLookaside函数分配内存块:

NTSTATUS  RtlAllocateMemoryBlockLookaside(
    _In_  PVOID  MemoryBlockLookaside,
    _In_  ULONG  BlockSize,
    _Out_  PVOID  *Block);
1
2
3
4

BlockSize是要分配的内存块字节数,必须在创建后备列表对象时指定的范围内。分配成功后,*Block中会返回内存块指针。如果后备列表已满(实际上是内部内存区域已满),分配将失败,此时需要扩展后备列表。

要释放内存块(将其标记为可用),调用RtlFreeMemoryBlockLookaside函数:

NTSTATUS  RtlFreeMemoryBlockLookaside(
    _In_  PVOID  MemoryBlockLookaside,
    _In_  PVOID  Block);
1
2
3

Block是RtlAllocateMemoryBlockLookaside函数返回的内存块指针。该内存块会被放回可用块池(块根据其大小分组管理)。

如果需要的内存超过初始请求的大小,则需要扩展后备列表:

NTSTATUS  RtlExtendMemoryBlockLookaside(
    _In_  PVOID  MemoryBlockLookaside,
    _In_  ULONG  Increment);
1
2
3

Increment是要扩展后备列表的字节数。

与内存区域类似,后备列表也支持锁定/解锁操作,或重置操作(有效清除所有分配的内存块):

NTSTATUS  RtlLockMemoryBlockLookaside(_In_  PVOID  MemoryBlockLookaside);
NTSTATUS  RtlUnlockMemoryBlockLookaside(_In_  PVOID  MemoryBlockLookaside);
NTSTATUS  RtlResetMemoryBlockLookaside(_In_  PVOID  MemoryBlockLookaside);
1
2
3

这些函数是对底层使用的内存区域对应操作的轻量级封装。

最后,可以通过以下函数彻底销毁后备列表:

NTSTATUS  RtlDestroyMemoryBlockLookaside(_In_  PVOID  MemoryBlockLookaside);
1

# 12.4 总结

本章介绍了各类与内存管理相关的API。节是内核对象,而内存区域和后备列表是由NtDll.dll内部实现的对象类型——它们并非内核对象,但能为应用程序提供实用的内存管理服务。

第11章 安全性(Security)
第13章 注册表

← 第11章 安全性(Security) 第13章 注册表→

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