欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[翻译]《深入解析windows操作系统第6版下册》第10章:内存管理(第三部分)

[翻译]《深入解析windows操作系统第6版下册》第10章:内存管理(第三部分)


由于论坛限制了每帖能够引用的外链图片数为40张。因此第三部分译文将接着第二部分的“64 位地址空间布局”一节中未完成的部分继续 ,直到“IA 64 虚拟地址翻译”一节结束。
The detailed IA64 and x64 address space layouts vary slightly. The IA64 address space layout is shown in Figure 10-12, and the x64 address space layout is shown in Figure 10-13.
IA64 与 x64 地址空间布局的细节略有差异。        图 10-12 给出了 IA64 的地址空间布局;10-13 给出了 x64 的地址空间布局。(译注:为了能够容纳下译文,将原图划分成了几张子图,并以罗马数字标记,请注意左侧地址的改变及其关联性)








下面是 x64  地址空间布局



(译注:关于 64 位 Windows 的虚拟地址空间布局,上图的介绍可能过于简单,特别是对于某些特定的系统虚拟地址空间区域,有鉴于此,特别翻译了 CodeMachine 站点上一篇对此作了详尽介绍的文章。原文链接为 http://www.codemachine.com/article_x64kvas.html,并把它放在这里与上图对比参照,可以更好的理解。为了区别,以褐色表示来自 CodeMachine 站点的译文。首先援引其中的地址空间布局概要表,如下所示。它与上图 10-13 的 x64 地址空间布局图的每个区域一一对应:

未使用的系统空间(FFFF0800`00000000~FFFFF67F`FFFFFFFF)
这个区域的起始地址保存在内核全局变量 nt!MmSystemRangeStart 中。此区域在  Windows 7 X64 上是未使用的。


PTE 空间(FFFFF680`00000000~FFFFF6FF`FFFFFFFF)
此区域包含 X64 处理器使用的四级页表页面,用于用户模式与内核模式虚拟地址空间的映射。各种类型的 X64 页表页面被映射到下面列出的特定地址范围内:
PTE Pages   FFFFF680`00000000                       
PDE Pages   FFFFF6FB`40000000               
PPE Pages   FFFFF6FB`7DA00000                       
PXE Pages   FFFFF6FB`7DBED000

超空间(FFFFF700`00000000~FFFFF77F`FFFFFFFF)
驻留在物理内存中的进程相关页面称为“工作集”,那么显然这些物理页面需要映射到系统虚拟地址空间中的一个特殊区域进行统一管理,此区域称为“超空间”
进程工作集列表和每进程相关的内存管理数据结构(它们不需要在任意进程的上下文中被访问)都映射到这个 512GB 的区域中。每个进程的 EPROCESS.Vm.VmWorkingSetList 成员存储地址
0xFFFFF700`01080000 的信息(一个 _MMWSL 型指针)。对于每个在进程工作集中的页面(这些页面驻留在物理内存中),在此区域(即 EPROCESS.Vm.VmWorkingSetList 指向的地址)都存在一个对应的 _MMWSL(内存管理器工作集列表)数据结构;还有一个对应的 _MMWSLE(内存管理器工作集列表条目)数据结构也在此区域中,后者的具体位置由 EPROCESS.Vm.VmWorkingSetList.wsle 指向的地址确定。下面以 32 位系统为例:

[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]dt nt!_EPROCESS  -r 872e1d28 (某个进程的 EPROCESS 结构的虚拟地址)

(。。。省略无关输出。。。)

 +0x054 VmWorkingSetList : 0xc0802000 _MMWSL

    +0x000 FirstFree        : 0x5693

    +0x004 FirstDynamic     : 6

    +0x008 LastEntry        : 0x5970

    +0x00c NextSlot         : 6

    +0x010 Wsle             : 0xc0802d08 _MMWSLE

dt nt!_EPROCESS  -r

(。。。省略无关输出。。。)

  +0x054 VmWorkingSetList : Ptr32 _MMWSL

     +0x000 FirstFree        : Uint4B

     +0x004 FirstDynamic     : Uint4B

     +0x008 LastEntry        : Uint4B

     +0x00c NextSlot         : Uint4B

     +0x010 Wsle             : Ptr32 _MMWSLE

[/COLOR][/SIZE][/FONT]


在上面这个例子中,地址 0xc0802000 在 32 位系统上的超空间区域内,_MMWSL 结构位于此处;而 _MMWSLE 结构则位于其后不远的 0xc0802d08 地址处。
超空间也用于临时将物理页面(PFN)映射到系统空间。而超空间内的虚拟地址实际上是从系统 PTE 区域中分配的。其中一个例子就是,除了进程页表中的无效页表条目外,引用的有效页面(例如,当一个页面从备用[standby]列表中被移除时)。
MiMapPageInHyperSpaceWorker() 把一个 PFN 映射到超空间,并返回分配给此映射的虚拟地址。MiMapPageInHyperSpaceWorker() 本应该把驻留在物理内存中的进程页面映射到超空间,但实际上它把这些页面映射到系统 PTE 区域。
MiZeroPhysicalPage(),MiWaitForInPageComplete(),MiCopyHeaderIfResident(),MiRestoreTransitionPte() 等函数,都调用 MiMapPageInHyperSpaceWorker() ,临时获取被映射到超空间内物理页面的虚拟地址。

共享的系统页面(FFFFF780`00000000~FFFFF780`00000FFF)
这个 4KB 的页面在 UVAS(用户虚拟地址空间)与 KVAS(内核虚拟地址空间)之间共享。它提供了一个在用户和内核模式间快速传递信息的方法。与此相关的共享数据结构为
nt!_KUSER_SHARED_DATA

系统缓存工作集(FFFFF780`00001000~FFFFF7FF`FFFFFFFF)
此区域用于映射系统缓存工作集和系统缓存工作集列表条目。
内核变量 nt!MmSystemCacheWs 指向用于系统缓存的工作集数据结构(例如 nt!_MMSUPPORT)。要显示用于系统缓存的工作集列表条目,
使用命令“!wsle 1 @@(((nt!_MMSUPPORT *) @@(nt!MmSystemCacheWs))->VmWorkingSetList)”。工作集修剪器使用这些条目来从系统缓存虚拟地址修剪对应的物理页面。(关于工作集修剪器,请参考第一部分译文的“平衡集管理器”)

由最初的加载器映射(FFFFF800`00000000~FFFFF87F`FFFFFFFF)
在系统引导阶段,winload.exe 将 NTOSKRNL.EXE,HAL.DLL,以及内核调试器 DLL(KDCOM, KD1394, KDUSB)加载到此区域。该区域还包含了空闲线程的栈,DPC(延迟过程调用)的栈,以及 KPCR(内核处理器控制区,Kernel Processor Control Region)和 Idle 线程的数据结构。

分页池区(FFFFF8a0`00000000~FFFFF8bF`FFFFFFFF)
内核变量 nt!MmPagedPoolEnd 存储可分页池的当前结束地址(由此可见它是动态增长或缩减的)。同样的,其当前大小存储在内核变量 nt!MmSizeOfPagedPoolInBytes 中。当调用
MiObtainSystemVa() 时传入 MiVaPagedPool 类型的系统虚拟地址范围时,它就会在此区域中分配。内核位图(Bitmap)——nt!MiPagedPoolVaBitMap——控制从可分页池中分配虚拟地址空间的操作,并且,内核变量 nt!MiPagedPoolVaBitMapHint 存储其分配提示(allocation hint)

PFN 数据库(FFFFFa80`00000000~*nt!MmNonPagedPoolStart-1)
系统中每一个物理页面在 PFN 数据库中都有一个条目来描述其物理页框号(系统上的物理页面总数为,内核变量 nt!MmHighestPossiblePhysicalPage 的值加1),这是为了让 PFN 条目能适应热插拔内存。在内核调试器中,以下表达式:
? poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase)
可以用来确定 PFN 数据库的大小。另外,要确定 PFN 数据库中条目的总数,可以使用以下表达式:
?(poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase))/ @@(sizeof(nt!_MMPFN))
内核变量  nt!MmPfnDatabase 负责定义此区域(用于 PFN 数据库)的起始地址。
(译注:使用 32 位 Windows 7 客户机版本,在任务管理器中显示的可用物理内存总数为 3.5GB。每个物理页面跨越 4KB 的物理地址范围,这里按 4096 字节计算。
命令 dd nt!MmHighestPossiblePhysicalPage 的输出结果为 000defff,加1为 000dffff,即10进制的 913408,913408 * 4096 B = 3741319168 B ,与 3.5GB 大致相符,验证了上面讨论的部分内容
另外,从命令 ?(poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase))/ @@(sizeof(nt!_MMPFN)) 的输出得知, PFN 数据库中条目的总数为 913993,而物理页面总数为 913408,如果按照一个 PFN 条目描述一个 4KB 范围的物理页面,那么数据库中还剩余 585 项未使用 )

非分页池区(*nt!MmNonPagedPoolStart ~ *nt!MmNonPagedPoolEnd)
非分页池区域紧随在 PFN 数据库之后。非分页池的起始地址存储在内核变量 nt!MmNonPagedPoolStart 中。当调用 MiObtainSystemVa() 时传入 MiVaNonPagedPool 类型的系统虚拟地址范围时,它就会在此区域中分配。内核位图——nt!MiNonPagePoolVaBitmap——控制从非分页池中分配虚拟地址空间的操作,并且,内核变量
nt!MiNonPagedPoolVaBitMapHint 存储其分配提示。

HAL 和 winload.exe 映射区域(FFFFFFFF`FFc00000~FFFFFFFF`FFFFFFFF)
内核全局变量 nt!MiLowHalVa 存储此区域的起始地址,即,0xFFFFFFFFFFC00000。此虚拟地址范围结束于 0xFFFFFFFFFFFFFFFF,它同时也是 X64 内核虚拟地址空间的结尾
此区域仅用于系统启动时刻,在 MmInitSystem() 例程的内部逻辑中,会将 HAL 与 winload.exe 映射到此区域。这意味着,在初始化阶段后,系统无法使用属于此地址范围的内存。

(译注:结合前面对“由最初的加载器映射”区域的讨论,可以看出,在系统引导阶段,winload.exe 将 NTOSKRNL.EXE,HAL.DLL 映射到 FFFFF800`00000000~FFFFF87F`FFFFFFFF 范围,然后,控制权转交内核时,也就是进入系统初始化阶段时,内核(NTOSKRNL.EXE 中的 MmInitSystem() 例程)反过来将 HAL 与 winload.exe 映射到 FFFFFFFF`FFc00000~FFFFFFFF`FFFFFFFF 范围,整个过程中,HAD.DLL 分别被映射到2个不同的系统虚拟地址范围。)
在系统初始化结束时刻,MmInitSystem() 调用函数 MiAddHalIoMappings(),后者扫描此虚拟地址范围并判断是否需要向由系统维护的 I/O 映射列表中添加任何 I/O 映射,如果需要,则调用 MiInsertIoSpaceMap() 例程。MiInsertIoSpaceMap() 为每个 I/O 映射创建一个追踪器条目,其带有名称为 MmIo "IO space mapping trackers " 的池标签,并且将该追踪器条目添加到一个双向链表(即 I/O 映射列表)中;内核变量 nt!MmIoHeader 指向此链表头部的地址。该双向链表中每个追踪器条目都代表一个被映射到系统页表条目(SysPTE)区域的物理内存块。
这些追踪器条目中的前几个字段(域)包含一些有趣的信息,描述了物理内存及其虚拟地址的映射。函数 MiInsertIoSpaceMap() 也会被 MmMapIoSpace() 例程调用,以跟踪系统上所有的适配器内存映射。

(译注:换言之,MiAddHalIoMappings() 在 FFFFFFFF`FFc00000~FFFFFFFF`FFFFFFF 区域中扫描,并且调用 MiInsertIoSpaceMap() 在 nt!MmIoHeader 指向的 I/O 映射列表中添加追踪器条目,虽然每个条目都包含一个指针,指向的内核虚拟地址空间属于“系统页表条目”区域,但是,nt!MmIoHeader 变量自身,以及它指向的 I/O 映射列表,不一定会在“系统页表条目”区域中)
下面是 I/O 映射列表(即前述的双向链表)中的追踪器条目的内部结构:
[FONT="微软雅黑"][SIZE="4"][COLOR="Black"][B]struct _IO_SPACE_MAPPING_TRACKER {

    LIST_ENTRY Link;

    PHYSICAL_ADDRESS  Pfn;

    ULONGLONG  Pages;

    PVOID Va;

    . . . 

}[/B][/COLOR][/SIZE][/FONT]

(译注:MmInitSystem() 与 MiAddHalIoMappings() 定义在 WRK 1.2 版的 mminit.c 源文件中,它们会在负责系统初始化阶段0和阶段1的 initos.c 源文件中被调用,具体而言,在系统初始化阶段0,由 initos.c 中,负责初始化执行体各组件的 ExpInitializeExecutive() 例程调用 MmInitSystem(),而MmInitSystem() 在阶段0的主要任务就是初始化执行体组件——内存管理器;稍后,由 nitos.c 中,负责系统初始化阶段1的 Phase1InitializationDiscard() 例程再次调用 MmInitSystem(),此时 MmInitSystem() 才会调用 MiAddHalIoMappings() ,执行扫描或添加 I/O 映射。
MiInsertIoSpaceMap() 则定义在 iosup.c 源文件中。上面的 _IO_SPACE_MAPPING_TRACKER 结构定义在 WRK 1.2 版中并不存在,取而代之使用了一个叫做 _MMIO_TRACKER 的结构来存储类似的信息,其定义在 mi.h 头文件中,如下所示,重要的字段添加了注释说明: )

