首页
论坛
课程
招聘
[原创]Windows内核学习笔记之内存管理
2021-12-27 13:23 19407

[原创]Windows内核学习笔记之内存管理

2021-12-27 13:23
19407

一.Windows内存管理概述

Windows采用页式内存管理方案,在Intel x86处理器上,Windows不适用段来管理虚拟内存,但是,Intel x86处理器在访问内存时必须要通过段描述符,这意味着Windows将所有的段描述符都构造成了从基地址0开始,且段的大小根据段的用户和系统设置的不同,会设置为0x80000000,0xC0000000或0xFFFFFFFF。所以,Windows系统中的代码,包括操作系统本身的代码和应用程序的代码,所面对的地址空间都是线性地址空间。这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

 

Windows使用了两种特权级别:0和3,其中特权级0称为内核模式,特权级3称为用户模式。当处理器指向内核模式代码时,它们处于系统地址空间,位于0x80000000~0xFFFFFFFF,所有的进程共享此空间;当处理器执行用户模式代码时,它们处于进程地址空间,位于0x00000000~0x7FFFFFFF,这部分空间是进程私有的。用户模式代码只能访问进程自身的数据,而内核模式代码不仅可以访问当前进程的数据,也可也访问系统地址空间中的数据。所有的进程,一旦进入到内核模式,则共享同一的系统地址空间。

 

在Windows的每个地址空间中,虚拟地址的分配和回收都必须按照规定执行。Windows规定,应用程序在使用内存以前必须先申请,所以,操作系统内部可以根据应用程序的申请和释放操作来维护好整个虚拟地址空间的内存分配情况。而且,Windows也采用了按需分配的策略,也就是说,只有当一段虚拟地址空间真正被使用的时候,系统才会为它分配页表和物理页面。每个进程的虚拟地址空间的分配情况通过一组虚拟地址描述符(VAD)记录下来,这些描述符构成了有一颗平衡二叉树,以便于快速地定位到一个指定虚拟地址地描述符上。

二.Windows系统内存管理

1.概述

在系统地址空间中,有些部分提供一些特殊模块使用,比如会话空间是由会话管理器和Windows子系统使用的;而换页内存池和非换页内存池则是提供给系统内核模块和设备驱动程序使用的。在换页内存池中分配的内存有可能在物理内存紧缺的情况瞎被换出到外存中;而非换页内存池中分配的内存总是处于物理内存中。为了实现这两种池,Windows使用了两层内存管理。下层是基于页面的内存管理,仅限于执行体内部使用;上层建立在下层的内存管理功能基础之上,对外提供各种粒度的内存服务。两层结果如下图所示

 

avatar

 

系统换页内存池和非换页内存池是Windows系统提供的最基本动态内存管理手段,它以页面为基本粒度来管理系统中划定的地址范围。

 

页面粒度对于一般的代码逻辑而言太大了。所以,为了适应各种内核组件对于内存管理的需要,Windows内核在系统的非换页内存池和换页内存池的基础上,实现了灵活的,可适应各种大小内存需求的内存池,这便是执行体内存池。

 

执行体内存对象是由数据结构POOL_DESCRIPTOR来描述的,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
kd> dt _POOL_DESCRIPTOR
nt!_POOL_DESCRIPTOR
   +0x000 PoolType         : _POOL_TYPE
   +0x004 PoolIndex        : Uint4B
   +0x008 RunningAllocs    : Uint4B
   +0x00c RunningDeAllocs  : Uint4B
   +0x010 TotalPages       : Uint4B
   +0x014 TotalBigPages    : Uint4B
   +0x018 Threshold        : Uint4B
   +0x01c LockAddress      : Ptr32 Void
   +0x020 PendingFrees     : Ptr32 Void
   +0x024 PendingFreeDepth : Int4B
   +0x028 ListHeads        : [512] _LIST_ENTRY
偏移 名称 作用
0x000 PoolType 枚举类型POOL_TYPE,其中的NonPagedPool代表了执行体非分页内存池,PagedPool代表执行体的分页内存池
0x004 PoolIndex 指当前对象在该类型的内存池数组中的索引,对于换页内存池,该域是它在ExpPagedPoolDescriptor数组中的下标值
0x028 ListHeads 执行体内存池对于小内存的分配和回收,是通过一组空间内存块链表实现的
 

以下是WRK中和内存管理有关的一部分全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#if defined(NT_UP)
#define NUMBER_OF_PAGED_POOLS 2
#else
#define NUMBER_OF_PAGED_POOLS 4
#endif
 
ULONG ExpNumberOfPagedPools = NUMBER_OF_PAGED_POOLS;
 
ULONG ExpNumberOfNonPagedPools = 1;
 
POOL_DESCRIPTOR NonPagedPoolDescriptor;
 
#define EXP_MAXIMUM_POOL_NODES 16
 
PPOOL_DESCRIPTOR ExpNonPagedPoolDescriptor[EXP_MAXIMUM_POOL_NODES];
 
#define NUMBER_OF_POOLS 2
PPOOL_DESCRIPTOR PoolVector[NUMBER_OF_POOLS];
 
PPOOL_DESCRIPTOR ExpPagedPoolDescriptor[EXP_MAXIMUM_POOL_NODES + 1];
 
volatile ULONG ExpPoolIndex = 1;

ExpNumberOfNonPagePools代表非换页内存池数量,只有一个;而换页内存池的数量,即变量ExpNumberOfPagedPools,在单处理器系统上是3个,在多处理器系统上有5个。PoolVector是一个包含两个数组的元素,第一个元素PoolVector[0]指向非换页内存池,即NonPagedPoolDescriptor的地址;第二个元素PoolVector[1]指向换页内存池,即指向第一个换页内存池。数组ExpNonPagedPoolDescriptor仅用于有多个非换页内存池的情形;数组ExpPagedPoolDescriptor中的每个元素分别指向一个换页内存池。

 

执行体内存池对象的初始化是按照换页内存池和非换页内存池分开进行的,在Windows系统内核中通过一些列内核函数来使用内存池对象。

2.初始化内存池

执行体内存池的初始化是在InitializePool函数中完成的,函数首先会判断要初始化的内存池是否是非分页内存池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INIT:005EB2F9 ; int __stdcall InitializePool(POOL_TYPE PoolType, ULONG Threshold)
INIT:005EB2F9 _InitializePool@8 proc near             ; CODE XREF: MiInitMachineDependent(x)-1FE3↑p
INIT:005EB2F9                                         ; MiBuildPagedPool()+276↓p
INIT:005EB2F9
INIT:005EB2F9 PoolType        = dword ptr  8
INIT:005EB2F9 Threshold       = dword ptr  0Ch
INIT:005EB2F9
INIT:005EB2F9                 mov     edi, edi
INIT:005EB2FB                 push    ebp
INIT:005EB2FC                 mov     ebp, esp
INIT:005EB2FE                 push    ebx
INIT:005EB2FF                 xor     ebx, ebx        ; ebx清0
INIT:005EB301                 cmp     [ebp+PoolType], ebx ; PoolTpe是否为NonPagedPool
INIT:005EB304                 push    esi
INIT:005EB305                 push    edi
INIT:005EB306                 jnz     loc_5EB21C

如果要初始化非分页内存池,就会判断KeNumberNodes中的数据是否大于1,根据后面代码的内容可推测这个变量保存的是处理器的核数

1
2
3
4
INIT:005EB319 loc_5EB319:                             ; CODE XREF: InitializePool(x,x)+10BB9↓j
INIT:005EB319                 mov     al, _KeNumberNodes
INIT:005EB31E                 cmp     al, 1
INIT:005EB320                 ja      loc_5FBEB7      ; al是否大于1

如果大于1,就会修改全局变量ExpNumberOfNonPagedPools保存的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
INIT:005FBEB7 loc_5FBEB7:                             ; CODE XREF: InitializePool(x,x)+27↑j
INIT:005FBEB7                 movzx   eax, al
INIT:005FBEBA                 cmp     eax, 7Fh
INIT:005FBEBD                 mov     ds:_ExpNumberOfNonPagedPools, eax
INIT:005FBEC2                 jbe     short loc_5FBECC
INIT:005FBEC4                 push    7Fh
INIT:005FBEC6                 pop     eax
INIT:005FBEC7                 mov     ds:_ExpNumberOfNonPagedPools, eax
INIT:005FBECC
INIT:005FBECC loc_5FBECC:                             ; CODE XREF: InitializePool(x,x)+10BC9↑j
INIT:005FBECC                 cmp     eax, 10h
INIT:005FBECF                 jbe     short loc_5FBED9
INIT:005FBED1                 push    10h
INIT:005FBED3                 pop     eax
INIT:005FBED4                 mov     ds:_ExpNumberOfNonPagedPools, eax

调用ExInitializePoolDescriptor来初始化非分页内存池对象,该内存池对象的地址则是全局变量NonPagedPoolDescriptor,且在初始化之前会将地址保存到PoolVector数组中的第一个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
INIT:005EB326 loc_5EB326:                             ; CODE XREF: InitializePool(x,x)+10BE4↓j
INIT:005EB326                                         ; InitializePool(x,x)+10C28↓j
INIT:005EB326                 push    offset _ExpTaggedPoolLock ; SpinLock
INIT:005EB32B                 call    _KeInitializeSpinLock@4 ; KeInitializeSpinLock(x)
INIT:005EB330                 push    ebx             ; PoolLock
INIT:005EB331                 push    [ebp+Threshold] ; Threshold
INIT:005EB334                 mov     eax, offset _NonPagedPoolDescriptor ; 将NonPagedPoolDescriptor地址赋给eax
INIT:005EB339                 push    ebx             ; PoolIndex
INIT:005EB33A                 push    ebx             ; PoolType
INIT:005EB33B                 push    eax             ; int
INIT:005EB33C                 mov     _PoolVector, eax ; 将NonPagedPoolDescriptor赋给PoolVector[0]
INIT:005EB341                 call    _ExInitializePoolDescriptor@20 ; ExInitializePoolDescriptor(x,x,x,x,x)
INIT:005EB346
INIT:005EB346 loc_5EB346:                             ; CODE XREF: InitializePool(x,x)-16↑j
INIT:005EB346                                         ; InitializePool(x,x)-7↑j ...
INIT:005EB346                 pop     edi
INIT:005EB347                 pop     esi
INIT:005EB348                 pop     ebx
INIT:005EB349                 pop     ebp
INIT:005EB34A                 retn    8
INIT:005EB34A _InitializePool@8 endp

如果要初始化分页内存池,首先依然会判断KeNumberNodes中保存的数据是否大于1

1
2
3
4
INIT:005EB21C loc_5EB21C:                             ; CODE XREF: InitializePool(x,x)+D↓j
INIT:005EB21C                 mov     cl, _KeNumberNodes
INIT:005EB222                 cmp     cl, 1
INIT:005EB225                 ja      loc_5FBF3E      ; KeNumberNodes是否大于1

如果大于1,则修改ExpNumberOfPagedPools中保存的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INIT:005FBF3E loc_5FBF3E:                             ; CODE XREF: InitializePool(x,x)-D4↑j
INIT:005FBF3E                 movzx   eax, cl
INIT:005FBF41                 cmp     eax, 7Fh
INIT:005FBF44                 mov     ds:_ExpNumberOfPagedPools, eax
INIT:005FBF49                 jbe     loc_5EB230
INIT:005FBF4F                 push    7Fh
INIT:005FBF51                 pop     eax
INIT:005FBF52                 mov     ds:_ExpNumberOfPagedPools, eax
INIT:005FBF57                 jmp     loc_5EB230
INIT:005FBF5C ; ---------------------------------------------------------------------------
INIT:005FBF5C
INIT:005FBF5C loc_5FBF5C:                             ; CODE XREF: InitializePool(x,x)-C6↑j
INIT:005FBF5C                 push    10h
INIT:005FBF5E                 pop     eax
INIT:005FBF5F                 mov     ds:_ExpNumberOfPagedPools, eax
INIT:005FBF64                 jmp     loc_5EB239

调用函数来申请一块内存,由于此时ebx等于0,所以申请的是非分页内存,而申请的内存的大小则是ExpNumberOfPagedPools的大小加1乘以0x1048的大小

1
2
3
4
5
6
7
8
9
INIT:005EB242                 lea     esi, [eax+1]
INIT:005EB245                 imul    esi, 1048h
INIT:005EB24B                 push    'looP'          ; Tag
INIT:005EB250                 push    esi             ; NumberOfBytes
INIT:005EB251                 push    ebx             ; PoolType
INIT:005EB252                 call    _ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
INIT:005EB257                 mov     edi, eax        ; 要获取的内存赋值赋给esi
INIT:005EB259                 cmp     edi, ebx
INIT:005EB25B                 jz      loc_5FBFF2

这里申请的内存大小之所以要乘以0x1048,是因为要保存的结构体除了内存池对象的0x1028字节大小,还要申请FAST_MUTEX对象的0x20大小,该对象结构体如下:

1
2
3
4
5
6
7
kd> dt _FAST_MUTEX
nt!_FAST_MUTEX
   +0x000 Count            : Int4B
   +0x004 Owner            : Ptr32 _KTHREAD
   +0x008 Contention       : Uint4B
   +0x00c Event            : _KEVENT
   +0x01c OldIrql          : Uint4B

将内存池对象地址赋给PoolVector数组下标为1的地址,将FAST_MUTEX对象地址赋给全局变量ExpPagedPoolMutex,并将参数PoolType修改为0,因为此时ebx等于0

1
2
3
4
5
6
7
8
9
INIT:005EB261                 mov     eax, _ExpNumberOfPagedPools
INIT:005EB266                 lea     esi, [eax+1]
INIT:005EB269                 imul    esi, 1028h      ; 计算内存池对象占用的内存大小
INIT:005EB26F                 add     esi, edi        ; 将esi指向FAST_MUTEXT对象地址
INIT:005EB271                 inc     eax
INIT:005EB272                 mov     _PoolVector+4, edi
INIT:005EB278                 mov     _ExpPagedPoolMutex, esi
INIT:005EB27E                 mov     [ebp+PoolType], ebx ; 将PoolType赋值为1
INIT:005EB281                 jz      short loc_5EB2CD

将填充申请到的ExpNumberOfPagedPools+1个的内存池对象和FAST_MUTEX对象

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
INIT:005EB283 loc_5EB283:                             ; CODE XREF: InitializePool(x,x)-2E↓j
INIT:005EB283                 xor     ecx, ecx
INIT:005EB285                 inc     ecx             ; ecx=1
INIT:005EB286                 push    esi             ; PoolLock
INIT:005EB287                 push    [ebp+Threshold] ; Threshold
INIT:005EB28A                 mov     [esi+_FAST_MUTEX.Count], ecx
INIT:005EB28C                 mov     [esi+_FAST_MUTEX.Owner], ebx
INIT:005EB28F                 mov     [esi+_FAST_MUTEX.Contention], ebx
INIT:005EB292                 mov     [esi+_FAST_MUTEX.Event.Header.Type], cl
INIT:005EB295                 mov     [esi+_FAST_MUTEX.Event.Header.Size], 4
INIT:005EB299                 mov     [esi+_FAST_MUTEX.Event.Header.SignalState], ebx
INIT:005EB29C                 lea     eax, [esi+_FAST_MUTEX.Event.Header.WaitListHead]
INIT:005EB29F                 mov     [esi+_FAST_MUTEX.Event.Header.WaitListHead.Blink], eax
INIT:005EB2A2                 mov     [eax], eax
INIT:005EB2A4                 mov     eax, [ebp+PoolType] ; PoolType赋给eax
INIT:005EB2A7                 push    eax             ; PoolIndex
INIT:005EB2A8                 push    ecx             ; PoolType
INIT:005EB2A9                 push    edi             ; int
INIT:005EB2AA                 mov     _ExpPagedPoolDescriptor[eax*4], edi
INIT:005EB2B1                 call    _ExInitializePoolDescriptor@20 ; ExInitializePoolDescriptor(x,x,x,x,x)
INIT:005EB2B6                 mov     eax, _ExpNumberOfPagedPools ; ExpNumberOfPagedPools赋给eax
INIT:005EB2BB                 add     edi, 1028h      ; edi指向下一个内存池对象
INIT:005EB2C1                 add     esi, 20h        ; esi指向下一个FAST_MUTEX对象
INIT:005EB2C4                 inc     [ebp+PoolType]  ; PoolType加1
INIT:005EB2C7                 inc     eax
INIT:005EB2C8                 cmp     [ebp+PoolType], eax
INIT:005EB2CB                 jb      short loc_5EB283 ; PoolType是否小于ExpNumberOfPagedPools

因此,可以得出结论,分页内存池对象也是用非分页内存来保存的。而无论是初始化非分页内存对象还是初始化分页内存对象,都需要通过ExInitializePoolDescriptor函数来实现

 