[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]typedef struct _MMIO_TRACKER {

    LIST_ENTRY ListEntry;			

[B]    PVOID BaseVa;				//此成员存储的虚拟地址位于“系统PTE”区域内

    PFN_NUMBER PageFrameIndex;		// 虚拟地址映射的物理页框号

    PFN_NUMBER NumberOfPages;		// 追踪器条目使用的物理页面数量[/B]

    MI_PFN_CACHE_ATTRIBUTE CacheAttribute;

    PVOID StackTrace[MI_IO_BACKTRACE_LENGTH];

} MMIO_TRACKER, *PMMIO_TRACKER;[/COLOR][/SIZE][/FONT]

会话空间(FFFFF900`00000000~FFFFF97F`FFFFFFFF)
会话数据结构,会话池以及会话映像都被加载到这个区域内
会话映像空间包含驱动程序映像,例如 Win32K.sys(实现了窗口管理器), CDD.DLL(规范的显示驱动程序),TSDDD.dll(帧缓存显示驱动程序),DXG.sys(DirectX 图形驱动程序)。。等等。
对于任何属于某个会话的进程,其 EPROCESS 结构中的 Session 字段指向一个用于该会话的 MM_SESSION_SPACE 类型结构。会话的可分页池限制由 MM_SESSION_SPACE 的 PagesPoolStart 与 PagesPoolEnd 成员各自指向的地址来共同确定。(即,PagesPoolEnd 指向的结束地址,减去 PagesPoolStart 指向的起始地址)

系统页表条目(FFFFF880`00000000~FFFFF89F`FFFFFFFF)
此区域包含映射视图,MDLs(内存描述符列表),适配器内存映射,驱动程序映像,以及内核栈。(当然,还有前文提到的,用于 I/O 映射的虚拟地址)
此区域由位图 nt!MiSystemPteBitmap 描述;内核变量 nt!MiSystemPteBitMapHint 则存储此区域的分配提示。当调用 MiObtainSystemVa() 时传入 MiVaSystemPtes 类型的系统虚拟地址范围时,它会在此区域中分配。

动态内核虚拟地址空间(FFFFF980`00000000~FFFFFa70`FFFFFFFF)
此区域由系统缓存视图,可分页特殊池,以及非分页特殊池组成。
内核变量 nt!MiSystemAvailableVa 存储此区域中,可用于分配的 “2MB 区域”的数量。
当调用 MiObtainSystemVa() 时传入 MiVaSystemCache, MiVaSpecialPoolPaged, 或者 MiVaSpecialPoolNonPaged 类型的系统虚拟地址范围时,它都会在此区域中分配。
此区域由位图 nt!MiSystemVaBitmap 描述;内核变量 nt!MiSystemVaBitMapHint 则存储此区域的分配提示。

x64 Virtual Addressing Limitations
x64 虚拟寻址限制
As discussed previously, 64 bits of virtual address space allow for a possible maximum of 16 exabytes (EB) of virtual memory, a notable improvement over the 4 GB offered by 32-bit addressing. With such a copious amount of memory, it is obvious that today's computers, as well as tomorrow's foreseeable machines, are not even close to requiring support for that
much memory.