ExInitializePoolDescriptor函数首先对对象中的前几个成员赋值

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
PAGE:004E281B ; int __stdcall ExInitializePoolDescriptor(int, POOL_TYPE PoolType, ULONG PoolIndex, ULONG Threshold, PVOID PoolLock)
PAGE:004E281B _ExInitializePoolDescriptor@20 proc near
PAGE:004E281B                                         ; CODE XREF: MiInitializeSessionPool()+45↓p
PAGE:004E281B                                         ; InitializePool(x,x)-48↓p ...
PAGE:004E281B
PAGE:004E281B PoolDescriptor  = dword ptr  8
PAGE:004E281B PoolType        = dword ptr  0Ch
PAGE:004E281B PoolIndex       = dword ptr  10h
PAGE:004E281B Threshold       = dword ptr  14h
PAGE:004E281B PoolLock        = dword ptr  18h
PAGE:004E281B
PAGE:004E281B ; FUNCTION CHUNK AT PAGE:004E36F6 SIZE 0000001B BYTES
PAGE:004E281B
PAGE:004E281B                 mov     edi, edi
PAGE:004E281D                 push    ebp
PAGE:004E281E                 mov     ebp, esp
PAGE:004E2820                 mov     eax, [ebp+PoolDescriptor]
PAGE:004E2823                 mov     ecx, [ebp+PoolIndex]
PAGE:004E2826                 mov     edx, [ebp+Threshold]
PAGE:004E2829                 mov     [eax+_POOL_DESCRIPTOR.Threshold], edx
PAGE:004E282C                 mov     edx, [ebp+PoolLock]
PAGE:004E282F                 mov     [eax+_POOL_DESCRIPTOR.PoolIndex], ecx
PAGE:004E2832                 xor     ecx, ecx        ; ecx清0
PAGE:004E2834                 push    esi
PAGE:004E2835                 mov     esi, [ebp+PoolType] ; 将PoolType赋给esi
PAGE:004E2838                 mov     [eax+_POOL_DESCRIPTOR.LockAddress], edx
PAGE:004E283B                 mov     [eax+_POOL_DESCRIPTOR.PoolType], esi
PAGE:004E283D                 mov     [eax+_POOL_DESCRIPTOR.RunningAllocs], ecx
PAGE:004E2840                 mov     [eax+_POOL_DESCRIPTOR.RunningDeAllocs], ecx
PAGE:004E2843                 mov     [eax+_POOL_DESCRIPTOR.TotalPages], ecx
PAGE:004E2846                 mov     [eax+_POOL_DESCRIPTOR.TotalBigPages], ecx
PAGE:004E2849                 mov     [eax+_POOL_DESCRIPTOR.PendingFrees], ecx
PAGE:004E284C                 mov     [eax+_POOL_DESCRIPTOR.PendingFreeDepth], ecx

此时eax是内存池对象地址,加上0x28就会获得ListHeads成员的地址,而该成员是一个双向链表的数组,共有512个元素,所占内存大小为0x1000,所以接下来就是将ListHeads中的每一个双向链表都指向自己

1
2
3
4
5
6
7
8
9
10
PAGE:004E284F                 add     eax, 28h        ; eax指向ListHeads
PAGE:004E2852                 lea     edx, [eax+1000h] ; edx指向ListHead成员中的最后一个元素
PAGE:004E2858
PAGE:004E2858 loc_4E2858:                             ; CODE XREF: ExInitializePoolDescriptor(x,x,x,x,x)+49↓j
PAGE:004E2858                 cmp     eax, edx
PAGE:004E285A                 jnb     short loc_4E2866 ; eax是否大于edx
PAGE:004E285C                 mov     [eax+LIST_ENTRY.Blink], eax
PAGE:004E285F                 mov     [eax+LIST_ENTRY.Flink], eax
PAGE:004E2861                 add     eax, 8          ; 获取数组下一元素地址
PAGE:004E2864                 jmp     short loc_4E2858

3.分配池内存

为了管理内存的开销,每次通过执行体内存池分配的内存块有8字节大的POOL_HEADER结构体,该结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
kd> dt _POOL_HEADER
nt!_POOL_HEADER
   +0x000 PreviousSize     : Pos 0, 9 Bits
   +0x000 PoolIndex        : Pos 9, 7 Bits
   +0x002 BlockSize        : Pos 0, 9 Bits
   +0x002 PoolType         : Pos 9, 7 Bits
   +0x000 Ulong1           : Uint4B
   +0x004 ProcessBilled    : Ptr32 _EPROCESS
   +0x004 PoolTag          : Uint4B
   +0x004 AllocatorBackTraceIndex : Uint2B
   +0x006 PoolTagHash      : Uint2B
名称 作用
PreviousSize 记录了当前内存块前面的内存块的大小,依据此域可用找到同一页面前面的内存块
PoolIndex 说明了当前内存块属于哪个执行体内存池
BlockSize 记录了当前内存块的大小,依据此域可用找到同一页面后面的内存块
PoolType 记录了当前内存块所在的内存池的类型,对于空闲内存块,该成员为0
PoolTag 记录了当前被分配内存块的一个标记

这里的PreviousSize和BlockSize都是指实际大小除以8之后得到的值,并非原始值

 

POOL_DESCRIPTOR对象中的ListHeads成员维护了一组快表,这些快表包含了8字节倍数大小的空闲内存块链表。虽然ListHeads有512个成员,但是由于最小的内存块是8个字节,且POOL_HEADER也是8个字节,所以,实际使用的只有510项,分别对应于8,16,... ,4072和4080大小的空内存块。如果申请的内存块大小在4080到4096之间,就会使用整个页面,因为剩下的空间不足以容纳最小内存块(8字节)加上管理开销(8字节)。下图显示了执行体内存池对象的管理结构:

 

avatar

 

通常用户会使用ExAllocateTag或者ExAllocateTagEx来申请内存,但是真正申请内存的其实是函数ExAllocateTagEx,因为前者是通过调用后者来实现的

1
2
3
4
5
6
7
8
PVOID
ExAllocatePool(
    IN POOL_TYPE PoolType,
    IN ULONG NumberOfBytes
    )
{
    return ExAllocatePoolWithTag(PoolType, NumberOfBytes, 'enoN');
}

在函数ExAllocatePoolEx中,首先会通过PoolType从PoolVector中取出相应的内存池对象,局部变量则用来记录申请的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CheckType = PoolType & 1;
if ( PoolType & 0x20 )
{
  var_PoolType = 0;
  if ( CheckType )
    PoolDesc = ExpSessionPoolDescriptor;
  else
    PoolDesc = PoolVector[0];
}
else
{
  PoolDesc = PoolVector[CheckType];
  var_PoolType = 1;
}

通过参数NumberOfBytes判断申请的内存大小是否是大于0xFF0的大页面,如果是大页面就会直接通过调用函数MiAllocatePoolPages来直接申请内存块,此时的内存地址按页对齐,也就是地址的低12位为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
if ( NumberOfBytes > 0xFF0 )
{
  v63 = 0;
  while ( 1 )
  {
    if ( *(_BYTE *)PoolDesc & 1 )
    {
      ExAcquireFastMutex(*(PFAST_MUTEX *)(PoolDesc + 28));
    }
    else if ( (_UNKNOWN *)PoolDesc == &NonPagedPoolDescriptor )
    {
      LockHandle.OldIrql = KeAcquireQueuedSpinLock(6);
    }
    else
    {
      KeAcquireInStackQueuedSpinLock(*(PKSPIN_LOCK *)(PoolDesc + 28), &LockHandle);
    }
    Entry = (_DWORD **)MiAllocatePoolPages(PoolType & 0x61, NumberOfBytes, v59);
    if ( Entry )
      break;
    if ( *(_BYTE *)PoolDesc & 1 )
    {
      ExReleaseFastMutex(*(PFAST_MUTEX *)(PoolDesc + 28));
    }
// 省略部分代码
  }
 
  return Entry;
}

如果不是大页面,判断NumberOfBytes为0,则假设为1字节,因为有些驱动在分配的时候会设置为0,根据要申请的内存NumberOfBytes计算在快表ListHeads数组中的索引,即ListNumber局部变量

1
2
3
4
if ( !NumberOfBytes )
  NumberOfBytes = 1;
ListNumber = (NumberOfBytes + 15) >> 3;
NeedSize = (NumberOfBytes + 15) >> 3;

对于申请分页内存情况,根据情况ListNumber是否是小于0x20的小页面内存来决定是要从处理器的后备链表中分配内存,还是通过全局变量ExpPagedPoolDescriptor保存的分页池对象分配内存

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
    if ( CheckType == 1 )
    {
      if ( var_PoolType == 1 )
      {
        if ( ListNumber <= 0x20 )
        {
          var_KPRCB = KeGetPcr()->Prcb;
          // 从处理器后备链表中获取符合条件的链表
          LookasideList = var_KPRCB->PPNPagedLookasideList[ListNumber + 31].P;
          ++*((_DWORD *)LookasideList + 3);
          NumberOfBytesb = (SIZE_T)var_KPRCB;
          // 从链表中获取内存对象
          v15 = ExInterlockedPopEntrySList(LookasideList);
          v16 = v15;
          // 省略部分代码,找到就返回
    }
 
        // 如果处理器个数大于1
        if ( (unsigned __int8)KeNumberNodes > 1u )
        {
          v50 = KeGetPcr()->Prcb->ParentNode->Color;
          var_ExpNumberOfPagedPools = ExpNumberOfPagedPools;
          PoolIndex = v50;
          if ( v50 < ExpNumberOfPagedPools )
          {
            PoolDesc = ExpPagedPoolDescriptor[v50 + 1];
            PoolIndex = v50 + 1;
           //省略部分代码,找到就返回
          }
         }
        else
        {
           // 处理器个数等于1
          var_ExpNumberOfPagedPools = ExpNumberOfPagedPools;
        }
 
        PoolIndex = 1;
        if ( var_ExpNumberOfPagedPools != 1 )
        {
          PoolIndex = ++ExpPoolIndex;
          if ( ExpPoolIndex > var_ExpNumberOfPagedPools )
          {
            PoolIndex = 1;
            ExpPoolIndex = 1;
          }
          JUMPOUT(**(_DWORD **)(ExpPagedPoolDescriptor[PoolIndex] + 28), 1, &loc_47AC36);
        }
        // 获取相应的内存池对象
        PoolDesc = ExpPagedPoolDescriptor[PoolIndex];
LABEL_17:
        ListNumber = NeedSize;
        goto LABEL_18;
    }

对于申请非分页内存情况,根据变量var_PoolType与NeedSize来决定是否要从处理器的后备链表中来分配内存,还是从全局变量ExpNonPagedPoolDescriptor中获取非分页内存对象来分配内存

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
else
{
  if ( var_PoolType == 1 && NeedSize <= 0x20 )
  {
    KPRCB = KeGetPcr()->Prcb;
    var_LookasideList = KPRCB->PPLookasideList[NeedSize + 15].P;
    ++*((_DWORD *)var_LookasideList + 3);
    NumberOfBytesa = (SIZE_T)KPRCB;
    v8 = ExInterlockedPopEntrySList(var_LookasideList);
    v9 = v8;
    // 省略代码,找到则返回结果
  }
  if ( (unsigned int)ExpNumberOfNonPagedPools > 1 )
  {
    tmpPoolIndex = KeGetPcr()->Prcb->ParentNode->Color;
    PoolIndex = tmpPoolIndex;
    if ( tmpPoolIndex >= ExpNumberOfNonPagedPools )
    {
      PoolIndex = ExpNumberOfNonPagedPools - 1;
      tmpPoolIndex = ExpNumberOfNonPagedPools - 1;
    }
    // 获取非分页内存对象
    PoolDesc = ExpNonPagedPoolDescriptor[tmpPoolIndex];
    goto LABEL_17;
  }
}

如果是要在非分页内存对象中申请内存,最后会跳转到上面使用分页内存对象来申请内存时候的LABEL_17执行,而LABEL17则跳转到LABEL_18执行剩下的代码

 

剩下的代码就是先从链表中寻找内存块,如果没有找到可用的内存块,那么就会通过调用函数MiAllocatePoolPages来申请新的内存块,而多申请的内存块也会被加入到链表中,而内存块链接进链表的位置是POOL_HEADER以后的八字节,也就是说池对象头后就是用来链接进链表的双向链表,以下是WRK中关于这部分内容的部分代码:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
ListHead = &PoolDesc->ListHeads[ListNumber];
 
 do {
     // 检查链表中是否有可用的内存块, FALSE为有
     if (PrivateIsListEmpty (ListHead) == FALSE) {
 
         // 获取内存块
         Block = PrivateRemoveHeadList (ListHead);
 
         // 指向POOL_HEADER,此处的POOL_OVERHEAD为0x8
         Entry = (PPOOL_HEADER)((PCHAR)Block - POOL_OVERHEAD);
 
         // 内存块大小是和需要的内存大小不一致
         if (Entry->BlockSize != NeededSize) {
 
             if (Entry->PreviousSize == 0) {
                 // 页面开始处,取前面一块作为返回结果的内存块,保存在Entry中
                 // 剩余部分保存在SplitEntry中
 
                 SplitEntry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry + NeededSize);
 
                 SplitEntry->BlockSize = (USHORT)(Entry->BlockSize - NeededSize);
                 SplitEntry->PreviousSize = (USHORT) NeededSize;
 
                 // 获取下一内存块
                 NextEntry = (PPOOL_HEADER)((PPOOL_BLOCK)SplitEntry + SplitEntry->BlockSize);
                 if (PAGE_END(NextEntry) == FALSE) {
                     NextEntry->PreviousSize = SplitEntry->BlockSize;
                 }
             }
             else {
                 // 非页面开始处,取后面一块作为返回结果的内存块,保存在Entry中
                 // 剩余部分保存在SplitEntry中
 
                 SplitEntry = Entry;
                 Entry->BlockSize = (USHORT)(Entry->BlockSize - NeededSize);
                 Entry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry + Entry->BlockSize);
                 Entry->PreviousSize = SplitEntry->BlockSize;
 
                 NextEntry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry + NeededSize);
                 if (PAGE_END(NextEntry) == FALSE) {
                     NextEntry->PreviousSize = (USHORT) NeededSize;
                 }
             }
 
             // 为内存块赋值
             Entry->BlockSize = (USHORT) NeededSize;
             SplitEntry->PoolType = 0;
             Index = SplitEntry->BlockSize;
 
             if ((POOL_OVERHEAD != POOL_SMALLEST_BLOCK) ||
                 (SplitEntry->BlockSize != 1)) {
                 // 将剩余内存块加入到链表中
                 PrivateInsertTailList(&PoolDesc->ListHeads[Index - 1], ((PLIST_ENTRY)((PCHAR)SplitEntry + POOL_OVERHEAD)));
             }
         }
 
         // 为申请的内存块的POOL_HEADER赋值
         Entry->PoolType = (UCHAR)(((PoolType & (BASE_POOL_TYPE_MASK | POOL_QUOTA_MASK | SESSION_POOL_MASK | POOL_VERIFIER_MASK)) + 1) | POOL_IN_USE_MASK);
 
         InterlockedIncrement ((PLONG)&PoolDesc->RunningAllocs);
 
         InterlockedExchangeAddSizeT (&PoolDesc->TotalBytes,
                                      Entry->BlockSize << POOL_BLOCK_SHIFT);
 
         Entry->PoolTag = Tag;
 
 
         ((PULONGLONG)((PCHAR)Entry + CacheOverhead))[0] = 0;
 
         // 将申请到的内存地址返回,此时的CacheOverhead为0x8,所以会略过POOL_HEADER
         return (PCHAR)Entry + CacheOverhead;
     }
 
     // 指向下一个链表
     ListHead += 1;
 // 是否是最后一个链表
 } while (ListHead != &PoolDesc->ListHeads[POOL_LIST_HEADS]);
 
 
 // 如果没有找到可用内存块,就通过MiAllocatePoolPages来申请一块内存
 Entry = (PPOOL_HEADER) MiAllocatePoolPages (RequestType, PAGE_SIZE);
 
 // 为申请的内存块赋值
 Entry->Ulong1 = 0;
 Entry->PoolIndex = (UCHAR) PoolIndex;
 Entry->BlockSize = (USHORT) NeededSize;
 
 Entry->PoolType = (UCHAR)(((PoolType & (BASE_POOL_TYPE_MASK | POOL_QUOTA_MASK | SESSION_POOL_MASK | POOL_VERIFIER_MASK)) + 1) | POOL_IN_USE_MASK);
 
 // 指向剩余内存块
 SplitEntry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry + NeededSize);
 
 SplitEntry->Ulong1 = 0;
 
 Index = (PAGE_SIZE / sizeof(POOL_BLOCK)) - NeededSize;
 
 SplitEntry->BlockSize = (USHORT) Index;
 SplitEntry->PreviousSize = (USHORT) NeededSize;
 SplitEntry->PoolIndex = (UCHAR) PoolIndex;
 
 
 InterlockedIncrement ((PLONG)&PoolDesc->TotalPages);
 
 NeededSize <<= POOL_BLOCK_SHIFT;
 
 InterlockedExchangeAddSizeT (&PoolDesc->TotalBytes, NeededSize);
 
 if ((POOL_OVERHEAD != POOL_SMALLEST_BLOCK) ||
     (SplitEntry->BlockSize != 1)) {
     // 将剩余的内存块加入到链表中
     PrivateInsertTailList(&PoolDesc->ListHeads[Index - 1], ((PLIST_ENTRY)((PCHAR)SplitEntry + POOL_OVERHEAD)));
 
 }
 
 InterlockedIncrement ((PLONG)&PoolDesc->RunningAllocs);
 
 // 指向申请的内存块的内存地址,此时的CacheOverhead为0x8,所以会略过POOL_HEADER
 Block = (PVOID) ((PCHAR)Entry + CacheOverhead);
 
 Entry->PoolTag = Tag;
 
 return Block;