如我们前面所讨论的,64 位的虚拟地址空间支持可能最大的 16 EB 虚拟内存,这是对于 32 位寻址提供的 4 GB 虚拟内存的一个显著改进。很明显的,对于如此大量的内存,当前以及可预见未来的计算机(和其上运行的程序),甚至都不会需要这么多的内存支持。
Accordingly, to simplify chip architecture and avoid unnecessary overhead, particularly in address translation (to be described later), AMD's and Intel's current x64 processors implement only 256 TB of virtual address space. That is, only the low-order 48 bits of a 64-bit virtual address are implemented. However, virtual addresses are still 64 bits wide, occupying 8 bytes in registers or when stored in memory. The high-order 16 bits (bits 48 through 63) must be set to the same value as the highest order implemented bit (bit 47), in a manner similar to sign extension in two's complement arithmetic. An address that conforms to this rule is said to be a “canonical” address.
因此,为了简化处理器芯片架构以及避免非必要的开销——尤其是地址翻译方面(后面会讨论)—— 当前 AMD 和 Intel 的 x64 处理器仅实现了 16 EB 虚拟地址空间中的 256 TB。换言之,一个 64 位的虚拟地址中,仅有低 48 位被实现(使用)。然而,虚拟地址仍旧是 64 位宽,在寄存器中,或存储在内存中,它们都占用 8 字节。虚拟地址中的高 16 位(比特位 48~63)需要被设置成与最高的“实现位”(也就是比特位 47)相同的值,这是通过一种类似于二进制补码运算的符号扩展来完成的。符合这一运算规则的地址被称为“规范”(canonical)地址。
Under these rules, the bottom half of the address space thus starts at 0x00000000`00000000, as expected, but it ends at 0x00007FFF`FFFFFFFF. The top half of the address space starts at
0xFFFF8000`00000000 and ends at 0xFFFFFFFF`FFFFFFFF. Each “canonical” portion is 128 TB. As newer processors implement more of the address bits, the lower half of memory will expand upward, toward 0x7FFFFFFF`FFFFFFFF, while the upper half of memory will expand downward, toward 0x80000000`00000000 (a similar split to today's memory space but with 32 more bits).

根据这些规则,正如预期的那样,地址空间的下半部分从 0x00000000`00000000 开始,但是结束于 0x00007FFF`FFFFFFFF。地址空间的上半部分从 0xFFFF8000`00000000 开始,结束于 0xFFFFFFFF`FFFFFFFF。每个“规范的”部分为 128 TB。随着更新的处理器实现/使用更多的地址位,内存中的下半部分将会向上扩展,直到 0x7FFFFFFF`FFFFFFFF;而内存中的上半部分则会向下扩展,直到 0x80000000`00000000 (与当前的 32 位用户—内核内存空间分割法类似,只是又多出了 32 位)
Windows x64 16-TB Limitation
Windows x64 16-TB 限制
Windows on x64 has a further limitation: of the 256 TB of virtual address space available on x64 processors, Windows at present allows only the use of a little more than 16 TB. This is split into two 8-TB regions, the user mode, per-process region starting at 0 and working toward higher addresses (ending at 0x000007FF`FFFFFFFF), and a kernel-mode, systemwide region starting at “all Fs” and working toward lower addresses, ending at 0xFFFFF800`00000000 for most purposes. This section describes the origin of this 16-TB limit.
x64 上的 Windows(即 64 位版本)对 x64 处理器可用(实现)的 256 TB 虚拟地址空间作出了进一步的限制——当前(写作本书时)的 64 位 Windows 仅支持略多于 16 TB 虚拟地址空间的使用。
它被分为两个 8 TB 的区域:用户模式,每进程区域从 0 开始并且朝向更高的地址段增长(结束于 0x000007FF`FFFFFFFF);内核模式,系统范围的区域则从“all Fs”(译注:推测作者的意思是
指“0xFFFFFFFF`FFFFFFFF”)开始并且朝向更低的地址段增长,多数情况下它结束于 0xFFFFF800`00000000。本节讨论这个原始的 16 TB 限制。

A number of Windows mechanisms have made, and continue to make, assumptions about usable bits in addresses. Pushlocks, fast references, Patchguard DPC contexts, and singly linked lists are common examples of data structures that use bits within a pointer for nonaddressing purposes. Singly linked lists, combined with the lack of a CPU instruction in the original x64 CPUs required to "port" the data structure to 64-bit Windows, are responsible for this memory addressing limit on Windows for x64.
Windows 已经采用并将持续引入一系列机制,以假设地址中的可用二进制位。推锁,快速引用(fast references),Patchguard DPC 上下文,以及单向链表。。。这些数据结构都是使用一个非寻址用指针中二进制位的常见例子。单向链表的使用,再加上原始 x64 处理器中一条 CPU 指令的缺失(译注:在后文中可以看到这条缺失的指令就是 “CMPXCHG16B”),导致在64 位 Windows上,需要“port”该数据结构(即单向链表),才能让 64 位 Windows 负责此 16 TB 内存寻址限制的实施。
Here is the SLIST_HEADER, the data structure Windows uses to represent an entry inside a list:
下面是 Windows 用来表示链表内一个条目(一项)的数据结构,即 SLIST_HEADER:
[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]typedef union _SLIST_HEADER {

	ULONGLONG Alignment;

	struct {

		SLIST_ENTRY Next;	      //32 位

		USHORT Depth;	      //16 位

		USHORT Sequence;	      //16 位

	} DUMMYSTRUCTNAME;

} SLIST_HEADER, *PSLIST_HEADER;[/COLOR][/SIZE][/FONT]

Note that this is an 8-byte structure, guaranteed to be aligned as such, composed of three elements: the pointer to the next entry (32 bits, or 4 bytes) and depth and sequence numbers, each 16 bits (or 2 bytes). To create lock-free push and pop operations, the implementation makes use of an instruction present on Pentium processors or higher—CMPXCHG8B (Compare and Exchange 8 bytes), which allows the atomic modification of 8 bytes of data. By using this native CPU instruction, which also supports the LOCK prefix (guaranteeing atomicity on a multiprocessor system), the need for a spinlock to combine two 32-bit accesses is eliminated, and all operations on the list become lock free (increasing speed and scalability).
注意,在这个联合(union)中有一个 8 字节大小的结构,以及一个用来保证对齐长度的 8 字节(ULONGLONG 型)变量 Alignment;其中的结构体由三个元素组成:指向下一个条目(下一项)的指针(32 位,或者 4字节),一个 USHORT 型变量 Depth 用来表示深度(16 位,或者 2 字节),以及一个 USHORT 型变量 Sequence 用来表示序列号。为了创建无锁(lock-free)的压栈与弹栈操作,实现利用了在 Pentium 以及更高型号处理器中支持的一条指令——CMPXCHG8B(比较和交换 8 字节),该指令允许原子的修改 8 字节的数据。通过使用这一条原生的 CPU 指令,及其支持的 LOCK (指令)前缀(用来保证在一个多处理器系统上的原子性),一个自旋锁需要结合两个 32 位来访问的规则被取消了,并且链表中的所有操作都变成锁无关的(提高了速度与可扩展性)。
On 64-bit computers, addresses are 64 bits, so the pointer to the next entry should logically be 64 bits. If the depth and sequence numbers remain within the same parameters, the system must provide a way to modify at minimum 64+32 bits of data—or better yet, 128 bits, in order to increase the entropy of the depth and sequence numbers. However, the first x64
processors did not implement the essential CMPXCHG16B instruction to allow this. The implementation, therefore, was written to pack as much information as possible into only 64 bits, which was the most that could be modified atomically at once. The 64-bit SLIST_HEADER thus looks like this:

64 位计算机上的地址是 64 位的,因此指向下一个条目的指针(即 DUMMYSTRUCTNAME 结构的 Next 成员)逻辑上应该是 64 位的。如果深度和序列号成员的大小保持不变,那么系统必须提供一种方式来修改至少 64+32 位的数据——如果能够修改 128 位则更好,这是由于,为了增加深度与序列号成员的“熵”(entropy,平均信息量),而分别将这2个成员的大小增大一倍:32 位,所以整个结构体的大小就变为 128 位。然而,首个 x64 处理器并没有实现支持这一操作(原子的修改这 128 位数据)所必要的 CMPXCHG16B 指令。因此, Windows 自身的实现代码就被编写成:把尽可能多的信息封装到仅有的 64 位结构体中(前文中“port”一词的语义),这是让数据最可能被一次性原子地修改的方法(依旧使用 CMPXCHG8B)。封装尽可能多信息的结果导致了 64 位的 SLIST_HEADER 看起来像下面这个样子:
(译注:注意其中的 NextEntry 指针,在 64 位系统上它应该是 64 位宽,此处却缩短成了 39 位;而有趣的是,每个结构成员依旧被定义成按常理讲应该是 8 字节大小的 ULONGLONG 型)

[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]struct { // 8-byte header

	ULONGLONG Depth:16;

	ULONGLONG Sequence:9;

	ULONGLONG NextEntry:39;

} Header8;[/COLOR][/SIZE][/FONT]

The first change is the reduction of the space for the sequence number to 9 bits instead of 16 bits, reducing the maximum sequence number the list can achieve. This leaves only 39 bits for the pointer, still far from 64 bits. However, by forcing the structure to be 16-byte aligned when allocated, 4 more bits can be used because the bottom bits can now always be assumed to be 0. This gives 43 bits for addresses, but there is one more assumption that can be made. Because the implementation of linked lists is used either in kernel mode or user mode but cannot be used across address spaces, the top bit can be ignored, just as on 32-bit machines. The code will assume the address to be kernel mode if called in kernel mode and vice versa.  This allows us to address up to 44 bits of memory in the NextEntry pointer and is the defining constraint of the addressing limit in Windows.
第一个变化是,序列号成员的占用空间从原来的 16 位减少成只有 9 位,从而减小了链表能够实现的最大序列号。仅为 NextEntry 指针留下了 39 位,仍旧与 64 位相去甚远。然而,通过在为此数据结构分配内存空间时,强制其按照 16 字节大小对齐,就可以使用多出来的 4 位,因为最低的 4 位现在总是可以假设为 0 。这就为地址提供了 43 位(即 NextEntry 指针,39+4),但是系统还可以再作出一个假设。
因为链表的实现被用于内核模式或用户模式,但不能同时跨越二者的地址空间,因此可以忽略最高位,就像在 32 位机器上一样。如果函数调用在内核模式下,代码将假设使用的地址是内核模式的,反之亦然。这就允许我们使用 NextEntry 指针寻址最多 44 位的内存,它是 64 位 Windows 寻址限制的决定性约束。

Forty-four bits is a much better number than 32. It allows 16 TB of virtual memory to be described and thus splits Windows into two even chunks of 8 TB for user-mode and kernel-mode memory. Nevertheless, this is still 16 times smaller than the CPU's own limit (48 bits is 256 TB), and even farther still from the maximum that 64 bits can describe. So, with scalability in mind, some other bits do exist in the SLIST_HEADER that define the type of header being dealt with. This means that when the day comes when all x64 CPUs support 128-bit Compare and Exchange, Windows can easily take advantage of it (and to do so before then would mean distributing two different kernel images). Here's a look at the full 8-byte header:
44 位是一个比 32 更好的数字,它允许能够描述 16 TB 的虚拟内存,并由此将 Windows 分割成 2 个 8 TB 的块,分别用于用户与内核模式内存。尽管如此,16 TB 的虚拟内存仍旧只有处理器自身限制(48 位 = 256 TB)的 1/16,并且与 64 位能够描述的虚拟内存上限相比仍旧是九牛一毛。因此,在考虑到可扩展性的情况下,SLIST_HEADER 中确实有一些其它的位,用以定义所处理的头部类型。(译注:我们即将在下面给出的源码中看到,当结构中一个叫做 HeaderType 的二进制位,其值为 0 时,表示 SLIST_HEADER 的大小为 8 字节;其值为 1 时,表示大小为 16 字节,从而可以支持 NextEntry 指针使用 60 位来寻址,在这种情况下,能够支持 1 EB 的虚拟地址空间,计算方法是,每多出 4 位,实际可寻址的内存就为原来的 16 倍,即 2 的 4 次方 )
这意味着,未来某一天当所有的 x64 处理器都支持 128 位的“比较和交换”(即前文提到的 CMPXCHG16B 指令),那时 Windows 就可以很容易地利用这个硬件特性,寻址更大范围的地址空间(同时也意味着,需要预先发布两个不同的内核映像)下面来看看“完整的” 8 字节 SLIST_HEADER 头部:

[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]struct { // 8-byte header

	ULONGLONG Depth:16;

	ULONGLONG Sequence:9;

	ULONGLONG NextEntry:39;

	[COLOR="Red"]ULONGLONG HeaderType:1; 		// 0: 8-byte; 1: 16-byte[/COLOR]

	ULONGLONG Init:1; 			       // 0: uninitialized; 1: initialized

	ULONGLONG Reserved:59;

	ULONGLONG Region:3;

} Header8;[/COLOR][/SIZE][/FONT]

Note how the HeaderType bit is overlaid with the Depth bits and allows the implementation to deal with 16-byte headers whenever support becomes available. For the sake of completeness, here is the definition of the 16-byte header:
请注意,HeaderType 这个二进制位是如何覆盖 Depth 成员的二进制位(译注:虽然原文如是说,但是从给出的结构体布局还真的看不出来是怎么“覆盖”的),以及无论硬件是否支持,都允许实现代码处理 16 字节头部的。为了完整性起见,下面给出当 HeaderType 为 1 时,使用的 16 字节头部定义(布局):
[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]struct { // 16-byte header

	ULONGLONG Depth:16;

	ULONGLONG Sequence:48;

	ULONGLONG HeaderType:1; 		// 0: 8-byte; 1: 16-byte

	ULONGLONG Init:1; 			        // 0: uninitialized; 1: initialized

	ULONGLONG Reserved:2;

	[COLOR="Red"]ULONGLONG NextEntry:60; 		// last 4 bits are always 0's[/COLOR]

} Header16;

[/COLOR][/SIZE][/FONT]

Notice how the NextEntry pointer has now become 60 bits, and because the structure is still 16-byte aligned, with the 4 free bits, leads to the full 64 bits being addressable.
Conversely, kernel-mode data structures that do not involve SLISTs are not limited to the 8-TB address space range. System page table entries, hyperspace, and the cache working set all occupy virtual addresses below 0xFFFFF800`00000000 because these structures do not use SLISTs.

注意到 NextEntry 指针现在变成了 60 位,而且,由于该结构依然是按照 16 字节对齐的,前文提到过这种对齐方式能够多出 4 位来使用,因此这将导致所有的 64 位都可用来寻址。
反之,不涉及 SLISTs (单向链表)的内核模式数据结构,也不会因此被限制在 8 TB 的地址空间范围内。系统页表条目,超空间,以及缓存工作集等,都占用  0xFFFFF800`00000000 以下的虚拟地址空间,因为这些结构不使用单向链表。(译注:这里应该是作者笔误,因为根据图 10-13 以及 CodeMachine 站点上的 x64 地址空间布局概要,系统页表条目占用 0xFFFFF8A0`00000000 以下的虚拟地址空间,还有一个证据就是,去掉前面 5 个 F,即未用于寻址的 20 位,地址 0x8A000000000 约为 9 TB 多,符合作者原意,而地址 0x80000000000 基本就是 8 TB,与之不符 )
最后,用一张图来总结一下 x64 处理器支持的虚拟地址空间与 64 位 Windows 的虚拟地址空间限制之间的关系,该图取自 MSDN 文档,添加了说明文字:


Dynamic System Virtual Address Space Management
动态的系统虚拟地址空间管理
Thirty-two-bit versions of Windows manage the system address space through an internal kernel virtual allocator mechanism that we'll describe in this section. Currently, 64-bit versions of Windows have no need to use the allocator for virtual address space management (and thus bypass the cost), because each region is statically defined as shown in Table 10-8 earlier.
Windows 的 32 位版本通过一个内部的内核虚拟分配器机制来管理系统地址空间,我们将在本节讨论这一机制。当前, Windows 的 64 位版本没有必要使用分配器机制来管理虚拟地址空间(因而绕过了成本开销),请参考表 10-8(在第二部分译文中),这是由于, 64 位虚拟地址空间中的每个区域都是静态定义的。

When the system initializes, the MiInitializeDynamicVa function sets up the basic dynamic ranges (the ranges currently supported are described in Table 10-9) and sets the available virtual address to all available kernel space. It then initializes the address space ranges for boot loader images, process space (hyperspace), and the HAL through the
MiIntializeSystemVaRange function, which is used to set hard-coded address ranges. Later, when nonpaged pool is initialized, this function is used again to reserve the virtual address ranges for it. Finally, whenever a driver loads, the address range is relabeled to a driver image range (instead of a boot loaded range).

在系统初始化阶段,MiInitializeDynamicVa 函数建立起基本的动态范围(下面的表 10-9 列出了当前支持的范围),并将可用的虚拟地址设置成所有可用的内核空间。然后 MiInitializeDynamicVa() 通过调用 MiIntializeSystemVaRange 函数,初始化用于映射启动加载器映像(winload.exe),进程空间(超空间),以及 HAL 的地址空间范围。(译注:请参考图 10-8 与 10-13)
MiIntializeSystemVaRange() 用于设置硬编码(hard-coded)的地址范围。稍后,当非分页(非换页)池被初始化时,将再次使用该函数来为非分页池保留(reserve)虚拟地址范围。最后,每当一个驱动程序加载时,原本标记为启动加载器地址范围(MiVaBootLoaded)中的部分区域被重新标记为相应驱动程序映像的地址范围(MiVaDriverImages)。


(译注:关于 MiInitializeDynamicVa() 与 MiIntializeSystemVaRange() 这2个函数,从其名称的 Mi 前缀可以看出,这是执行体组件——内存管理器——内部的例程,i 表示 internal,即内部的。
即便如此,在 WRK 1.2 版源码的所有目录中的源文件也找不到这2个函数的定义;搜索 MSDN 站点也没有公开的在线文档;使用 kd.exe/windbg.exe 调试,甚至无法解析此符号,表明 Microsoft 没有在符号文件中提供有关这2个函数的信息;使用符号服务器的 URL 按需下载 ntkrpamp.pdb 符号文件,用于解析 ntkrnlpa.exe 中的机器指令,却得到刻意被模糊处理的反汇编输出;求助百度与谷歌搜索大神,有关的第三方文档少的可怜。。。以上种种迹象表明,Microsoft 有意隐藏这些函数内部的实现细节,算法,以及数据结构;从而保护其商业操作系统的核心竞争力。关于这一点,从原文作者仅仅是几句话轻描淡写的带过也可以推测出来。
不过,既然我们知道这2个函数实现在内核映像文件中,只需用 IDA PRO 打开分析即可,使用许可证已过期的 IDA PRO 6.6 版打开 ntkrnlpa.exe 的一份副本,左侧的函数窗口列出了其中所有的例程,包括暴露给用户模式的系统服务,例如Nt*();执行体组件和下层内核之间可以相互调用的接口函数,例如Ps*(),Ke*();以及前缀名包含 i 或 p 的例程,例如Ki*() ,Psp*()。。。所有这些加起来,一共有1万多个例程。其中,“i”表示该例程是没有公开文档的,至少在 MSDN 站点上查询不到,也可能没有相应的调试器符号;而“p”则表示该例程仅可以被相同组件中的例程调用,其它组件中的例程不能调用,也就是非接口函数。
在其中搜索这2个函数的名称,总算找到了,看来 IDA PRO 作为静态逆向分析的首选工具不是没有道理的。下面贴出 IDA PRO 对  MiInitializeDynamicVa() 例程的反汇编输出,重要的部分以红色高亮并且给出注释。可以观察到,代码逻辑中包含了对 MiIntializeSystemVaRange() 例程的调用,从而验证了原文讨论的内容。此外, MiInitializeDynamicVa() 是在 MmInitNucleus() 的 0xD5 偏移处被调用的,
后者同样是一个未文档化的例程,从其名称来推测,应该是负责内存管理器初始化的。看来,但凡涉及系统组件初始化的方方面面都被 Microsoft 视为机密。)

[FONT="微软雅黑"][SIZE="4"][COLOR="Black"]INIT:0079E4FC                         ; =============== S U B R O U T I N E =======================================

INIT:0079E4FC

INIT:0079E4FC

INIT:0079E4FC                         ; _DWORD __stdcall MiInitializeDynamicVa()

[COLOR="Red"]//这里标记的交叉引用表明 MiInitializeDynamicVa() 是在 MmInitNucleus() +0x5D 处被调用的

INIT:0079E4FC                         _MiInitializeDynamicVa@0 proc near      ; CODE XREF: MmInitNucleus(x)+D5p[/COLOR]

INIT:0079E4FC 8B FF                       mov     edi, edi

INIT:0079E4FE 51                          push    ecx

INIT:0079E4FF 83 25 78 B7 54 00 00        and     _MiSystemVaLock, 0

INIT:0079E506 56                          push    esi

INIT:0079E507 57                          push    edi

INIT:0079E508 6A 00                       push    0                           ; State

INIT:0079E50A 6A 00                       push    0                           ; Type

INIT:0079E50C 68 60 BC 54 00              push    offset _MiFreeSystemCacheVa ; Event

INIT:0079E511 E8 05 8C C9 FF              call    _KeInitializeEvent@12       ; KeInitializeEvent(x,x,x)

INIT:0079E516 6A 00                       push    0                           ; State

INIT:0079E518 6A 01                       push    1                           ; Type

INIT:0079E51A 68 80 BC 54 00              push    offset _MiReclaimNonPagedPoolEvent ; Event

INIT:0079E51F E8 F7 8B C9 FF              call    _KeInitializeEvent@12       ; KeInitializeEvent(x,x,x)

[COLOR="Red"]//此处将 MmSystemRangeStart 的值载入 EAX 寄存器

INIT:0079E524 A1 4C C8 56 00              mov     eax, ds:_MmSystemRangeStart[/COLOR]

INIT:0079E529 8B C8                       mov     ecx, eax

INIT:0079E52B F7 D9                       neg     ecx

[COLOR="Red"]//原文中“将可用的虚拟地址设置成所有可用的内核空间”

INIT:0079E52D 89 0D 84 BB 54 00           mov     _MiSystemAvailableVa, ecx[/COLOR]

INIT:0079E533 89 0D 80 BB 54 00           mov     _MiSystemAvailableVaLow, ecx

[COLOR="Red"]// 这里的比较指令证实了,MmSystemRangeStart 的值就是 0x80000000,即 32 位 Windows 的系统空间起始地址

INIT:0079E539 3D 00 00 00 80              cmp     eax, 80000000h[/COLOR]

INIT:0079E53E 74 19                       jz      short loc_79E559

INIT:0079E540 8D 88 00 00 00 80           lea     ecx, [eax+80000000h]

INIT:0079E546 C1 E9 15                    shr     ecx, 15h

INIT:0079E549 BA 7F BB 54 00              mov     edx, offset unk_54BB7F

INIT:0079E54E 85 C9                       test    ecx, ecx

INIT:0079E550 74 07                       jz      short loc_79E559

INIT:0079E552

INIT:0079E552                         loc_79E552:                             ; CODE XREF: MiInitializeDynamicVa()+5Bj

INIT:0079E552 C6 02 03                    mov     byte ptr [edx], 3

INIT:0079E555 4A                          dec     edx

INIT:0079E556 49                          dec     ecx

INIT:0079E557 75 F9                       jnz     short loc_79E552

INIT:0079E559

INIT:0079E559                         loc_79E559:                             ; CODE XREF: MiInitializeDynamicVa()+42j

INIT:0079E559                                                                 ; MiInitializeDynamicVa()+54j

INIT:0079E559 8B F0                       mov     esi, eax

INIT:0079E55B C1 EE 12                    shr     esi, 12h

INIT:0079E55E 81 E6 F8 3F 00 00           and     esi, 3FF8h

INIT:0079E564 8B F8                       mov     edi, eax

INIT:0079E566 81 EE 00 00 A0 3F           sub     esi, 3FA00000h

INIT:0079E56C 85 FF                       test    edi, edi

INIT:0079E56E 74 21                       jz      short loc_79E591

INIT:0079E570

INIT:0079E570                         loc_79E570:                             ; CODE XREF: MiInitializeDynamicVa()+93j

INIT:0079E570 8B 06                       mov     eax, [esi]

INIT:0079E572 0B 46 04                    or      eax, [esi+4]

INIT:0079E575 74 0F                       jz      short loc_79E586

[COLOR="Red"]//此处的立即数“3”,实际上就是表 10-9 中的第三项:MiVaBootLoaded(0x3),表明下面第一次调用 MiInitializeSystemVaRange() ,将初始化用于 winload.exe 的系统虚拟地址空间范围

INIT:0079E577 6A 03                       push    3[/COLOR]

INIT:0079E579 8D 8F 00 00 20 00           lea     ecx, [edi+200000h]

INIT:0079E57F 8B C7                       mov     eax, edi

[COLOR="Red"]//   1st call for winload.exe

INIT:0079E581 E8 69 E5 C6 FF              call    _MiInitializeSystemVaRange@12 ; MiInitializeSystemVaRange(x,x,x)[/COLOR]

INIT:0079E586

INIT:0079E586                         loc_79E586:                             ; CODE XREF: MiInitializeDynamicVa()+79j

INIT:0079E586 83 C6 08                    add     esi, 8

INIT:0079E589 81 C7 00 00 20 00           add     edi, 200000h

INIT:0079E58F 75 DF                       jnz     short loc_79E570

INIT:0079E591

INIT:0079E591                         loc_79E591:                             ; CODE XREF: MiInitializeDynamicVa()+72j

[COLOR="Red"]// 数据段中的 MmHyperSpaceEnd 全局变量,存储超空间的结束地址

INIT:0079E591 8B 0D 50 C1 56 00           mov     ecx, ds:_MmHyperSpaceEnd[/COLOR]

[COLOR="Red"]//索引表 10-9 中的第二项:MiVaProcessSpace(0x2),验证了原文中讲到的,第二次调用 MiInitializeSystemVaRange() ,初始化用于超空间的系统虚拟地址空间范围

INIT:0079E597 6A 02                       push    2[/COLOR]

INIT:0079E599 41                          inc     ecx

INIT:0079E59A B8 00 00 00 C0              mov     eax, 0C0000000h

[COLOR="Red"]//2nd call  for   hyperspace

INIT:0079E59F E8 4B E5 C6 FF              call    _MiInitializeSystemVaRange@12 ; MiInitializeSystemVaRange(x,x,x)[/COLOR]

[COLOR="Red"]// 数据段中的 MiLowHalVa 全局变量,存储为 HAL.DLL 分配的虚拟地址下限值

INIT:0079E5A4 A1 84 73 56 00              mov     eax, _MiLowHalVa[/COLOR]

[COLOR="Red"]//索引表 10-9 中的第十项:MiVaHal ,验证了原文中讲到的,第三次调用 MiInitializeSystemVaRange() ,初始化用于 HAL 的系统虚拟地址空间范围

INIT:0079E5A9 6A 0A                       push    0Ah[/COLOR]

INIT:0079E5AB 33 C9                       xor     ecx, ecx

[COLOR="Red"]//   3rd call  for   hal.dll

INIT:0079E5AD E8 3D E5 C6 FF              call    _MiInitializeSystemVaRange@12 ; MiInitializeSystemVaRange(x,x,x)[/COLOR]

INIT:0079E5B2 5F                          pop     edi

INIT:0079E5B3 5E                          pop     esi

INIT:0079E5B4 59                          pop     ecx

INIT:0079E5B5 C3                          retn

INIT:0079E5B5                         _MiInitializeDynamicVa@0 endp[/COLOR][/SIZE][/FONT]

IDA PRO 绘制出的 MiInitializeDynamicVa() 内部流程如下图所示,原文中所有2句话就带过的函数,例程,都可以用类似的方法来验证

After this point, the rest of the system virtual address space can be dynamically requested and released through MiObtainSystemVa (and its analogous MiObtainSessionVa) and MiReturnSystemVa.
Operations such as expanding the system cache, the system PTEs, nonpaged pool, paged pool, and/or special pool; mapping memory with large pages; creating the PFN database; and creating a new session all result in dynamic virtual address allocations for a specific range. Each time the kernel virtual address space allocator obtains virtual memory ranges for use by a certain type of virtual address, it updates the MiSystemVaType array, which contains the virtual address type for the newly allocated range. The values that can appear in MiSystemVaType are shown in Table 10-9.

在这之后,系统虚拟地址空间中剩下的区域可以通过 MiObtainSystemVa() (及其类似的 MiObtainSessionVa())与 MiReturnSystemVa() ,分别动态地请求(分配)和释放。
诸如像扩展系统缓存,系统页表条目,非分页池,分页池,以及/或特殊池;大页面内存映射,创建 PFN 数据库,以及创建一个新的会话。。。所有这些操作都将导致一个特定范围的动态虚拟地址分配。内核虚拟地址空间分配器每次通过某种类型的虚拟地址,获得使用的虚拟内存范围时,它会更新 MiSystemVaType 数组,该数组包含新近分配范围的虚拟地址类型。MiSystemVaType 数组中可能出现的值都列在了表 10-9 中。

(译注:表 10-9 有一个对应的枚举类型的系统数据结构,叫做 _MI_SYSTEM_VA_TYPE,下面是内核调试器转储的结果)

(此外,关于 MiSystemVaType 数组的内容,下面是一个例子,可以看到,每个字节都表示一个分配的,特定类型的系统虚拟地址范围,这些字节的取值都在表 10-9 或 _MI_SYSTEM_VA_TYPE 描述的范围内)

Although the ability to dynamically reserve virtual address space on demand allows better management of virtual memory, it would be useless without the ability to free this memory. As such, when paged pool or the system cache can be shrunk, or when special pool and large page mappings are freed, the associated virtual address is freed. (Another case is when the boot registry is released.) This allows dynamic management of memory depending on each
component's use. Additionally, components can reclaim memory through MiReclaimSystemVa, which requests virtual addresses associated with the system cache to be flushed out (through the dereference segment thread) if available virtual address space has dropped below 128 MB. (Reclaiming can also be satisfied if initial nonpaged pool has been freed.)

尽管按需动态保留虚拟地址空间的能力允许了更佳的虚拟内存管理方式,但如果它没有办法释放已分配的内存,那么这种能力就毫无用处。因此,当分页池或系统缓存可能缩小时,或者当特殊池与大页面映射被释放时,与其关联的虚拟地址就被释放。另一种情况是,当启动注册表(boot registry,原文直译,但不知作者所指为何)被释放时。这就允许根据每个内核组件的使用情况来实施动态内存管理。
此外,组件可以通过 MiReclaimSystemVa() 例程回收内存,如果可用的虚拟地址空间已经低于 128 MB,此例程会请求与系统缓存关联的虚拟地址应被冲洗掉(通过”内存段解引用线程“,即 MiDereferenceSegmentThread(),关于此线程的细节,请参考第一部分译文)(如果初始的非分页池已经被释放,那么也可以被回收)

In addition to better proportioning and better management of virtual addresses dedicated to different kernel memory consumers, the dynamic virtual address allocator also has advantages when it comes to memory footprint reduction. Instead of having to manually preallocate static page table entries and page tables, paging-related structures are allocated on demand. On both 32-bit and 64-bit systems, this reduces boot-time memory usage because unused addresses won’t have their page tables allocated. It also means that on 64-bit systems, the large address space regions that are reserved don't need to have their page tables mapped in memory, which allows them to have arbitrarily large limits, especially on
systems that have little physical RAM to back the resulting paging structures.

除了专门为不同的内核内存消费者提供更恰当的比例分配,以及更好的虚拟地址管理外,当涉及降低内存占用率时,动态虚拟地址分配器也具有它的优势。与分页(换页)相关的数据结构是按需分配的,而不需手动预先分配静态页表条目和页表。在 32 位和 64 位系统上,这都能够减少引导阶段的内存使用率,这是由于,未使用的地址将不会有它们的页表分配。这也意味着,在 64 位系统上,被保留出来的大范围地址空间不需要让它们的页表映射到内存,因此这些区域能够有任意范围的限制,特别是在只有很少物理 RAM 的系统上,这样就能够节省导致分页的数据结构。
EXPERIMENT: Querying System Virtual Address Usage
实验:查询系统虚拟地址的使用情况
You can look at the current usage and peak usage of each system virtual address type by using the kernel debugger. For each system virtual address type described in Table 10-9, the  MiSystemVaTypeCount, MiSystemVaTypeCountFailures, and MiSystemVaTypeCountPeak arrays in the kernel contain the sizes, count failures, and peak sizes for each type. Here's how you can dump the usage for the system, followed by the peak usage (you can use a similar technique for the failure counts):
通过内核调试器,你可以检阅每一类系统虚拟地址的当前使用情况,以及峰值使用情况。对于表 10-9 中列出的每一类系统虚拟地址,内核空间中的 MiSystemVaTypeCount,MiSystemVaTypeCountFailures,以及 MiSystemVaTypeCountPeak 数组,分别存储了对应类型系统虚拟地址的大小,失败次数,以及峰值大小。下面的输出向你展示了如何转储系统虚拟地址的使用情况
(命令 dd /c 1 MiSystemVaTypeCount l c),以及峰值使用情况
(命令 dd /c 1 MiSystemVaTypeCountPeak l c)(你可以使用类似的技巧来转储分配失败的次数)

[FONT="微软雅黑"][SIZE="4"]lkd> dd /c 1 MiSystemVaTypeCount l c

81f4f880 00000000                [B][COLOR="Red"]//会话空间[/COLOR][/B]

81f4f884 00000028                [B][COLOR="Red"]//进程地址空间[/COLOR][/B]

81f4f888 00000008                [B][COLOR="Red"]//winload.exe(NTLDR 的继任者)加载”启动型“驱动程序使用[/COLOR][/B]

81f4f88c 0000000c                 [B][COLOR="red"]//PFN数据库[/COLOR][/B]

81f4f890 0000000b                [B][COLOR="red"]//不可分页池[/COLOR][/B]

81f4f894 0000001a                [B][COLOR="red"]//可分页池[/COLOR][/B]

81f4f898 0000002f                [B][COLOR="red"]//特殊/专用池[/COLOR][/B]

81f4f89c 00000000                [B][COLOR="red"]//系统缓存[/COLOR][/B]

81f4f8a0 000001b6               [B][COLOR="red"]//系统 PTEs[/COLOR][/B]

81f4f8a4 00000030               [B][COLOR="red"]//HAL[/COLOR][/B]

81f4f8a8 00000002              [B][COLOR="red"]//会话全局空间[/COLOR][/B]

81f4f8ac 00000006              [B][COLOR="red"]//用于已加载的驱动程序映像(.sys)[/COLOR][/B]

lkd> dd /c 1 MiSystemVaTypeCountPeak l c

81f4f840 00000000

81f4f844 00000038

81f4f848 00000000

81f4f84c 00000000

81f4f850 0000003d

81f4f854 0000001e

81f4f858 00000032

81f4f85c 00000000

81f4f860 00000238

81f4f864 00000031

81f4f868 00000000

81f4f86c 00000006[/SIZE][/FONT]

Theoretically, the different virtual address ranges assigned to components can grow arbitrarily in size as long as enough system virtual address space is available. In practice, on 32-bit systems, the kernel allocator implements the ability to set limits on each virtual address type for the purposes of both reliability and stability. (On 64-bit systems, kernel address space
exhaustion is currently not a concern.) Although no limits are imposed by default, system administrators can use the registry to modify these limits for the virtual address types that are currently marked as limitable (see Table 10-9).

理论上而言,只要存在足够的系统虚拟地址空间可供使用,分配给内核组件的不同系统虚拟地址区域可以随意增大。而在实际应用中,32 位系统上的内核内存分配器实现了对每一类虚拟地址设限的能力;设置上限的目的是为了可靠性还有稳定性。(在 64 位系统上,目前无需担心“内核地址空间耗尽”的问题。)尽管在默认情况下没有强加任何限制,对于在表 10-9 中,当前被标记为“可被限制”的虚拟地址类型,系统管理员可以使用注册表来修改这些限制。
If the current request during the MiObtainSystemVa call exceeds the available limit, a failure is marked (see the previous experiment) and a reclaim operation is requested regardless of available memory. This should help alleviate memory load and might allow the virtual address allocation to work during the next attempt. (Recall, however, that reclaiming affects only system cache and nonpaged pool).
如果在执行 MiObtainSystemVa() 例程调用的期间,当前请求超出了可用的限制,则会标记一个“分配失败”(译注:前文提及的内核数组 MiSystemVaTypeCountFailures 用来保存每一类系统虚拟地址的“分配失败”次数 ),并且,无论是否有可用内存,都会请求(并执行)一个相关的回收操作(译注:估计是通过前文提及的 MiReclaimSystemVa() 例程来回收内存)。这应该有助于缓解内存负载,并且允许虚拟地址分配机制在(请求者)下一次尝试时,能够正常工作。(然而,请回想一下,回收操作仅对系统缓存区域和非分页池区域有效)
EXPERIMENT: Setting System Virtual Address Limits
实验:设定系统虚拟地址限制
The MiSystemVaTypeCountLimit array contains limitations for system virtual address usage that can be set for each type. Currently, the memory manager allows only certain virtual address types to be limited, and it provides the ability to use an undocumented system call to set limits for the system dynamically during run time. (These limits can also be set through the registry, as described at http://msdn.microsoft.com/en-us/library/bb870880(VS.85).aspx.) These limits can be set for those types marked in Table 10-9.
You can use the MemLimit utility (http://www.winsiderss.com/tools/memlimit.html) from Winsider Seminars & Solutions to query and set the different limits for these types, and also to see the current and peak virtual address space usage. Here's how you can query the current limits with the –q flag:

对于每一类可设限的系统虚拟地址使用情况, MiSystemVaTypeCountLimit 数组包含了相应的限制。目前,内存管理器仅允许对某些虚拟地址类型做出限制,而且,它提供在系统运行期间,使用未文档化(未公开)的系统调用来动态地设限的能力。(也可以通过注册表来设置这些限制,请参考 http://msdn.microsoft.com/en-us/library/bb870880(VS.85).aspx)。表 10-9 标记了能够设限的地址类型。你可以使用 Winsider Seminars & Solutions 的 MemLimit 工具(http://www.winsiderss.com/tools/memlimit.html)来查询和对这些类型做出不同的限制,还可以查看当前和峰值的虚拟地址空间使用情况。通过该工具的 -q 标志,你可以查询这些信息:
[FONT="微软雅黑"][SIZE="4"]C:\ >[B]memlimit.exe -q[/B]

MemLimit v1.00 - Query and set hard limits on system VA space consumption

Copyright (C) 2008 Alex Ionescu

www.alex-ionescu.com

System Va Consumption:

[B]Type[/B]                               [B]Current[/B]                              [B]Peak[/B]                            [B]Limit[/B]

Non Paged Pool               102400 KB                         0 KB                            0 KB

Paged Pool                      59392 KB                           83968 KB                    0 KB

System Cache                 534528 KB                        536576 KB                   0 KB

System PTEs                   73728 KB                          75776 KB                     0 KB

Session Space                 75776 KB                           90112 KB                    0 KB[/SIZE][/FONT]

As an experiment, use the following command to set a limit of 100 MB for paged pool:
作为一次实战,使用下面的命令来设置可分页池的上限为 100 MB:
[FONT="微软雅黑"][SIZE="4"]memlimit.exe -p 100M[/SIZE][/FONT]

And now try running the testlimit –h experiment from Chapter 3 (in Part 1) again, which attempted to create 16 million handles. Instead of reaching the 16 million handle count, the process will fail, because the system will have run out of address space available for paged pool allocations.
现在,试着运行第三章的实验中使用过的 testlimit 工具,添加 -h 选项,它会尝试创建 16000000 (16 million)个句柄。此时 testlimit 进程会输出创建失败信息,而不是真的到达 16000000 个句柄数,因为系统将耗尽可分页池中,可用于分配的地址空间。
System Virtual Address Space Quotas
系统虚拟地址空间配额
The system virtual address space limits described in the previous section allow for limiting systemwide virtual address space usage of certain kernel components, but they work only on 32-bit systems when applied to the system as a whole. To address more specific quota requirements that system administrators might have, the memory manager also collaborates with the process manager to enforce either systemwide or user-specific quotas for each process.
前一节讨论的系统虚拟地址空间限制,虽然允许对某些内核组件的系统级虚拟地址空间使用率设限,但是在 32 位系统上,只有此一机制被应用到整个系统时,才能正常工作。为了解决系统管理员更多的具体配额需求,内存管理器与进程管理器联手协作,来强制实施每个进程的“系统级”或“用户相关”配额(限制)。
The PagedPoolQuota, NonPagedPoolQuota, PagingFileQuota, and WorkingSetPagesQuota values in the HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management key can be configured to specify how much memory of each type a given process can use. This information is  read at initialization, and the default system quota block is
generated and then assigned to all system processes (user processes will get a copy of the default system quota block unless per-user quotas have been configured as explained next).

注册表键 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management 下的 PagedPoolQuotaNonPagedPoolQuota,,PagingFileQuota,以及 WorkingSetPagesQuota 键值,其数据可以被配置,对于一个给定的进程,分别指定它能够使用每一类地址空间中的内存上限。系统在初始化时读取这些信息,并且生成默认的系统配额块(system quota block),然后分配给所有系统进程(除非已经配置了每用户的配额——稍后解释——否则用户进程将得到一份“默认系统配额块”的副本)
To enable per-user quotas, subkeys under the registry key HKLM\SYSTEM\CurrentControlSet\Session Manager\Quota System can be created, each one representing a given user SID. The values mentioned previously can then be created under this specific SID subkey, enforcing the limits only for the processes created by that user. Table 10-10 shows how to configure these
values, which can be configured at run time or not, and which privileges are required.

要启用每用户的配额,可以在注册表键 HKLM\SYSTEM\CurrentControlSet\Session Manager\Quota System 下面,创建相应的子键,每个子键代表一个给定用户的 SID 。(译注:系统上的每一个帐户都有一个关联的 SID,例如,系统内置帐户 NT AUTHORITY\SYSTEM 的 SID 为 S-1-5-18;NT AUTHORITY\LOCAL SERVICE 帐户的 SID 为 S-1-5-19;而用户创建的帐户,如 user1 ,其 SID 类似 S-1-5-21-2263621008-420926174-3264184512-1000 这种格式;作者的意思就是要用这些 SID 作为子键的名称)。然后,就可以在这个特定的 SID 子键右侧,创建前文提到的四类键值(分别代表相应类型的内核虚拟地址空间),从而仅对由该用户创建的进程(例如,以 user1 帐户运行的 explorer.exe 资源管理器和它创建的子进程),在这些地址空间的使用上实施限制。下表 10-10 向你展示如何配置这些键值的数据;是否可在运行时配置;以及要使配置生效所需的权限:


User Address Space Layout
用户地址空间布局
Just as address space in the kernel is dynamic, the user address space is also built dynamically—the addresses of the thread stacks, process heaps, and loaded images (such as DLLs and an application's executable) are dynamically computed (if the application and its images support it) through a mechanism known as Address Space Layout Randomization, or ASLR.
正如内核地址空间是动态的那样,用户地址空间也是动态建立的——线程栈地址,进程堆,以及加载的映像(例如 DLL 和一个应用程序的可执行文件),都是通过一个被称为地址空间布局随机化(ASLR)的机制,来动态计算的(假设应用程序及其映像文件支持这种技术)。
At the operating system level, user address space is divided into a few well-defined regions of memory, shown in Figure 10-14. The executable and DLLs themselves are present as memory mapped image files, followed by the heap(s) of the process and the stack(s) of its thread(s). Apart from these regions (and some reserved system structures such as the TEBs and PEB), all other memory allocations are run-time dependent and generated. ASLR is involved with the location of all these run-timedependent regions and, combined with DEP, provides a mechanism for making remote exploitation of a system through memory manipulation harder to achieve. Since Windows code and data are placed at dynamic locations, an attacker cannot typically hardcode a meaningful offset into either a program or a system-supplied DLL.
在操作系统层级,用户地址空间被划分为数个明确定义好的内存区域,如图 10-14 所示。可执行文件与 DLL 文件,它们自身都作为内存映射映像存在,接着是进程堆及其线程的栈。除了这些区域外(以及某些保留的系统结构,如“TEBs”——线程环境块,和“PEBs”——进程环境块;因为同样运行在用户模式下的映像加载器和子系统 DLL 需要向 PEB 与 TEB 中写入信息,所以这2个环境块必须位于用户空间,内核空间在用户模式下不可写),所有其它的内存分配都依赖于运行时(动态)生成。ASLR 参与所有这些运行时相关区域的位置(确定),它与 DEP (译注:数据执行保护。请参考第一部分译文)相结合——这样就提供了一种保护机制,使得通过内存操纵来远程利用一个系统(的漏洞)更难实现。(译注:这里指的就是远程执行代码的缓冲区溢出攻击,例如使用 metasploit 通过网络发送 shellcode 来取得一个从受害机器到攻击主机的反弹 shell,而且还具备管理员,或者 root 权限。这是由于,被攻击的有缺陷的应用程序同样是以这些特权级别运行的 )
既然 Windows 代码和数据被放置在动态区域,攻击者通常无法将一个有意义的偏移量硬编码至一个程序或系统提供的 DLL 里。


EXPERIMENT: Analyzing User Virtual Address Space
实验:分析用户虚拟地址空间
The VMMap utility from Sysinternals can show you a detailed view of the virtual memory being utilized by any process on your machine, divided into categories for each type of allocation, summarized as follows:
Image                 Displays memory allocations used to map the executable and its dependencies (such as dynamic libraries) and any other memory mapped image (portable executable format) files
■ Private                 Displays memory allocations marked as private, such as internal data structures, other than the stack and heap
■ Shareable         Displays memory allocations marked as shareable, typically including shared memory (but not memory mapped files, which are either Image or Mapped File)
■ Mapped File         Displays memory allocations for memory mapped data files
■ Heap                 Displays memory allocated for the heap(s) that this process owns
■ Stack                 Displays memory allocated for the stack of each thread in this process
■ System                 Displays kernel memory allocated for the process (such as the process object)
The following screen shot shows a typical view of Explorer as seen through VMMap.

Sysinternals 出品的 VMMap 实用工具能够向你显示你机器上的任意进程使用的虚拟内存详细视图,每种类型的分配被划分为相应的类别(段),概括如下:
Image                 显示用于映射可执行文件及其依赖关系(例如动态链接库),以及任何其他的内存映射映像文件(可移植可执行格式,即 PE 文件)的内存分配。        (需要特别指出:在 VMmap 的 image 类型段中的 DLL 虽然没有明确指出是否为共享,但实际上系统将所有进程加载的相同 DLL(非第三方)都映射到相同的物理内存页,也就是仅维护一个 DLL 副本,然后在所有进程之间共享——通过共享文件映射的方式——所以本质上 image 类型段中的所有系统 DLL 与下文提及的 shareable 类型段,Mapped File 类型段一样,都是基于共享文件映射来实现共享。)
Private                 显示被标记为“私有的”(private)内存分配,例如内部数据结构(堆栈除外,它们有各自独立的区域)。(Private data 类型段有几个区域来保存相应线程的“线程环境块”信息,以线程 ID来标识该区域是保存哪个线程的环境信息;Private data 类型段还有一个区域用来保存进程环境块的信息,因而,存储 TEB 和 PEB 的区域与堆区/栈区是不同的)
Shareable         显示被标记为“共享的”(shareable)内存分配,通常包含共享内存(不包括内存映射文件,它要么属于 Image 类,要么属于 Mapped File 类)
Mapped File        显示用于“内存映射数据文件”的内存分配。 Mapped File 类型段经常用于进程对数据文件的访问(即 .nls 后缀的文件,以及  ttf/ttc/fon 类型文件)。可以在进程浏览器(process explorer)中,查看某个进程加载的 DLL 视图,然后添加“Mapping type”列,类型为 data 的文件就是进程通过共享文件映射的数据文件。综上所述,Image 类型段中的 DLL(代码),Shareable 以及 Mapped File 类型段中的数据文件,都可以在进程之间共享。
Heap                 显示用于此进程堆的内存分配。(通常堆区也分为2种类型:进程私有堆区,以及可共享的堆区)
Stack                 显示用于此进程中每个线程的栈的内存分配。(进程内的各线程都会使用栈区,因此进程栈区的每个“区间”由各自所属的线程 ID 来标识)
System                 显示此进程的内核内存分配(例如进程对象)
Reserved      又称作“保留的”或“尚未使用的”。此类型段用于扩展进程/线程的堆栈,例如每次扩展栈大小就增大已使用的 Stack 类型段,并减小此类型段。一般而言,未使用空间多数分布在共享内存,堆和栈区,共享内存区可以和其它相同进程共享数据,堆栈的大小也是动态变化,因此这3个区域不可能在进程刚运行阶段就满载,一定还会有剩余的空间。
Unusable    又称作“不可使用”的。此类型段用来表示其它类型的虚拟内存段之间随机偏移的缝隙,用于为前文讨论的 ASLR 技术提供一定的前后缓冲,仔细观察图 10-14,你会发现每个类型段之间都有间隔,它们就是 unusable 类型段。
Free    又称作“空闲”的。在 VMmap 中可以通过选择 Fragmentation view 查看所有穿插在进程地址空间的空闲区域,这些区域使得地址空间非连续分布,形成所谓的内存片断;一般而言,进程的内存片断布局是由映像加载器在首次运行该进程时,以及内核的(动态)内存管理模式共同决定的;我们可以使用 sysinternal 的 Testlimit 工具加上 -t 选项来动态创建“线程栈”。当动态创建大量线程栈的时候,系统无法保证这些栈区都是连续不断的,通常在栈区之间会形成随机大小的片断,这些片断就属于空闲区域。
下面的屏幕截图显示使用 VMMap 分析“资源管理器”(Explorer.EXE)的虚拟内存布局的典型视图:


(译注:我们运用本章前面所学的知识,尝试分析一下上图 VMmap 的输出中,“image”一行中,各列值的含义:
image 虚拟内存段映射的 41176 个页面中(164704 / 4),Total WS,即驻留在物理内存中的总共有 9534 个页面( 38136 KB / 4KB),而实际提交(commited,已使用)的页面也是 41176 个;其中,有 41176 - 9534 = 31642 个页面位于磁盘上的页面文件内((164704 - 38136) / 4KB = 31642),这 3 万多个页面的其中一些会在必要的时刻由缺页异常处理程序,或者其它的内核例程将其换入物理内存;另外,9534 个驻留在物理内存的页面中,Private WS,即“私有”的页面有 714 个(2856 KB / 4KB);Shareable WS ,即“可共享”的页面有 8820 个 (35280 KB / 4KB);Shared WS ,即“已共享”的页面有 5421 个(21684 KB / 4KB);
结合对原文相关内容的理解,“可共享”和“已共享”的页面通常包含资源管理器运行时依赖的动态链接库——多数属于 Windows 子系统 DLL 。“私有”的页面通常包含资源管理器的可执行 PE 文件。
查看  C:\Windows\explorer.exe  的属性,其大小为 2.8 MB ,与“私有”的页面总大小相符。
还要注意, type 列中的 total 类型段的 size 列表示 ,分配给资源管理器进程的所有虚拟内存“段”的总大小,包括 image,Mapped file,Shareable,Heap,Stack,Private data 等全部加起来的总大小,其值为 416936 KB,即 104234 个页面;而它实际使用的(commited)总大小为 319456 KB,即 79864 个页面;而驻留在物理内存中的页面总数为 88436 KB / 4KB = 22109 页面——这 2 万多个驻留在物理内存中的页面,分别映射资源管理器进程的不同类型虚拟内存段;而剩余的 79864 - 22109 = 57845 个页面,表示位于磁盘上的页面文件内的页面总数,它们也用于映射不同类型的虚拟内存段。你可以查看每种类型虚拟内存段各自驻留在物理内存中,以及位于页面文件内的页面数并相加来验证这一点。)

再来看一个例子,我们启动 notepad.exe(记事本),然后运行 VMmap 查看记事本的用户地址空间布局,选中“image”类型段,工具底部会显示此类型段的详细信息,如下图所示:

notepad.exe 可执行文件自身被加载到 0x005A0000 的虚拟地址,它和它依赖的 DLL 都启用了 ASLR ,也就是每次加载的地址都是随机的。点击“0x005A0000 节点”可以展开其中的“子”区域,可以看到:
■  notepad.exe 的代码节(.text)被加载到  0x005A1000,为其分配了 44 KB,实际使用了 44 KB,驻留在物理内存的页面数为 11 个(44 / 4),其中有 1 个(4 / 4)是私有的;其余 10 个是可共享的,并且已共享了 10 个 。用于映射代码节的页面是只读(10 个共享的页面不可写,这也是可执行文件映像中的特定“节”用于共享时的保护策略),并且是可执行的。
■  数据节(.data)加载到 0x005AC000,同样启用了 ASLR,当前仅分配和使用了 1 个页面,驻留在物理内存中,并且是私有的,可读写。
■  还有一个数据节被加载到  0x005AD000,其保护措施为“写时复制”(Copy On Write),根据第一部分译文对写时复制的讨论可知,直到进程实际向被标记为写时复制的共享页面写入时,内存管理器才在物理内存中分配一个原始页的副本,然后将进程指向这个副本页面,这就是为什么它的“Total WS”和后续的“WS”列没有数值的原因——进程当前尚未往其中写入内容,因此这些页面不在物理内存中。

继续分析 notepad.exe 的内存映射中,“Mapped file”段的内容,如下图所示:

“映射文件”共享内存段包含三个区域:0x00060000,0x01FA0000,以及 0x028D0000,很明显,由于是共享的,所以在“Private”列中没有相关信息;在“Detial”列中,可以得知用于共享的具体文件:两个 nls 文件和一个 dat 文件,这与原文描述的相符。
接下来分析 notepad.exe 进程的堆区,如下图所示:

第一个堆区是可共享的(Type 为Shareable)。前文提到过,进程的堆区包含私有堆区,以及可共享的堆区。第二个私有堆的虚拟地址从 0x00170000 开始,为其分配了 1024 KB,实际使用了 280 KB,
也就是有 744 KB 是未使用的,展开“0x00170000 节点”,你可以看到 744 KB 的未使用子区域,其各列的值均为空,并且“Protection”列标记出“Reserved”,前文提到过,Reserved 类型段用于扩展进程/线程的堆栈区;在“Detial”列中,显示出该堆区的 ID (请参考后续译文),以及它是一个“低碎片堆”(能够显著减少堆内存碎片,具体请参考第二部分译文);后面几个私有堆区都分配了各自的 ID,并且都是可读写的。

接着是 notepad.exe 进程的栈区,如下图所示:

notepad.exe 进程内部的每个线程使用各自的栈区,“Detial”列的线程 ID 指出该栈区被哪个线程使用;全部的线程栈都是私有的,否则其它进程内的线程就可以任意读写其中的代码和数据。每个分配的线程栈中,包含了已使用和未使用的子区域,这一点类似于进程堆区。可以看出,为 notepad.exe 的 4 个线程栈都分配了 256 KB ,每个线程只使用了 76 KB,并且只有 1~4 个线程页面驻留在物理内存中。
前面提到过,进程的虚拟地址空间中,有一种“Private data”类型段,用于存储 PEB 和 TEB,其中,每个 TEB 用线程 ID 来标识该区域是保存哪个线程的环境信息,如下图所示:

可以看到,notepad.exe 进程的“Private data”段中,有 4 个段分别保存各自线程的环境信息,以及一个段用于保存进程环境块信息;这些内部数据结构都是私有的。注意,这些区域(0x7FFxxxxx)已经非常接近内核与用户空间的分界线,这意味着它们可以被同样是位于分界线附近的用户空间映像加载器,以及子系统 DLL 方便地访问(例如,Private data 段与 ntdll.dll 都被映射到 2.0 GB ~ 2.1GB 附近的虚拟地址空间)
点击“Total”类型段, VMmap 会显示 notepad.exe 进程的完整虚拟地址空间布局,这非常有助于我们从宏观的角度来理解用户地址空间的组成,从而“既见树木又见林”,如下图所示,其中最后一个线程栈区(0x02C60000)与首个加载的 dwmapi.dll (0x73B50000)之间,存在 1.8 GB 多的“空闲”(Free)区域。如前所述,空闲区域是指:穿插在进程地址空间的未分配区域,这些区域使得地址空间非连续分布,形成所谓的内存片断。另外,最后一个进程私有堆区(0x01770000)与首个“映射文件”共享内存区(0x01FA0000)之间,也存在 8 MB 左右的空闲区域,这体现出内存管理器对用户空间的动态管理模式:

甚至在映射到 notepad.exe 进程虚拟地址空间内的不同 DLL 之间,也存在空闲区域和“不可使用”(unusable)区域,如前所述,DLL 之间的空闲区域分布取决于映像加载器运行该进程时的行为模式,例如,是否要执行“DLL 重定位”。
“不可使用”区域则用于为 ASLR 技术提供前后缓冲空间,DLL 映像每次都可以加载到这个缓冲空间内的任意地址,但是正如你在下面这张截图中所见的,这些“不可使用”区域一般都只有几十 KB,造成 ASLR 技术在实际应用中的成效不彰,此机制甚至可能被绕过:


Depending on the type of memory allocation, VMMap can show additional information, such as file names (for mapped files), heap IDs (for heap allocations), and thread IDs (for stack allocations). Furthermore, each allocation's cost is shown both in committed memory and working set memory. The size and protection of each allocation is also displayed.
VMMap 是否能够显示额外的信息,这取决于内存分配的类型,比如文件名称(对于映射文件),堆 ID (对于堆分配),以及栈 ID(对于栈分配)。再者,每个分配的成本开销都通过“提交的内存”(committed memory)以及工作集内存来表示。对于每个分配,“size”与“protection”列中也显示了具体的值。
(译著:原文没有详细解读 VMMap 输出中的每一列含义及其高级功能,youtube 站点上有一个80分钟的完整视频教程,原书作者  Mark Russinovich 在视频中通过 VMMap 揭秘了用户地址空间的内幕,视频 URL 如下,国内用户可能需要 VPN 工具来翻墙访问:https://www.youtube.com/watch?v=AjTl53I_qzY

在上面的视频中,演示了一种使用 TestLimit 工具来自动生成“内存碎片”(空闲区域)的方式,然后就可以使用 VMmap 查看  TestLimit 进程空间中的内存碎片。
首先,在 CMD 提示符下,执行命令 Testlimit.exe -t  ,这会在 TestLimit 进程空间中创建大量的线程栈,具体数量取决于机器上的可用 RAM 大小,例如,在我的机器上能够创建 1500 多个线程,并且没有空间来创建更多的线程:


使用 VMmap 查看  TestLimit 进程的虚拟地址空间布局,点击“Stack”节点,可以看到,系统为 TestLimit 进程内每个创建的线程都分配了对应的线程栈(由 TID 标识),除了首个线程栈的大小为 256 KB 外(其中有 2 个页面驻留在物理内存中),其余线程栈的大小都是 1 MB,这也是 32 位体系结构上的线程栈默认大小(64 位体系结构上会创建相等数量的 256 KB 与 1 MB 线程栈);因为每个线程栈的大小为 1 MB,所以 TestLimit 进程的栈区总大小现在为 1576192 KB,即 1.5 GB ,其中包含 1500 多个线程栈:


通过前面的学习我们知道,每个线程都需要有一个 TEB 来描述其环境信息,因此,上面的创建操作也会导致同时在 TestLimit 进程的“Private data 段”中,创建相等数量的 TEB,由于每个 TEB 的大小为 4 KB,因此 1500 个 TEB 加上一个 PEB ,加上该区域中其它一些较大的子区域,其总大小为 7392 KB,如前所述,TEB/PEB 集中分布在用户空间与内核空间的分界附近,这与线程栈在虚拟地址空间中的分布有鲜明的区别:


通过点击“Total”节点查看 TestLimit 进程空间的总体布局你就会发现:由于系统无法保证为其分配的线程栈在用户空间的分布是连续不断的,这就造成了不同线程栈区域之间存在随机大小的空闲(Free)区域,即内存碎片。注意,为了能够显示空闲区域,你需要在主菜单的“Option”->勾选“Show Free and Unusable Regions”


所有空闲区域加起来的总大小为 488 MB,其中最大的单个空闲区域(0x64160000,960 KB)位于 0x64060000 的线程栈与 0x64250000 的线程栈之间:

VMmap 提供了一个内存碎片在虚拟地址空间中分布的直观,可视化显示:从主菜单中依序选择“View”->“Fragmentation view”,它能够绘制并展现出每种类型区域分布的关联性:

VMmap 能够为进程从创建到后续操作的每个阶段创建快照,按下 F5 可以刷新至当前快照;时间线(TimeLine)则通过记录所有快照描绘出进程的虚拟地址空间使用随时间的变化。还是以前面的记事本进程为例子,首次运行 notepad.exe 进程时,不要执行任何操作,使用 VMmap 查看,按下 F5,然后点击右下角的时间线按钮,则开始记录虚拟地址空间的使用情况;此后,执行 notepad.exe 的“打开文件”操作,在 VMmap 中再次按下  F5 ,其时间线记录出了虚拟内存的变化,可以看到,打开文件操作使得 notepad.exe 进程的内存占用从原来的 45 MB 升高到 111 MB 多;而且,如果单击并且拖动时间线中,这个线性平缓上升的区域,在虚拟地址空间布局的总体视图中,你会看到许多绿色的区域,它们表示因执行打开文件操作导致分配的内存,其中包括了新分配的“映射文件”,“线程栈”,“Private data”(用于存储新分配线程及其栈区的环境信息),等区域,以及分配用于映射新加载的 DLL 区域。。。等等,如下图所示:

“打开文件”操作还会导致“释放”或“回收”notepad.exe 进程地址空间中的少部分区域,例如下图中以红色显示的项目,其中的数值大小都是负的,表明这些地址空间被释放或者回收了:

现在,关闭前面 notepad.exe 进程打开的“打开文件”对话框,然后激活时间线视图并按 F5,你会看到虚拟内存使用率从 111 MB 降到了约 96 MB,拖动鼠标选择下图中的一段平缓下降区域,VMmap 会在其主界面的“Committed”栏目的最右侧,显示减少(释放)的虚拟内存数量——16 MB:

进一步追踪这释放的 16 MB 虚拟内存都是哪些类型的。单击“Image”类型段,我们发现,因关闭对话框操作导致一些加载至 notepad.exe 进程地址空间的 DLL 被卸载了,从而回收了约 7.4 MB 的虚拟内存:

关闭“打开文件”对话框还导致 notepad.exe 进程地址空间中的“映射文件”共享内存区域释放了 5.2 MB ,还有堆区也释放了 256 KB。下图显示用于映射 thumbcache 系列数据文件的一些虚拟内存区域都被释放了:

你可以从 MSDN 站点下载 VMmap,TestLimit ,以及其它 Sysinternals 套件中的工具。
ASLR begins at the image level, with the executable for the process and its dependent DLLs. Any image file that has specified ASLR  support in its PE header (IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE), typically specified by using the /DYNAMICBASE linker flag in Microsoft Visual Studio, and contains a relocation section will be processed by ASLR. When such an image is found, the system selects an image offset valid globally for the current boot. This offset is selected from a bucket of 256 values, all of which are 64-KB aligned.
应用 ASLR 机制的第一步从进程的可执行文件映像及其依赖的 DLL 映像开始。 任何在其 PE 文件头中指定了 ASLR 支持(IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE)的映像文件——通常由 Microsoft Visual Studio 的 /DYNAMICBASE 链接器标志指定——都会包含一个重定位的节,该节将由 ASLR 来处理。找到此类映像时,系统会为进程的本次启动选取一个全局有效的映像偏移量。此偏移量是从一组 256 个值中随机选取的一个,它们必须按照 64-KB 大小进行对齐。
(译注:我们以 Chrome 浏览器自带的动态链接库文件 chrome_child.dll 为例,如下图所示:)


Image Randomization
映像随机化

For executables, the load offset is calculated by computing a delta value each time an executable is loaded. This value is a
pseudo-random 8-bit number from 0x10000 to 0xFE0000, calculated by taking the current processor's time stamp counter (TSC),  shifting it by four places, and then performing a division modulo 254 and adding 1. This number is then multiplied by the allocation granularity of 64 KB discussed earlier. By adding 1, the memory manager ensures that the value can never be 0, so executables will never load at the address in the PE header if ASLR is being used. This delta is then added to the executable's preferred load address,  creating one of 256 possible locations within 16 MB of the image address in the PE header.

对于可执行文件,通过计算它每次加载时的 delta 值(差值或增量)来得出加载偏移量,它是介于 0x10000 到 0xFE0000 之间的 8 位的伪随机数,加载偏移量具体的计算方法是:
一,取得当前处理器的时间戳计数器(TSC),将其移动 4 位;
二,执行一次除以 254 的求模运算后再加 1;
三,将得出的数字乘以前文讨论过的 64 KB 的分配粒度。
(如果我们将 0x10000 除以 64 KB 就得到 1;将 0xFE0000 除以 64 KB 就得到 254,这就是第二步中的结果 )。
通过第二步中的“加 1”操作,内存管理器能够确保该值绝不为 0,因此对于在 PE 头中启用 ASLR 的可执行文件而言,它们绝对不会被加载到地址 0 处。此 delta 值稍后被添加到可执行文件的首选加载地址,最终在 PE 文件头中的映像地址(译注:实际上是 “Image Base ”字段)中,创建 256 个可能的加载区域之一 ,这些区域的取值都在 16 MB 范围内。
(译注:要理解上面这段内容,最好的办法是通过实验来证实,例如,使用 Process Explorer,查看任何启用了 ASLR 的应用程序,例如计算器——calc.exe,它恰好被加载到进程地址空间的 0xFE0000,也就是原文中描述的上限地址—— 16 MB,多数启用了ASLR 的进程的可执行文件都被加载到 0x10000~0xFE0000,也就是 64 KB~16MB 的区域以内,此类文件 PE 头的“Image Base ”字段值通常为 0x1000000。作为对比,没有指定 IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE 标记的可执行文件一般会加载到默认的 0x400000 地址处,与它们文件 PE 头的“Image Base ”字段指定的位置相同。你还可以反复启动一个 calc.exe 进程,透过 Process Explorer,你会发现每次它的加载地址都不一样,但
都在 0x10000~0xFE0000 之间变化,从而验证了原文的描述)

For DLLs, computing the load offset begins with a per-boot, systemwide value called the image bias, which is computed by
MiInitializeRelocations and stored in MiImageBias. This value corresponds to the time stamp counter (TSC) of the current CPU when this function was called during the boot cycle, shifted and masked into an 8-bit value, which provides 256 possible values. Unlike executables, this value is computed only once per boot and shared across the system to allow DLLs to remain shared in physical memory and relocated only once. If DLLs were remapped at different locations inside different processes, the code could not be shared. The loader would have to fix up address references differently for each process, thus turning what had been shareable read-only code into process-private data. Each process using a given DLL would have to have its own private copy of the DLL in physical memory.

对于 DLL,加载偏移量的计算从一个叫做“image bias”的每启动的系统范围值开始,该值由例程 MiInitializeRelocations() 计算得出,并且存储在内核变量 MiImageBias 中。该值对应于在启动期间,调用 MiInitializeRelocations() 返回的当前 CPU 的时间戳计数器。然后同样的将其移位,屏蔽成一个 8 位的值,这就提供了 256 种可能的取值。( 即 MiImageBias 的取值从 0x1~0xFF)
与可执行文件不同的是,每次启动时只计算一次这个加载偏移量,并且被整个系统共享,从而允许 DLL 在物理内存中保持共享,或者仅重定位一次。对于后者,假设 DLL 被重新映射到不同进程内部的不同区域,那么 DLL 中的代码就无法被共享。映像加载器将不得不为每个进程修正地址引用的
差别,从而将原本可共享的只读代码转变为进程私有的数据。在这种情况下,每个加载了一个给定 DLL 的进程都会在物理内存中持有一份它自己私有的 DLL 副本。

Once the offset is computed, the memory manager initializes a bitmap called the MiImageBitMap. This bitmap is used to represent ranges from 0x50000000 to 0x78000000 (stored in MiImageBitMapHighVa), and each bit represents one unit of allocation (64 KB, as mentioned earlier). Whenever the memory manager loads a DLL, the appropriate bit is set to mark its location in the system; when the same DLL is loaded again, the memory manager shares its section object with the already relocated information.
一旦计算得出偏移值,内存管理器会初始化一个叫做 MiImageBitMap 的位图。它用来表示从 0x50000000 到 0x78000000 之间的范围(后者,即上限,存储在内核变量 MiImageBitMapHighVa 中),此位图中的每一个比特代表一个分配的单元(前面说过,每个单元的大小为 64 KB)。
每当内存管理器加载一个 DLL 时,就会设置相应的比特位,从而标记该 DLL 在系统中的位置;再次加载相同的 DLL 时,内存管理器就会共享用于表示该 DLL 的 section 对象,此对象包含了已重定位的信息。

As each DLL is loaded, the system scans the bitmap from top to bottom for free bits. The MiImageBias value computed earlier is used as a start index from the top to randomize the load across different boots as suggested. Because the bitmap will be entirely empty when the first DLL (which is always Ntdll.dll) is loaded, its load address can easily be calculated: 0x78000000 – MiImageBias * 0x10000.
Each subsequent DLL will then load in a 64-KB chunk below. Because of this, if the address of Ntdll.dll is known, the addresses of other DLLs could easily be computed. To mitigate this possibility, the order in which known DLLs are mapped by the Session Manager during initialization is also randomized when Smss loads.

每个 DLL 加载时,系统自顶向下扫描位图,寻找空闲的比特位。先前计算出的 MiImageBias 值此刻被用作从位图顶端起始的索引,以作为跨越不同启动的随机化加载建议。由于在首个 DLL (Ntdll.dll)加载时,MiImageBitMap 将完全是空的,Ntdll.dll 的加载地址可以容易地计算得出:
0x78000000 – (MiImageBias * 0x10000) 。这里,0x78000000 是 MiImageBitMap 的“顶端”,减去 MiImageBias 的效果就是从顶端起始向下索引,0x10000 就是 10 进制的 64 KB 分配对齐大小。后续的每个 DLL 将各自加载到单独的 64 KB 块内。正因为如此,假设已知 Ntdll.dll 的加载地址,其它 DLL 的加载地址能够很容易地计算得出。(因为它们都相距 64 KB)
为了减轻这种可被预测性,在系统初始化期间,Smss.exe 加载时,由会话管理器在其中映射的已知 DLL 的顺序也被随机化。
(Smss.exe 是在执行体初始化过程的最后阶段,由 System 进程创建第一个用户模式进程,smss.exe 本身仅使用 Windows 原生系统调用服务——仅导入了 ntdll.dll 中的本机 API 用户模式代理,因此它加载的 DLL 只有少数几个)
(这样一来,每个 DLL 就不一定在地址空间中都间隔 64 KB,但会是 64 KB 的整数倍,后面会验证)

Finally, if no free space is available in the bitmap (which would mean that most of the region defined for ASLR is in use), the DLL relocation code defaults back to the executable case, loading the DLL at a 64-KB chunk within 16 MB of its preferred base address.
最后,假设 MiImageBitMap 位图中没有可用的空闲空间(这意味着为 ASLR 而定义的绝大多数区域都被使用了),DLL 重定位代码将默认回到
可执行文件的情况—— 将 DLL 加载到它 16 MB 范围的首选加载基址内中的一个 64 KB 块。(即 0x10000~0xFE0000 中的任意一个 64 KB 块)
(译注:小结一下,指定了 ASLR 的可执行文件在每次运行时的加载地址都不同,但多数介于 0x10000~0xFE0000 ;而指定了 ASLR 的系统 DLL,由于需要在进程间共享,对于每个共享它的进程而言其加载地址是相同的,只有在重启系统后,加载地址才会发生改变,而且这些 DLL 通常
被加载到地址空间的 0x78000000~0x50000000 ,前者由内核变量 MiImageBitMapHighVa 存储,首个系统 DLL 加载到这个范围中的具体位置由算法:0x78000000 – (MiImageBias * 0x10000) 确定;每次系统启动时 MiImageBias 存储的值都不一样,有 256 种可能的取值,这就实现了一定程度的随机性。后续的系统 DLL 在 0x78000000~0x50000000 区域内的加载布局并非都间隔固定的 64 KB,而是 64 KB 的随机整数倍,这进一步增强了不可预测性。简单地讲,位图 MiImageBitMap 就是用来描述 0x78000000~0x50000000 这片地址空间使用情况的内核变量)

译注:原文的描述是针对 Windows 7 和 Server 2008 的 ASLR,但是没有给出实现源码(也是这一套教材最美中不足的地方),
考虑到几位作者在 Microsoft 公司的职位较敏感,这也是可以理解的。国内外有一些独立的安全研究人员不受相关的员工条款限制,因而能够在网上自由发表包含商业源码的文章。有鉴于此,译者特别对其中一篇讲解 Windows Vista sp1 ASLR 内幕的文章进行了翻译并放在这里,加深各位的理解。
总体而言,Windows 7 / Server 2008 (NT 6.1 内核)与 Windows Vista sp1 (NT 6.0 内核)的 ASLR 关键环节基本大同小异,但还是要提醒一点,下面这篇内嵌的译文是针对 Windows Vista sp1 的 ASLR。对于文中引用源码的重要部分自行添加了注释说明。完整的原文可以参考
http://www.hakim.ws/BHUSA08/speakers/Sotirov_Dowd_Bypassing_Memory_Protections/BH_US_08_Sotirov_Dowd_Bypassing_Memory_Protections.pdf  中的“ASLR”一节。

Address Space Layout Randomization (ASLR) is a security feature that is intended to randomize the location of where objects will be mapped into memory in the virtual address space of a given process. When implemented correctly, ASLR provides a significant hurdle to a would-be attacker, since they will not know the precise location of an interesting address to overwrite. Furthermore, even if an attacker is able to overwrite a useful pointer in memory (such as a saved instruction pointer on the stack), rewriting it to point to something of value
will also be difficult.

地址空间布局随机化(ASLR)是一种安全特性,对于一个给定的进程而言,它旨在随机化对象将被映射到其虚拟地址空间中的内存位置。当正确地实施此功能时,ASLR 将为潜在的攻击者提供一个明显的障碍,因为它们将无法得知一个感兴趣地址的准确位置来将其改写。此外,就算一个攻击者能够改写内存中一个有用的指针(例如,一个保存在栈上的指令指针),将其重写来指向某些有用的东西(译注:如 shellcode)也会很困难。
Although the concept of ASLR is not new, it is a relatively recent addition to Windows. Windows Vista and Windows Server 2008 are the first operating systems in the Windows family to provide ASLR natively. Previous to these releases, there were a number of third party solutions available that provided ASLR functionality to varying degrees. This paper will focus on Vista's native implementation.
尽管 ASLR 并非一个新概念,但它是相对新近才加入到 Windows 中的。在 Windows 产品家族中,Windows Vista 和 Windows Server 2008 是首批原生提供 ASLR 的操作系统。 在这之前的发布版中,有一些在不同程度上提供 ASLR 功能的第三方解决方案。本文将关注 Vista 的原生实现。
Vista's ASLR randomizes the location of images (binaries mapped into memory), threads, stacks, and other process control information (primarily the PEBs and TEBs). Each of these components will be examined briefly, and then some of the limitations of the system will be explored later in the paper.
Vista 的 ASLR 会随机化映像(映射进内存的二进制文件),线程,栈,以及其它进程控制信息(主要是 PEB 和 TEB——进程环境块和线程环境块)的位置。本文将会简要地考察上述的每一个组件,然后探讨系统的一些局限性。
Image positioning randomization is designed to place images at a random location in the virtual address space of each process. Vista's ASLR has the capability to randomly position both shared libraries (DLLs) and executable files. Note that in order for a library or an executable to be randomly rebased, there are several conditions that need to be met; these will be discussed shortly. Before talking about the specifics, it is worth mentioning that there is a system-wide configuration parameter that will determine the exact behaviour regarding whether images are relocated in memory. This behaviour is controlled using the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\MoveImages, which by default does not exist. The following behaviour is defined for this key:
? If the value is set to 0, never randomize image bases in memory, always honour the base address
? If set to -1, randomize any image regardless of whether they have elected to take part in ASLR or not (providing they are relocatable).
? If set to any other value, randomize only images that have elected to take part in randomization. This is the default behaviour.

映像位置随机化被设计成把映像放置在每个进程各自虚拟地址空间中的一个随机位置。Vista 的 ASLR 能够随机定位(放置)共享库文件(DLLs)和可执行文件。注意,为了让一个库文件或可执行文件能够被随机地重定位,需要满足几个条件;后面将会对其进行讨论。在描述具体细节之前,值得一提的是,有一个系统范围的配置参数将决定有关映像是否在内存中被重定位的确切行为。此行为可以使用如下注册表键值进行控制:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\MoveImages
该键值默认不存在。该键值的取值描述如下:
如果值为 0 ,绝不会在内存中随机化映像基址,总是按照 PE 文件头中指定的基址来加载
如果值为 -1,随机化任何映像,无论它们是否选择了参与 ASLR(假设它们是可重定位的)
如果设置为任何其它值,仅随机化选择参与了 ASLR 的映像,这是默认行为。

Executable images are randomized if they elect to take part in ASLR and also have relocation information present. Participating in ASLR is indicated by setting the IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE (0x40) flag in the DllCharacteristics value, which is located within the PE file's optional header. Generally speaking, when a new address is being selected as an image base for an executable, a random
value will be added to or subtracted from the ImageBase value proposed in the executable's optional header. This random value is based on the time given by the system clock, and can take on a possible 255 different values (from 0x10000 through 0xFF0000). The result is that the image will be loaded at a random point within 16 MB of the preferred image base. The following code is some rough pseudocode derived from MiSelectImageBase() on Vista SP1, which shows the base address selection for an executable.

如果可执行映像文件选择参与 ASLR ,它们的加载基址会被随机化,这就会存在重定位信息(此类 PE 文件中都会包含一个重定位的节,该节将由 ASLR 来处理)。通过在 PE 文件的“可选”头中,为 DllCharacteristics 字段指明 IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE (0x40) 标志,这样就参加了 ASLR。一般而言,当一个新地址被选作为一个可执行文件的映像基址时,会在可执行文件的 PE 可选头的“Image Base”字段所推荐的值中,加上或者减去一个随机的值。此随机值基于由系统时钟给出的时间,可以取 256 种不同的值(从 0x10000 到 0xFF0000)。结果就是,映像将
被加载到 16 MB 的首选映像基址中的一个随机点。下面给出一些粗略的伪代码,源自 Vista SP1 的 MiSelectImageBase() 函数,它是 ASLR 机制的核心函数,负责选择一个可执行文件的基地址:

//注意,下面部分的示例代码取自 Ollie Whitehouse  (BH Federal 2007) 的简报,很明显,他从 Microsoft 那里获得了此源码。
#define  X64K  0x10000
#define  PAGE_SIZE  4096
/*  ImageInfo:指向存储 PE 文件信息结构的指针
ExportedImageInformation.ImageCharacteristics:表示 PE 文件特性(是 .exe 或 .dll)的字段。
usPageCountdiv16:值为 16 的整数倍 ,表示为每个加载的 DLL 或 EXE 分配 16 个连续的页面(0x10000 字节)的“批数”;也就是在 _MiImageBitMap 中,要通过RtlFindClearBitsAndSet() 设置的连续比特位(每个比特位代表“一批”16 个页面)
dwStartIndex:存储 RtlFindClearBits() 或 RtlFindClearBitsAndSet() 例程的返回值(在 _MiImageBitMap 中找到的起始空闲比特位)
_MiImageBias:在启动期间,调用 MiInitializeRelocations() 返回的当前 CPU 的时间戳计数器。然后将其移位,屏蔽成一个 8 位的值,这就提供了 256 种可能的取值。MiImageBias 的取值从 0x1~0xFF。具体求值过程参考标记为“RelocateExe”的代码块
_MiImageBitMap:描述 DLL 在 0x78000000~0x50000000 区域的加载情况的位图,长度为 0x2800 字节,此位图中的每一个比特位代表一个大小为 0x10000 字节的分配单元,用于特定 DLL。

先前计算出的 MiImageBias 随机值此刻被 RtlFindClearBits() 用作从位图 _MiImageBitMap 顶端起始的随机索引,自顶向下扫描位图,寻找空闲的比特位(置 0 的),并将其保存在 dwStartIndex 中;假设要重定位的映像文件不是 DLL ,或者 RtlFindClearBits() 返回 -1(表明 MiImageBitMap 位图中没有可用的空闲空间来实施 ASLR),DLL 重定位代码将默认回到可执行文件的情况(if 语句中的 RelocateExe 代码块)—— 将 DLL 加载到它 16 MB 范围的首选加载基址内中的一个 64 KB 块。(即 0x10000~0xFE0000 中的任意一个 64 KB 块) */
if ( (ImageInfo->ExportedImageInformation.ImageCharacteristics & IMAGE_FILE_DLL) == 0  ||
        (dwStartIndex = RtlFindClearBits( _MiImageBitMap, usPageCountdiv16,  _MiImageBias)) == -1 )
{
                RelocateExe:
                TSCStart = ReadTimeStampCounter();                 //此例程通过执行 rdtsc 指令,读取 CPU 的时间戳计数器中的值,在 x86 体系结构上,rdtsc 指令会把一个 64 位的值写入 EDX : EAX 寄存器组。由于该值可预测,在 windows 8 中,使用了更安全的 Intel CPU 指令 rdrand。
               
                Delta = (ULONG) ( (TSCStart & 0xFF) * X64K);                // Delta 是随机的时间戳值和 255 进行按位与运算,再乘以 64 KB 后得出的值。 windows 7 的 Delta 求值过程与此稍有差异,具体请参考前文

                if (Delta == 0)
                {
                                Delta = X64K;
                }
               
                // 假设 PE 文件可选头中的首选映像加载基址大于等于用户空间的上限,或者映像大小(以字节为单位,按 64 KB 对齐)超过用户空间的上限,
                //或者映像的结束地址(映像基址加上映像大小)小于映像基址,或者结束地址大于用户空间的上限,MiSelectImageBase() 都将返回 0,
                //表明目标映像加载基址无法被重定位或随机化

                if (ImageInfo->dwImageBase >= _MmHighestUserAddress ||
                (dwImageSize = usPageCountdiv16 * 16 * PAGE_SIZE) > _MmHighestUserAddress ||
                (dwImageEnd = ImageInfo->dwImageBase + dwImageSize) < ImageInfo- >dwImageBase ||
                ImageInfo->dwImageBase + dwImageSize > _MmHighestUserAddress)
                {
                                return 0;
                }               
               
               
                if ( (outputInfo->dwOffset14 + Delta) && (ImageInfo->dwImageBase < Delta) && // outputInfo->dwOffset14 == 0
                ( (dwNewBase = ImageInfo->dwImageBase + Delta) < ImageInfo->dwImageBase ||
                dwImageSize + dwNewBase > _MmHighestUserAddress || dwImageSize + dwNewBase < dwImageEnd) ) )
                {
                                   return 0;
                }       
                else
                {
                                  /*  如果映像的各种属性值通过前面一系列的检查,从首选映像加载基址中,减去这个随机的 Delta 值(回顾前文,在  windows 7 中是“加上”这个Delta 值 ),然后保存在 dwNewBase,作为新选取的映像加载基址返回到调用者  */
                                  dwNewBase = ImageInfo->dwImageBase - Delta;
                }       
                                       
                outputInfo->dwOffset1C = -1;
                outputInfo->usOffset20 = usPageCountdiv16;
                ImageInfo->pArea->dwOffset34 = -1;
                ImageInfo->pArea->usOffset38 = usPageCountdiv16;
               
                return dwNewBase;
}

As can be seen, a number of sanity checks are performed, and, providing they succeed, the new image base is returned. Note that a return value of 0 from MiSelectImageBase() indicates that an image cannot be relocated.
可以看到,执行了一系列的完整性检查,假设检查通过,则返回新的映像基地址。注意,如果 MiSelectImageBase() 例程返回 0,则表明该映像无法被重定位。
For DLLs, the randomization process is slightly different. For efficiency reasons, it is preferable to be able to load a DLL at the same address for each process that uses that DLL. To facilitate this behaviour, a global bitmap (named _MiImageBitMap) is used to represent a portion of the available address space starting from the highest available load address (0x78000000 on a 32- bit address space) and extending down towards 0. Each bit in the bitmap represents 16 pages (64K on an intel machine). Since _MiImageBitMap is 0x2800 bytes in length, the portion of the address space represented is 0x28000000 bytes (from 0x78000000 through 0x50000000). As each DLL is loaded, its position is recorded by setting the appropriate number of consecutive bits in the bitmap according to the size of the DLL
being mapped. The following pseudocode demonstrates this process.

对于 DLL,随机化的过程略有不同。出于效率方面的考量,最好能够将一个 DLL 加载到每个使用该 DLL 的进程地址空间内相同的位置。为了促进这一行为,一个叫做 _MiImageBitMap 的全局位图用于表示一部分可用的地址空间:从最高的可用加载地址 0x78000000 (32 位地址空间)开始,向下扩展到 0。此位图中的每个比特位代表 16 个页面(在 Intel 机器上,当页大小为 4 KB 时,16 个页面总大小为 64 KB)。由于 _MiImageBitMap 的大小为 0x2800 字节(10240 字节),因此它能够表示的部分地址空间大小为 0x28000000 字节(0x2800 * 0x10000),也就是从 0x78000000 到 0x50000000 这一部分。当每个 DLL 加载时,根据被映射 DLL 的大小,通过在 _MiImageBitMap 中,设置相应数量的连续比特位,来记录该 DLL 在这一部分的位置。下面的伪代码演示了这一过程:

#define  X64K  0x10000
#define  PAGE_SIZE  4096
usPageCountdiv = (USHORT) ( (ImageInfo->dwPageCount1 + 0x0F) / 16 );
dwHighVA = _MiImageBitMapHighVa;                //dwHighVA:0x78000000
if ( (ImageInfo->ExportedImageInformation.ImageCharacteristics & IMAGE_FILE_DLL) == 0  ||
        (dwStartIndex = RtlFindClearBits( _MiImageBitMap, usPageCountdiv16,  _MiImageBias)) == -1 )
{
                RelocateExe: .....            //前面的重定位 EXE 代码块,为求简洁,这里将其省略了
}
/* 下面是重定位 DLL 的代码块。假设要重定位的映像文件是 DLL,
调用 RtlFindClearBitsAndSet() 前,需要先通过 KeAcquireQueuedSpinLock() 取得“排队的自旋锁”
bLevel:存储 KeAcquireQueuedSpinLock() 例程的返回值,它可以作为 KeReleaseQueuedSpinLock() 例程的第二个参数  */

bLevel = KeAcquireQueuedSpinLock(2);
/* 当每个 DLL 加载时,RtlFindClearBitsAndSet() 根据被映射 DLL 的大小,以及先前由 RtlFindClearBits() 返回的随机空闲比特位(第三个参数 dwStartIndex),在 _MiImageBitMap 中,将相应数量(第二个参数 usPageCountdiv16)的连续比特位置 1 ,来记录该 DLL 在 _MiImageBitMap 中的索引,然后再次保存到 dwStartIndex 中  */
dwStartIndex = RtlFindClearBitsAndSet( _MiImageBitMap, usPageCountdiv16, dwStartIndex);
//调用 RtlFindClearBitsAndSet() 后,需要通过 KeReleaseQueuedSpinLock() 释放“排队的自旋锁”
KeReleaseQueuedSpinLock(2, bLevel);
/* 尽管重定位的映像文件是 DLL,如果 RtlFindClearBitsAndSet() 返回 -1,也跳转到重定位 EXE 文件的代码块。因此,仅当要重定位的映像文件是 DLL,并且 RtlFindClearBits() 与 RtlFindClearBitsAndSet() 的返回值都不是 -1 时,才实际对 DLL 进行重定位。 */
if ( dwStartIndex == -1 )
{
                goto  RelocateExe;
}
dwEndImage = dwStartIndex + usPageCountdiv16;
/* dwStartIndex 保存了该 DLL 在 _MiImageBitMap 中的索引,现在,需要将它实际转换为 0x78000000~0x50000000 中的某个随机加载的起始和结束区域:将这个起始索引加上为该 DLL 分配的 16 个页面“批数”,然后乘以 0x10000(“单批”16 个页面的大小,也就是 16 * PAGE_SIZE 的结果);用 0x78000000 减去这个值,然后保存到 dwImageBase 中;
最后检查 dwImageBase 的值,如果恰好与原始 DLL 的 PE 文件可选头中的 ImageBase 值相等,则需要再次随机化 */

if ( (dwImageBase = (dwHighVA - ((dwStartIndex + usPageCountdiv16) * 16 * PAGE_SIZE))) == ImageInfo->dwImageBase )
{
                bLevel = KeAcquireQueuedSpinLock(2);
                /* dwEndImage 等于 dwStartIndex 加上 usPageCountdiv16,在二次随机化时,它被当作为起始的随机空闲比特位,并且在 _MiImageBitmap 中查找,设置连续 usPageCountdiv16 个比特位 */
                if ( (dwNewStartIndex = RtlFindClearBitsAndSet( _MiImageBitmap, usPageCountdiv16, dwEndImage)) )
                {
                                RtlClearBits(_MiImageBitmap, dwStartIndex, usPageCountdiv16);
               
                }
                //现在 dwStartIndex 保存 _MiImageBitmap 中,新分配(置 1)的起始比特位
                dwStartIndex = dwNewStartIndex;
                KeReleaseQueuedSpinLock(2, bLevel);
                //再次计算加载基址
                dwImageBase = dwHighVA - ( (dwStartIndex + usPageCountdiv16) * PAGE_SIZE * 16 );
}
outputInfo->dwOffset1C = dwStartIndex;
outputInfo->usOffset20 = usPageCountdiv16;
ImageInfo->pArea->dwOffset34 = dwStartIndex;
ImageInfo->pArea->usOffset38 = usPageCountdiv16;
//如果 dwImageBase 与该 DLL 的 PE 文件可选头中的 ImageBase 值不同,随机化成功,直接返回新的加载地址
return dwImageBase;

A couple of things are worth explaining here. Firstly, the MiImageBias value is an 8-bit value selected earlier in the boot process. Like the executable positioning code showed previously, the time stamp counter is utilized (specifically, the low byte returned from the rdtsc instruction). Essentially, this MiImageBias value is used as a random offset from the beginning of the _MiImageBitMap bitmap, where space for the DLL being loaded will be searched for. In effect, this means that the first DLL loaded into the address space will end at
MiImageBias (remember, _MiImageBitMap starts from high memory and extends towards 0, so it is backwards), and additional DLLs will be placed one after the other following that one. (The ordering of the DLLs will depend somewhat on their size.) This behaviour is only exhibited for DLLs being rebased - ie, ones that are participating in ASLR or ones that cannot load at their preferred base and are deemed to be relocatable.

这里需要解释几件事情。首先,MiImageBias 存储一个 8 位的值,该值在先前的系统引导阶段随机选取。就像前面的可执行文件定位代码(RelocateExe 代码块)一样,使用到了时间戳计数器(具体而言,就是 rdtsc 指令返回的低位字节)。这是通过多数处理器都原生支持的 rdtsc 指令——读取 TSC——实现的。
本质上讲,这个 MiImageBias 值被用作从 _MiImageBitMap 位图顶端(0x78000000)开始的随机偏移量,从这个偏移量开始向下搜索要被加载的 DLL 基址。事实上,这意味着首个加载
进地址空间的 DLL 将结束于 MiImageBias(记住, _MiImageBitMap 从内存高址开始,向下扩展到 0 ),后续的 DLL 将一个接一个地放置(这些 DLL 的加载顺序在一定程序上取决于它们各自的大小)。仅在 DLL 被重定位时(参与了 ASLR 机制,或者无法被加载到其 PE 文件可选头中
首选加载基址的,都被认为是可重定位的),才会表现出这种行为。

上面的内嵌译文是针对 Windows 7 / Vista 的 ASLR 的。Windows 8 的 ALSR 进一步完善了许多方面,请参考这篇文章:http://bbs.pediy.com/showthread.php?t=208278

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功