因此,可得出ExAllocatePoolWithTag函数基本流程如下:

  1. 如果申请的是大页面,直接调用MiAllocatePoolPages分配池内存

  2. 如果请求小页面,尝试从当前处理器控制块后备链表分配

  3. 如果后备链表分配失败,在从PoolVector指向的池描述符512条链表中分配

  4. 如果池描述符链表分配也失败,在调用MiAllocatePoolPages分配

4.释放池内存

池内存的释放是通过ExFreePool来实现的,函数首先会判断要释放的池内存是否是大页面内存,由于大页面内存的地址是按页对齐的,低12位为0,所以此时如果与0xFFF与运算以后的结果为0,则说明是大页面池内存

1
2
POOLCODE:0047A202                 test    word ptr [ebp+P], 0FFFh ; 是否是大页面内存
POOLCODE:0047A208                 jz      loc_47A7E7

对于大页面池内存,会通过MiFreePoolPages来释放

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
POOLCODE:0047A7E7 loc_47A7E7:                             ; CODE XREF: ExFreePoolWithTag(x,x)+22↑j
POOLCODE:0047A7E7                                         ; ExFreePoolWithTag(x,x)+C53↓j
POOLCODE:0047A7E7                 push    [ebp+P]
POOLCODE:0047A7EA                 call    _MmDeterminePoolType@4 ; MmDeterminePoolType(x)
POOLCODE:0047A7EF                 mov     esi, _ExpSessionPoolDescriptor
POOLCODE:0047A7F5                 mov     ebx, eax
POOLCODE:0047A7F7                 cmp     ebx, 21h
POOLCODE:0047A7FA                 jz      short loc_47A803
POOLCODE:0047A7FC                 mov     esi, _PoolVector[ebx*4]
POOLCODE:0047A803
POOLCODE:0047A803 loc_47A803:                             ; CODE XREF: ExFreePoolWithTag(x,x)+614↑j
POOLCODE:0047A803                 cmp     _PoolTrackTable, 0
POOLCODE:0047A80A                 jnz     loc_47AEDC
POOLCODE:0047A810
POOLCODE:0047A810 loc_47A810:                             ; CODE XREF: ExFreePoolWithTag(x,x)+CF9↓j
POOLCODE:0047A810                                         ; ExFreePoolWithTag(x,x)+D57↓j
POOLCODE:0047A810                 test    byte ptr [esi], 1
POOLCODE:0047A813                 jz      loc_47A8ED
POOLCODE:0047A819                 mov     ecx, [esi+1Ch]  ; FastMutex
POOLCODE:0047A81C                 call    ds:__imp_@ExAcquireFastMutex@4 ; ExAcquireFastMutex(x)
POOLCODE:0047A822
POOLCODE:0047A822 loc_47A822:                             ; CODE XREF: ExFreePoolWithTag(x,x)+71F↓j
POOLCODE:0047A822                                         ; ExFreePoolWithTag(x,x)+D68↓j
POOLCODE:0047A822                 push    [ebp+P]         ; BugCheckParameter2
POOLCODE:0047A825                 inc     dword ptr [esi+0Ch]
POOLCODE:0047A828                 call    _MiFreePoolPages@4 ; MiFreePoolPages(x)
POOLCODE:0047A82D                 jmp     short loc_47A7BE

和申请池内存类似,如果释放的是小页面池内存,就会尝试将池内存交还给当前处理器控制块的后备链表

 

如果不是上面的两种情况,函数就会判断要释放的内存块的上一内存块和下一内存块是否在被使用中,如果没有被使用,就会将它们合并成为一个内存块。合并的内存块如果刚好一个页的大小,函数就会调用MiFreePoolPages释放内存,否则的话就会将其加入到链表中

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
84
85
86
87
88
// 获取要释放的池对象的POOL_HEADER
Entry = (PPOOL_HEADER)((PCHAR)P - POOL_OVERHEAD);
 
BlockSize = Entry->BlockSize;
 
EntryPoolType = Entry->PoolType;
 
PoolType = (Entry->PoolType & POOL_TYPE_MASK) - 1;
 
CheckType = PoolType & BASE_POOL_TYPE_MASK;
 
// 记录是否发生合并
Combined = FALSE;
 
// 获取池内存的下一内存块
NextEntry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry + BlockSize);
 
InterlockedIncrement ((PLONG)&PoolDesc->RunningDeAllocs);
 
InterlockedExchangeAddSizeT (&PoolDesc->TotalBytes, 0 - ((SIZE_T)BlockSize << POOL_BLOCK_SHIFT));
 
// 如果有下一内存块
if (PAGE_END(NextEntry) == FALSE) {
    // 如果下一内存块没有被使用
    if (NextEntry->PoolType == 0) {
        // 记录发生了合并
        Combined = TRUE;
 
        // 将下一内存块从链表中取出
        if ((POOL_OVERHEAD != POOL_SMALLEST_BLOCK) ||
            (NextEntry->BlockSize != 1)) {
            PrivateRemoveEntryList(((PLIST_ENTRY)((PCHAR)NextEntry + POOL_OVERHEAD)));
        }
 
        // 内存块的大小等于该内存块的大小加上下一内存块的大小
        Entry->BlockSize = Entry->BlockSize + NextEntry->BlockSize;
    }
}
 
 
// 判断是否有上一内存块
if (Entry->PreviousSize != 0) {
    // 获取上一内存块地址
    NextEntry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry - Entry->PreviousSize);
    // 判断上一内存块是否没被使用
    if (NextEntry->PoolType == 0) {
        // 记录发生了合并
        Combined = TRUE;
 
        // 将上一内存块从链表中取出
        if ((POOL_OVERHEAD != POOL_SMALLEST_BLOCK) ||
            (NextEntry->BlockSize != 1)) {
            PrivateRemoveEntryList(((PLIST_ENTRY)((PCHAR)NextEntry + POOL_OVERHEAD)));
        }
 
        // 上一内存块的大小等于上一内存块大小加上内存块大小
        NextEntry->BlockSize = NextEntry->BlockSize + Entry->BlockSize;
        // 将内存块地址改为上一内存块的地址
        Entry = NextEntry;
    }
}
 
// 判断是否是完整的一页
if (PAGE_ALIGNED(Entry) &&
    (PAGE_END((PPOOL_BLOCK)Entry + Entry->BlockSize) != FALSE)) {
 
    InterlockedExchangeAdd ((PLONG)&PoolDesc->TotalPages, (LONG)-1);
 
    // 释放内存
    MiFreePoolPages (Entry);
}
else {
    // 将内存块改为未使用状态
    Entry->PoolType = 0;
    BlockSize = Entry->BlockSize;
 
 
    if (Combined != FALSE) {
        NextEntry = (PPOOL_HEADER)((PPOOL_BLOCK)Entry + BlockSize);
        if (PAGE_END(NextEntry) == FALSE) {
            NextEntry->PreviousSize = (USHORT) BlockSize;
        }
    }
 
    // 将内存块加入到链表中
    PrivateInsertHeadList (&PoolDesc->ListHeads[BlockSize - 1],
                           ((PLIST_ENTRY)((PCHAR)Entry + POOL_OVERHEAD)));
}

三.Windows进程内存管理

1.概述

相比于高2GB每个进程共享的内核内存空间,低2GB的用户内存空间则是每个进程独立拥有的内存空间。也就是说,这部分空间只能由进程自己访问,其他进程无法访问。Windows系统在进程切换的时候,通过更换页目录表基址,也就是CR3寄存器中的内容来实现每个进程独享其高2GB的内存空间。高2GB的内存空间还根据地址分成了不同的分区,如下是32位系统的不同地址的分区情况

地址 分区
0x00000000~0x0000FFFF 空指针赋值区,通常为指针赋值为NULL的时候,指向的就是这个分区,该分区是不可读写的
0x00010000~0x7FFEFFFF 用户模式区, 这个地址范围的内容才是真正意义上供用户程序使用的地址范围
0x7FFF0000~0x7FFFFFFF 64KB大小的禁入区,该分区也是不可读写的
 

对于进程地址空间,用户程序必须经过“保留(reserve)”和"提交(commit)"两个阶段才可以使用一段地址范围。“保留一段地址范围”的用意是,将这段地址范围保留起来,但并不真正使用,由于这段地址范围不占用任何物理内存或其他外存空间,所以并不会形成实质的开销。这对于有些需要连续地址空间的程序有意义,它们可用在初始时保留一段大地址范围,以后需要的时候陆续使用。提交地址范围是指这段地址终究要消耗物理内存,由于Windows支持物理内存和页面文件之间的交换,因此可提交的内存数量是,可用物理内存总量去除系统使用的物理内存数量后,在加上页面文件的大小

 

在已提交的地址范围中,当页面被访问时,一定要先映射到物理内存页面上。这些页面要么是一个进程私有的,不共享的,要么被映射到一个内存区的视图上。后者可被多个进程共享

2.私有内存

在Windows提供的API中,VirtualAlloc或VirtualAllocEx被用来保留或提交地址范围,通过这两个API申请的内存就是私有内存,只有申请内存的进程可使用;之后可用通过VirtualFree或VirtualFreeEx函数来解除已提交的地址范围,或者完全释放此地址范围。解除提交是指回到保留状态。因此,对于进程地址空间中的任何一个页面的地址范围,它一定处于三种状态之一:空闲的,保留的,或已提交的

 

以VirtualAllloc函数为例,该函数的定义如下:

1
2
3
4
5
6
LPVOID WINAPI VirtualAlloc(
  __in_opt  LPVOID lpAddress,
  __in      SIZE_T dwSize,
  __in      DWORD flAllocationType,
  __in      DWORD flProtect
);

其中第三个参数flAllocationType指定了申请的进程内存地址是“保留“或是”提交“。当该参数是MEM_RESERVE的时候,申请的进程内存地址是”保留“的,当参数为MEM_COMMIT的时候,申请的进程内存地址就是”提交“的

3.共享内存

共享内存是通过创建内存区对象来实现的,内存区对象是Windows平台上多个进程之间共享内存的一种常用方法,它可被映射到系统的页面文件或其他文件中。实际上,内存区对象代表了一种物理存储资源,它可能在物理内存中,也可能在系统页面文件中,或其他文件中

 

内存区对象有两种,一种建立在页面文件的基础上,称为页面文件支撑的内存区,另一种被映射到其他文件中,称为文件支撑的内存区,也称为文件对象

 

Windows提供的CreateFileMapping这个API就是用来创建内存区对象的,该函数定义如下:

1
2
3
4
5
6
7
8
HANDLE WINAPI CreateFileMapping(
  __in      HANDLE hFile,
  __in_opt  LPSECURITY_ATTRIBUTES lpAttributes,
  __in      DWORD flProtect,
  __in      DWORD dwMaximumSizeHigh,
  __in      DWORD dwMaximumSizeLow,
  __in_opt  LPCTSTR lpName
);

当第一个参数传入INVALID_HANDLE_VALUE(宏定义,值为-1)的时候,就会创建一个页面文件支撑的内存区,最后一个参数则指定了内存区的名称。当其他进程想要使用该内存区的时候,就可使用如下定义的OpenFileMapping函数,通过第三个参数lpName来打开指定的内存区

1
2
3
4
HANDLE WINAPI OpenFileMapping(
  __in  DWORD dwDesiredAccess,
  __in  BOOL bInheritHandle,
  __in  LPCTSTR lpName);

获取指定内存区的句柄以后,就可通过MapViewOfFile函数来将物理地址映射到用户空间中

1
2
3
4
5
6
7
LPVOID WINAPI MapViewOfFile(
  __in  HANDLE hFileMappingObject,
  __in  DWORD dwDesiredAccess,
  __in  DWORD dwFileOffsetHigh,
  __in  DWORD dwFileOffsetLow,
  __in  SIZE_T dwNumberOfBytesToMap
);

4.进程地址空间的内存管理

Windows的进程地址空间是通过VAD(虚拟地址描述符)来管理的。VAD对象描述了一段连续的地址范围。在整个地址空间中,保留的或提交的地址范围有可能是不连续的,所以,Windows使用一颗平衡二叉搜索树(称为AVL树)来管理VAD对象。在进程内核对象EPROCESS偏移0x11C处保存的VadRoot域指向的就是此树的根。该树的每一个节点的对象结构体是MMVAD,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _MMVAD
nt!_MMVAD
   +0x000 StartingVpn      : Uint4B
   +0x004 EndingVpn        : Uint4B
   +0x008 Parent           : Ptr32 _MMVAD
   +0x00c LeftChild        : Ptr32 _MMVAD
   +0x010 RightChild       : Ptr32 _MMVAD
   +0x014 u                : __unnamed
   +0x018 ControlArea      : Ptr32 _CONTROL_AREA
   +0x01c FirstPrototypePte : Ptr32 _MMPTE
   +0x020 LastContiguousPte : Ptr32 _MMPTE
   +0x024 u2               : __unnamed
偏移 名称 作用
0x000 StartingVpn 该VAD对象描述的进程内存地址空间的起始地址
0x004 EndingVpn 该VAD对象描述的进程内存地址空间的结束地址
0x008 Parent 该VAD对象的父节点
0x00C LeftChild 该VAD对象的左子树
0x010 RightChild 该VAD对象的右子树
0x014 u 指定了该VAD对象描述的进程内存地址空间的属性,该属性用MMVAD_FLAGS结构体来描述
 

其中的MMVAD_FLAGS结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _MMVAD_FLAGS
nt!_MMVAD_FLAGS
   +0x000 CommitCharge     : Pos 0, 19 Bits
   +0x000 PhysicalMapping  : Pos 19, 1 Bit
   +0x000 ImageMap         : Pos 20, 1 Bit
   +0x000 UserPhysicalPages : Pos 21, 1 Bit
   +0x000 NoChange         : Pos 22, 1 Bit
   +0x000 WriteWatch       : Pos 23, 1 Bit
   +0x000 Protection       : Pos 24, 5 Bits
   +0x000 LargePages       : Pos 29, 1 Bit
   +0x000 MemCommit        : Pos 30, 1 Bit
   +0x000 PrivateMemory    : Pos 31, 1 Bit
名称 含义
ImageMap 指定了是否是一个镜像文件,如果为1则表示是,否则为其他文件
Protection 指定了内存的属性:1为READONLY;2为EXECUTE;3为EXECUTE _READ;4为READWITER;5为WRITECOPY;6为EXECUTE _READWITER;7为EXECUTE_WRITECOPY
PrivateMemory 如果为1,则表示是私有内存,如果为2则表示是共享内存
 

以记事本进程为例,通过查看EPROCESS结构的VadRoot就可得到该进程VAD对象的二叉树的根节点地址,此时为0x81D14F48

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
PROCESS 81dcc548  SessionId: 0  Cid: 06c0    Peb: 7ffda000  ParentCid: 0504
    DirBase: 029401e0  ObjectTable: e178e268  HandleCount:  45.
    Image: notepad.exe
 
kd> dt _EPROCESS 81dcc548
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x06c ProcessLock      : _EX_PUSH_LOCK
   +0x070 CreateTime       : _LARGE_INTEGER 0x1d7fa58`9d121cf8
   +0x078 ExitTime         : _LARGE_INTEGER 0x0
   +0x080 RundownProtect   : _EX_RUNDOWN_REF
   +0x084 UniqueProcessId  : 0x000006c0 Void
   +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x819d0a40 - 0x81e2fb28 ]
   +0x090 QuotaUsage       : [3] 0xa78
   +0x09c QuotaPeak        : [3] 0xc20
   +0x0a8 CommitCharge     : 0x192
   +0x0ac PeakVirtualSize  : 0x2451000
   +0x0b0 VirtualSize      : 0x1fbe000
   +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0x819d0a6c - 0x81e2fb54 ]
   +0x0bc DebugPort        : (null)
   +0x0c0 ExceptionPort    : 0xe157d380 Void
   +0x0c4 ObjectTable      : 0xe178e268 _HANDLE_TABLE
   +0x0c8 Token            : _EX_FAST_REF
   +0x0cc WorkingSetLock   : _FAST_MUTEX
   +0x0ec WorkingSetPage   : 0x15726
   +0x0f0 AddressCreationLock : _FAST_MUTEX
   +0x110 HyperSpaceLock   : 0
   +0x114 ForkInProgress   : (null)
   +0x118 HardwareTrigger  : 0
   +0x11c VadRoot          : 0x81d14f48 Void

根据VadRoot的地址就可找到相应的树的根,通过其左右子树就可遍历这颗二叉树

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _MMVAD 0x81d14f48
nt!_MMVAD
   +0x000 StartingVpn      : 0x290
   +0x004 EndingVpn        : 0x2d0
   +0x008 Parent           : (null)
   +0x00c LeftChild        : 0x81e7af58 _MMVAD
   +0x010 RightChild       : 0x81b2ed88 _MMVAD
   +0x014 u                : __unnamed
   +0x018 ControlArea      : 0x81dbb358 _CONTROL_AREA
   +0x01c FirstPrototypePte : 0xe16607b0 _MMPTE
   +0x020 LastContiguousPte : 0xe16609b0 _MMPTE
   +0x024 u2               : __unnamed

也可查看该VAD对象的内存属性

1
2
3
4
5
6
7
8
9
10
11
12
kd> dt _MMVAD_FLAGS 0x819d2ca8 + 0x14
nt!_MMVAD_FLAGS
   +0x000 CommitCharge     : 0y0000000000000000001 (0x1)
   +0x000 PhysicalMapping  : 0y0
   +0x000 ImageMap         : 0y0
   +0x000 UserPhysicalPages : 0y0
   +0x000 NoChange         : 0y0
   +0x000 WriteWatch       : 0y0
   +0x000 Protection       : 0y00100 (0x4)
   +0x000 LargePages       : 0y0
   +0x000 MemCommit        : 0y1
   +0x000 PrivateMemory    : 0y1

为了方便查看一个进程的内存使用情况,WinDbg提供了vad命令来解析这颗二叉树,如下就是对记事本进程的内存使用情况

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
kd> !vad 0x81d14f48
VAD     level      start      end    commit
81d13148 ( 3)         10       10         1 Private      READWRITE        
819d2ca8 ( 2)         20       20         1 Private      READWRITE        
81aa1220 ( 5)         30       3f         5 Private      READWRITE        
81dca1e0 ( 4)         40       7f        18 Private      READWRITE        
81a54570 ( 3)         80       82         0 Mapped       READONLY           Pagefile-backed section
81d22888 ( 4)         90       91         0 Mapped       READONLY           Pagefile-backed section
81e7af58 ( 1)         a0      19f        20 Private      READWRITE        
819598e0 ( 4)        1a0      1af         6 Private      READWRITE        
81eac848 ( 3)        1b0      1bf         0 Mapped       READWRITE          Pagefile-backed section
81a503b8 ( 4)        1c0      1d5         0 Mapped       READONLY           \WINDOWS\system32\unicode.nls
81a503e8 ( 2)        1e0      220         0 Mapped       READONLY           \WINDOWS\system32\locale.nls
81d14f78 ( 4)        230      270         0 Mapped       READONLY           \WINDOWS\system32\sortkey.nls
819d3a18 ( 3)        280      285         0 Mapped       READONLY           \WINDOWS\system32\sorttbls.nls
81d14f48 ( 0)        290      2d0         0 Mapped       READONLY           Pagefile-backed section
81d170f8 ( 5)        2e0      3a7         0 Mapped       EXECUTE_READ       Pagefile-backed section
81e6e220 ( 6)        3b0      3bf         8 Private      READWRITE        
81d146b0 ( 7)        3c0      3c0         1 Private      READWRITE        
81b2edc8 ( 8)        3d0      3d0         1 Private      READWRITE        
81ad5368 (10)        3e0      3e1         0 Mapped       READONLY           Pagefile-backed section
81ad5338 ( 9)        3f0      3f1         0 Mapped       READONLY           Pagefile-backed section
81ad5318 (10)        400      40f         3 Private      READWRITE        
81950eb8 ( 4)        410      41f         8 Private      READWRITE        
81dd0388 ( 3)        420      42f         4 Private      READWRITE        
81dd0358 ( 4)        430      432         0 Mapped       READONLY           \WINDOWS\system32\ctype.nls
81aa4190 ( 2)        440      47f         3 Private      READWRITE        
81e6e240 ( 5)        480      582         0 Mapped       READONLY           Pagefile-backed section
81960680 ( 4)        590      88f         0 Mapped       EXECUTE_READ       Pagefile-backed section
81aa8ae0 ( 3)        890      90f         1 Private      READWRITE        
81aba778 ( 5)        910      910         0 Mapped       READWRITE          Pagefile-backed section
81b1b2e8 ( 4)        920      95f         0 Mapped       READWRITE          Pagefile-backed section
81e7ba20 ( 6)        960      96d         0 Mapped       READWRITE          Pagefile-backed section
81d158b8 ( 5)        970      a6f       123 Private      READWRITE        
81a55f98 ( 7)        a70      a73         0 Mapped       READWRITE          Pagefile-backed section
81e6e1e0 ( 6)        a90      b0f         0 Mapped       READWRITE          Pagefile-backed section
81e2dab8 ( 7)        b10      b8f         0 Mapped       READWRITE          Pagefile-backed section
81b2ed88 ( 1)       1000     1012         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\notepad.exe
81952cc0 ( 7)      58fb0    59179        10 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\AppPatch\AcGenral.dll
81d84a88 ( 8)      5adc0    5adf6         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\uxtheme.dll
81d170c8 ( 6)      5cc30    5cc55        21 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\shimeng.dll
81960650 ( 7)      62c20    62c28         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\lpk.dll
81e72fd8 ( 5)      72f70    72f95         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\winspool.drv
81e7b9f0 ( 8)      73640    7366d         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\MSCTFIME.IME
81d226c8 ( 7)      73fa0    7400a        17 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\usp10.dll
8196a608 ( 8)      74680    746cb         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\MSCTF.dll
81d126e8 ( 6)      759d0    75a7e         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\userenv.dll
81aa4160 ( 7)      76300    7631c         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\imm32.dll
819d32b8 ( 4)      76320    76366         5 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\comdlg32.dll
81d14ee8 ( 8)      76990    76acd         8 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\ole32.dll
81e6d480 ( 7)      76b10    76b39         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\winmm.dll
819d46c0 ( 8)      770f0    7717a         4 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\oleaut32.dll
81d14eb8 ( 6)      77180    77282         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\WinSxS\x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.2600.6028_x-ww_61e65202\comctl32.dll
81e2d8d8 ( 8)      77bb0    77bc4         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\msacm32.dll
81dc9e08 ( 9)      77bd0    77bd7         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\version.dll
81d14f18 ( 7)      77be0    77c37         8 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\msvcrt.dll
81d22688 ( 8)      77d10    77d9f         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\user32.dll
81dcc1b8 ( 5)      77da0    77e48         6 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\advapi32.dll
81dcbca0 ( 6)      77e50    77ee2         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\rpcrt4.dll
81e2c630 ( 8)      77ef0    77f39         3 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\gdi32.dll
81e72f78 ( 9)      77f40    77fb5         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\shlwapi.dll
81aa9120 ( 7)      77fc0    77fd0         2 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\secur32.dll
81dccc68 ( 3)      7c800    7c91d         6 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\kernel32.dll
81aac6f0 ( 2)      7c920    7c9b2         5 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\ntdll.dll
81e72fa8 ( 5)      7d590    7dd83        31 Mapped  Exe  EXECUTE_WRITECOPY  \WINDOWS\system32\shell32.dll
81d212d8 ( 4)      7f6f0    7f7ef         0 Mapped       EXECUTE_READ       Pagefile-backed section
81a559a0 ( 3)      7ffa0    7ffd2         0 Mapped       READONLY           Pagefile-backed section
81aa2538 ( 4)      7ffda    7ffda         1 Private      READWRITE        
81aa79d0 ( 5)      7ffdf    7ffdf         1 Private      READWRITE        
 
Total VADs:    67  average level:    6  maximum depth: 10

四.Windows物理内存管理

在系统地址空间,有一个区域称为PFN数据库,用于管理系统的物理内存。PFN数据库是一个阵列,或者说是一个数组,每一项都描述了物理页面的状态。全局变量MnPfnDatabase是一个指向该数组的指针,以下是该全局变量的输出,可看到该数组的地址是0x80C00000

1
2
3
4
5
6
7
8
9
kd> dd MmPfnDatabase
805630c8  80c00000 ffffffff 00000006 0000003f
805630d8  00000000 00018597 00000005 00000000
805630e8  00000000 00000000 00000004 00000000
805630f8  00000000 00000000 00000000 00000000
80563108  00000000 00000000 00000000 0000001e
80563118  000000fa 0001de80 0001ffff 0001ffff
80563128  0001ff6a 00000040 00000000 7fff0000
80563138  80000000 7ffeffff 00000000 00000000

数组中的每一个元素都是一个MMPFN结构体,该结构体定义如下:

1
2
3
4
5
6
7
8
kd> dt _MMPFN
nt!_MMPFN
   +0x000 u1               : __unnamed
   +0x004 PteAddress       : Ptr32 _MMPTE
   +0x008 u2               : __unnamed
   +0x00c u3               : __unnamed
   +0x010 OriginalPte      : _MMPTE
   +0x018 u4               : __unnamed

在WRK中的定义如下:

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
typedef struct _MMPFN {
    union {
        PFN_NUMBER Flink;
        WSLE_NUMBER WsIndex;
        PKEVENT Event;
        NTSTATUS ReadStatus;
        SINGLE_LIST_ENTRY NextStackPfn;
    } u1;
    PMMPTE PteAddress;
    union {
        PFN_NUMBER Blink;
        ULONG_PTR ShareCount;
    } u2;
    union {
        struct {
            USHORT ReferenceCount;
            MMPFNENTRY e1;
        };
        struct {
            USHORT ReferenceCount;
            USHORT ShortFlags;
        } e2;
    } u3;
    union {
        MMPTE OriginalPte;
        LONG AweReferenceCount;
    };
    union {
        ULONG_PTR EntireFrame;
        struct {
            ULONG_PTR PteFrame: 25;
            ULONG_PTR InPageError : 1;
            ULONG_PTR VerifierAllocation : 1;
            ULONG_PTR AweAllocation : 1;
            ULONG_PTR Priority : MI_PFN_PRIORITY_BITS;
            ULONG_PTR MustBeCached : 1;
        };
    } u4;
} MMPFN, *PMMPFN;
名称 含义
WsIndex 包含了该页面在所属工作集链表中的索引。如果这个页面只被一个进程映射,也就是说它是一个私有页面,则WsIndex等于该页面在进程工作集链表中的索引。如果该页面被多个进程共享,则该域只保证第一个使该页面变成有效的进程是正确的,通过此域,可用快速地在工作集中定位到它地工作集链表项
PteAddress 指向此页面的PTE的虚拟地址
ShareCount 指向该页面的PET数量,而对于页表页面,该域表示页表中有效PTE和转移PTE的数量。只要ShareCount大于0,它就不能从内存中移除
ReferenceCount 代表这个页面必须保留在内存中的引用计数,包括该页面被加入工作集或者I/O的需要而被锁定的次数
e1 MMPFNENTRY结构,包含了各种标志位
OriginalPte 包含了指向此页面的原始内容,可能是一个PTE。它的用意是,当将一个物理页面分配给一个PTE时,它记录了原来的PTE;以后当该物理页面不再为它使用时,可用恢复其原来的PTE
u4 指向该页面的PTE所在的页表页面的物理页帧编号,以及一些标志位
 

其中MMPFNENTRY结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt _MMPFNENTRY
nt!_MMPFNENTRY
   +0x000 Modified         : Pos 0, 1 Bit
   +0x000 ReadInProgress   : Pos 1, 1 Bit
   +0x000 WriteInProgress  : Pos 2, 1 Bit
   +0x000 PrototypePte     : Pos 3, 1 Bit
   +0x000 PageColor        : Pos 4, 3 Bits
   +0x000 ParityError      : Pos 7, 1 Bit
   +0x000 PageLocation     : Pos 8, 3 Bits
   +0x000 RemovalRequested : Pos 11, 1 Bit
   +0x000 CacheAttribute   : Pos 12, 2 Bits
   +0x000 Rom              : Pos 14, 1 Bit
   +0x000 LockCharged      : Pos 15, 1 Bit
   +0x000 DontUse          : Pos 16, 16 Bits

数组中的每个元素都占0x1C字节的大小,因此要查看数组中的元素需要根据索引来乘以0x1C作为偏移,而索引值左移12位的地址就是数组中该元素所描述的物理页,比如以下查询的是索引为2的物理页面的状态,因此所查的物理页是0x200~0x2FFF页面的状态

1
2
3
4
5
6
7
8
kd> dt _MMPFN 80c00000 + 0x1C * 2
nt!_MMPFN
   +0x000 u1               : __unnamed
   +0x004 PteAddress       : 0xc0400011 _MMPTE
   +0x008 u2               : __unnamed
   +0x00c u3               : __unnamed
   +0x010 OriginalPte      : _MMPTE
   +0x018 u4               : __unnamed

五.Windows缺页异常

1.概述

当Intel x86处理器在执行一个执行流过程中,需要使用到地址页表项(PTE)来翻译一个虚拟地址引用,PTE结构如下图所示,其中的第0位(V位)代表了有效位,指定了该PTE是否是有效的

 

avatar

 

当有效位为0的时候,处理器会引发一个异常,也成为页面错误。Windows内核的陷阱处理器会把这一异常交给内存管理器的页面错误例程,由它来解决页面无效的问题。一般来说,只要这个页面错误是合理的,则页面错误处理例程将试图分配一个物理页面,并设置好PTE来消除虚拟地址引用错误,使得该地址引用能被正确地翻译成恰当地物理页面。由于异常是硬件触发地,而页面错误处理例程是操作系统提供的组件,所以这一页面分配并重映射的过程对于原始的指令流是完全透明的,最多只是时间上稍有停顿

 

如果一个PTE已经正确指向一个物理页面,则页面错误不会发生,虚拟地址翻译可快速进行。但是,随着进程使用越来越多的页面,最终系统的可用物理内存可能会消耗光。为了避免因内存消耗光而导致应用程序或系统的正常功能无法执行,现代操作系统提供了页面交换的能力,即把有些正在使用的页面中的内容存放到磁盘上,从而腾出这些物理页面以供使用。以后,当这些被换出到磁盘上的页面再次被引用时,页面错误处理例程在把它们换回内存中

 

Windows系统支持一个或多个页面文件(每个磁盘分区最多一个页面文件),用来存放被交换页面的内容。页面文件可被看做是物理内存的一种延伸。对于页面文件中的内容而言,页面文件域物理内存是同等的,只不过受页面调度的影响而在不同时刻位于不同的物理位置

2.内存页面交换

当第0位(V位)为0的时候,此时的PTE就是一个无效PTE。此时,处理器引发页面错误异常,该异常的陷阱处理器把控制权交给页面处理例程,所以,PET其他位的含义由操作系统来解释。正是由于这一层灵活性,Windows才可以在这一的PTE中承载一些信息,以便实现页面管理功能。无效PTE有下图所示的4种情形:

 

avatar

  • 情形一:如图中(a)所示,此时第10和11位为0,第1~4位指示了在哪个页面文件种,第12~31位共20位表示了该页面在页面文件中的偏移量,以页面单位(0x1000字节)为单位

  • 情形二:如图中(b)所示,是第一种情形的特例,若1~4位和12~31位全为0,则表明这是一个待分配的页面,它需要一个填满零的页面

  • 情形三:如图中(c)所示,第10位为1,说明这个页面尚在内存中,但正在换出过程中,位于系统的某个物理页面链表中。此时第12~31位指示了物理页帧编号,根据PFN数据库可用定位到该页面的实际状态

  • 情形四:如图中(d)所示,是一个全零的PTE,此时该页面的状态无法确定,甚至该PTE对应的虚拟地址是否已被提交也不能确定,所以页面错误处理例程应检查VAD树中对应的节点,以确定页面的状态

3.Windows的写时复制

当通过CreateFileMapping来创建内存区对象的时候,该函数的第三个参数flProtect指定了内存区对象的页面保护属性,可以是PAGE_READ, PAGE_READWRITE, PAGE_EXECUTE以及会产生写时复制的PAGE_WRITECOPY,该属性的含义是,如果一个进程要写这个页面,则生成器字节的私有拷贝,并且私有拷贝的保护属性为PAGE_READWRITE。而属性PAGE_READWRITE的含义是不管将来哪个进程读或者写这个页面,都始终只有一个页面,也就是说,完全共享这个页面

 

当一个进程使用内存区对象时,它必须映射一个视图,视图本身并不影响内存区对象的保护属性。然而,在视图所对应的VAD节点中包含了这一保护属性,代表该进程对于此内存区对象的操作类型。MMVAD结构中的u.VadFlags.Protection成员即为此目的。映像文件的视图,此值为MM_EXECUTE_WROTECPY,支持写时复制。而其他类型的视图,此值就会由函数MapVieOfFile来决定

 

通常将一个DLL文件装载进内存时所创建的内存区对象的属性就是写时复制,且相应的PTE的有效位也会是0,如下图是记事本进程的DLL内存区对象的属性

 

avatar

 

当程序对有写时复制属性的内存区进行写操作的时候,发现PTE是0,也就是无效PTE的时候,页面错误处理例程会检查其内存区对象属性。如果是写时复制属性,就会会这块内存在分配一块新的物理地址,这样对其写入的操作就会在这块物理地址中完成。这样做的好处就是,原来物理地址中的内容不会发生改变,就不会影响其他进程对该共享内存的使用

六.参考资料

  • 《Windows内核原理与实现》

  • 《Windows内核设计思想》


【看雪培训】目录重大更新!《安卓高级研修班》2022年春季班开始招生!

最后于 2022-1-5 17:48 被1900编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回