一、x64 地址空间布局
1.1 64 位虚拟地址真的有 16EB 吗?
理论上:
cpp
64 位地址 → 2^64 = 16 EB
但这是"理论值"。
实际上,AMD 和 Intel 的 x64 处理器:
只实现了 48 位有效虚拟地址
也就是说:
cpp
2^48 = 256 TB
这 256TB 叫做:
Canonical Virtual Address Space
1.2 Canonical Address 规则
处理器要求:
cpp
第 48--63 位必须等于第 47 位
也就是:
-
若 bit47 = 0 → 高 16 位全 0
-
若 bit47 = 1 → 高 16 位全 1
因此 256TB 被划分成两个 128TB 区域:
cpp
低半区:
0000 0000 0000 0000
↓
0000 7FFF FFFF FFFF (128TB)
高半区:
FFFF 8000 0000 0000
↓
FFFF FFFF FFFF FFFF (128TB)
中间是非法空洞。
1.3 Windows 7 x64 实际使用多少?
地址空间限制为 16TB
其中 8TB 给用户,8TB 给内核
也就是说:
cpp
Windows 实际只使用 44 位
2^44 = 16 TB
二、VAD ------ Windows 进程虚拟地址空间的真正管理者
在 win7 x64 下,每个进程拥有:
cpp
8TB 用户空间
问题来了:
-
Windows 如何知道哪些区域已分配?
-
哪些是 Private?
-
哪些是 Section 映射?
-
哪些是只读?
-
哪些支持写拷贝?
-
哪些已提交(Commit)?
答案是:
VAD 树。
2.1 什么是 VAD?
VAD 全称:
cpp
Virtual Address Descriptor
本质:
描述一段连续虚拟地址区间的内核数据结构
换句话说:
VAD 用来描述进程地址空间中一段连续的虚拟地址区间,以及该区间的类型、保护、提交和映射信息。
2.2 VAD 存在哪里?
我们可以使用windbg,!vad命令查看特定进程的VAD。首先使用!process命令找到VAD树的根地址,随后将改地址提供给!vad命令。
cpp
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
...
PROCESS fffffa8003d51060
SessionId: 1 Cid: 0378 Peb: 7fffffd4000 ParentCid: 0b04
DirBase: 31801000 ObjectTable: fffff8a0079601e0 HandleCount: 12.
Image: Project1.exe
...
kd> dt _eprocess fffffa8003d51060
nt!_EPROCESS
...
+0x448 VadRoot : _MM_AVL_TABLE
...
cpp
kd> !vad fffffa8003d51060+0x448
VAD Level Start End Commit
fffffa8003d514a8 0 0 0 5381 Mapped NO_ACCESS
fffffa8002bdfd60 5 10 1f 0 Mapped READWRITE Pagefile section, shared commit 0x10
fffffa8003dac7e0 4 20 2f 0 Mapped READWRITE Pagefile section, shared commit 0x10
fffffa8003c5ff80 5 30 33 0 Mapped READONLY Pagefile section, shared commit 0x4
fffffa8003192920 3 40 40 0 Mapped READONLY Pagefile section, shared commit 0x1
fffffa8003e00160 4 50 50 1 Private READWRITE
fffffa8002dee160 2 b0 1af 6 Private READWRITE
fffffa8002bfc110 5 1b0 216 0 Mapped READONLY \Windows\System32\locale.nls
fffffa8003c9a2c0 4 3a0 49f 32 Private READWRITE
fffffa8001c54aa0 5 779a0 77abe 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\kernel32.dll
fffffa8002009190 3 77bc0 77d5e 15 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
fffffa8001b8bbe0 5 7efe0 7f0df 0 Mapped READONLY Pagefile section, shared commit 0x5
fffffa8003cf1dd0 4 7f0e0 7ffdf 0 Private READONLY
fffffa8001be81b0 1 7ffe0 7ffef 2251799813685247 Private READONLY
fffffa8003eb1510 5 13f6d0 13f94f 176 Mapped Exe EXECUTE_WRITECOPY \Users\abc\Desktop\Project1.exe
fffffa8003e12010 4 7fef9f40 7fef9f42 0 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\api-ms-win-core-synch-l1-2-0.dll
fffffa8001a915e0 5 7fefd980 7fefd9d6 5 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apphelp.dll
fffffa8003c29e40 3 7fefde50 7fefdeb9 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\KernelBase.dll
fffffa8003b325f0 4 7feffec0 7feffec0 0 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apisetschema.dll
fffffa8002d26be0 2 7fffffa0 7fffffd2 0 Mapped READONLY Pagefile section, shared commit 0x33
fffffa8003526e70 3 7fffffd4 7fffffd4 1 Private READWRITE
fffffa8003b5af70 4 7fffffde 7fffffdf 2 Private READWRITE
Total VADs: 22, average level: 4, maximum depth: 5
Total private commit: 0x80000000015f9 pages (9007199254763492 KB)
Total shared commit: 0x5d pages (372 KB)
2.3 MM_AVL_TABLE
在 Win7 x64 中,EPROCESS.VadRoot 并不是一个 _MMVAD* 指针,而是一个:
cpp
_MM_AVL_TABLE
定义如下:
cpp
//0x40 bytes (sizeof)
struct _MM_AVL_TABLE
{
struct _MMADDRESS_NODE BalancedRoot; //0x0
ULONGLONG DepthOfTree:5; //0x28
ULONGLONG Unused:3; //0x28
ULONGLONG NumberGenericTableElements:56; //0x28
VOID* NodeHint; //0x30
VOID* NodeFreeHint; //0x38
};
1️⃣BalancedRoot
cpp
struct _MMADDRESS_NODE BalancedRoot
cpp
//0x28 bytes (sizeof)
struct _MMADDRESS_NODE
{
union
{
LONGLONG Balance:2; //0x0
struct _MMADDRESS_NODE* Parent; //0x0
} u1; //0x0
struct _MMADDRESS_NODE* LeftChild; //0x8
struct _MMADDRESS_NODE* RightChild; //0x10
ULONGLONG StartingVpn; //0x18
ULONGLONG EndingVpn; //0x20
};
cpp
_MMVAD_SHORT / _MMVAD / _MMVAD_LONG 的前半部分布局
与 _MMADDRESS_NODE 完全兼容
也就是说:
cpp
struct _MMVAD
{
// 前 0x28 字节与 _MMADDRESS_NODE 一致
}
所以 !vad eprocess+0x448 仍然可以工作
2️⃣ DepthOfTree : 3
树的深度(0~31)。AVL 树高度一般很小,所以 5 bit 足够。
3️⃣ NumberGenericTableElements : 56
树中节点数量(这里就是 VAD 数量,或者说 address node 数量)。
4️⃣ NodeHint / NodeFreeHint
这两个是 查找/插入的缓存提示指针:
-
NodeHint:上次查找/插入附近的节点,用于加速下一次查找(比如按地址顺序分配时,很多操作都发生在相邻区域) -
NodeFreeHint:与"空闲区搜索/分配"相关的提示(不同版本用途略有差异)
这属于性能优化字段,不影响树的逻辑正确性。
2.4 _MMVAD、_MMVAD_SHORT、_MMVAD_LONG 结构解析
在 Windows 7 x64中,VAD 节点并非只有一种结构。
内核根据"该虚拟地址区间是否需要附加元数据",使用两种不同大小的结构:
-
_MMVAD_SHORT
-
_MMVAD_LONG
而 _MMVAD 是两者的公共头部。
cpp
//0x78 bytes (sizeof)
struct _MMVAD
{
union
{
LONGLONG Balance:2; //0x0
struct _MMVAD* Parent; //0x0
} u1; //0x0
struct _MMVAD* LeftChild; //0x8
struct _MMVAD* RightChild; //0x10
ULONGLONG StartingVpn; //0x18
ULONGLONG EndingVpn; //0x20
union
{
ULONGLONG LongFlags; //0x28
struct _MMVAD_FLAGS VadFlags; //0x28
} u; //0x28
struct _EX_PUSH_LOCK PushLock; //0x30
union
{
ULONGLONG LongFlags3; //0x38
struct _MMVAD_FLAGS3 VadFlags3; //0x38
} u5; //0x38
union
{
ULONG LongFlags2; //0x40
struct _MMVAD_FLAGS2 VadFlags2; //0x40
} u2; //0x40
union
{
struct _SUBSECTION* Subsection; //0x48
struct _MSUBSECTION* MappedSubsection; //0x48
};
struct _MMPTE* FirstPrototypePte; //0x50
struct _MMPTE* LastContiguousPte; //0x58
struct _LIST_ENTRY ViewLinks; //0x60
struct _EPROCESS* VadsProcess; //0x70
};
//0x90 bytes (sizeof)
struct _MMVAD_LONG
{
union
{
LONGLONG Balance:2; //0x0
struct _MMVAD* Parent; //0x0
} u1; //0x0
struct _MMVAD* LeftChild; //0x8
struct _MMVAD* RightChild; //0x10
ULONGLONG StartingVpn; //0x18
ULONGLONG EndingVpn; //0x20
union
{
ULONGLONG LongFlags; //0x28
struct _MMVAD_FLAGS VadFlags; //0x28
} u; //0x28
struct _EX_PUSH_LOCK PushLock; //0x30
union
{
ULONGLONG LongFlags3; //0x38
struct _MMVAD_FLAGS3 VadFlags3; //0x38
} u5; //0x38
union
{
ULONG LongFlags2; //0x40
struct _MMVAD_FLAGS2 VadFlags2; //0x40
} u2; //0x40
struct _SUBSECTION* Subsection; //0x48
struct _MMPTE* FirstPrototypePte; //0x50
struct _MMPTE* LastContiguousPte; //0x58
struct _LIST_ENTRY ViewLinks; //0x60
struct _EPROCESS* VadsProcess; //0x70
union
{
struct _LIST_ENTRY List; //0x78
struct _MMADDRESS_LIST Secured; //0x78
} u3; //0x78
union
{
struct _MMBANKED_SECTION* Banked; //0x88
struct _MMEXTEND_INFO* ExtendedInfo; //0x88
} u4; //0x88
};
//0x40 bytes (sizeof)
struct _MMVAD_SHORT
{
union
{
LONGLONG Balance:2; //0x0
struct _MMVAD* Parent; //0x0
} u1; //0x0
struct _MMVAD* LeftChild; //0x8
struct _MMVAD* RightChild; //0x10
ULONGLONG StartingVpn; //0x18
ULONGLONG EndingVpn; //0x20
union
{
ULONGLONG LongFlags; //0x28
struct _MMVAD_FLAGS VadFlags; //0x28
} u; //0x28
struct _EX_PUSH_LOCK PushLock; //0x30
union
{
ULONGLONG LongFlags3; //0x38
struct _MMVAD_FLAGS3 VadFlags3; //0x38
} u5; //0x38
};
1️⃣ 关键标志:如何判断一个 VAD 是 LONG还是SHORT?
看 VadFlags2的LongVad标志位:
cpp
ULONG LongVad:1;
如果 VadFlags2.LongVad = 1
该节点实际结构应解释为 _MMVAD_LONG
2️⃣ StartingVpn / EndingVpn 如何计算真实地址?
它们表示:
这条 VAD 覆盖的虚拟页号区间(Virtual Page Number)
假设:
cpp
StartingVpn = 0x2770
EndingVpn = 0x27EF
真实虚拟地址区间为:
cpp
StartVA = 0x2770 << 12 = 0x2770000
EndVA = (0x27EF << 12) + 0xFFF
2.5 _MMVAD_FLAGS 结构解析
在 Win7 x64 中,VAD 节点的核心属性字段位于:
cpp
union
{
ULONGLONG LongFlags; // 0x28
_MMVAD_FLAGS VadFlags;
} u;
其定义如下:
cpp
// 8 bytes
struct _MMVAD_FLAGS
{
ULONGLONG CommitCharge:51;
ULONGLONG NoChange:1;
ULONGLONG VadType:3;
ULONGLONG MemCommit:1;
ULONGLONG Protection:5;
ULONGLONG Spare:2;
ULONGLONG PrivateMemory:1;
};
总共:
cpp
51 + 1 + 3 + 1 + 5 + 2 + 1 = 64 bits
刚好8字节
1️⃣ CommitCharge(51 位)
含义:
该 VAD 区间占用的"已提交页数"
单位:
cpp
页(4KB)
例如:
cpp
CommitCharge = 32
表示:
cpp
32 页 = 128KB 已提交内存
2️⃣ NoChange(1 位)
表示:
该区域的保护属性是否允许修改
如果:
NoChange = 1VirtualProtect 无法更改保护属性
3️⃣ VadType(3 位)
这 3 位字段用于标识当前 VAD 节点的"内存类型"。
在 Win7 x64 中,其枚举定义为:
cpp
typedef enum _MI_VAD_TYPE {
VadNone, // 0
VadDevicePhysicalMemory, // 1
VadImageMap, // 2
VadAwe, // 3
VadWriteWatch, // 4
VadLargePages, // 5
VadRotatePhysical, // 6
VadLargePageSection // 7
} MI_VAD_TYPE, *PMI_VAD_TYPE;
4️⃣ MemCommit(1 位)
表示:
该 VAD 是否已经提交(commit)
区别:
-
CommitCharge 表示页数
-
MemCommit 表示状态标志
5️⃣ Protection(5 位)
_MMVAD_FLAGS.Protection 不是用户态 API 里看到的 PAGE_READWRITE / PAGE_EXECUTE... 那些常量值本身。
-
它是 内核内部的保护码(常叫 MM protection code),用 5 bit(0~31)编码,表示这段 VAD 的"基础页保护属性"。
VAD 记录的是"这段区间应该是什么保护"
-
真正落实到 PTE 里时,PTE 的 NX、RW、U/S 等位会根据这个 Protection 进一步展开(并可能叠加别的机制:CopyOnWrite、NoChange、SecNoChange、Image/Section 等)
6️⃣Spare(2 位)
保留字段。
7️⃣PrivateMemory(1 位)
表示:
是否为 Private VAD
-
1 = 私有内存(VirtualAlloc)
-
0 = Section 映射(文件 / pagefile / image)
这和 !vad 输出的:
cpp
Private / Mapped
直接对应
2.6 _MMVAD_FLAGS2 结构解析
_MMVAD_FLAGS2 位于 _MMVAD/_MMVAD_LONG 的 u2 中:
其定义如下:
cpp
//0x4 bytes (sizeof)
struct _MMVAD_FLAGS2
{
ULONG FileOffset:24; //0x0
ULONG SecNoChange:1; //0x0
ULONG OneSecured:1; //0x0
ULONG MultipleSecured:1; //0x0
ULONG Spare:1; //0x0
ULONG LongVad:1; //0x0
ULONG ExtendableFile:1; //0x0
ULONG Inherit:1; //0x0
ULONG CopyOnWrite:1; //0x0
};
1️⃣FileOffset(24 位)
含义:
FileOffset 主要用于 "文件映射 / section 映射" 相关的 VAD,用来表达"该 VAD 对应的映射视图在文件中的偏移"。
2️⃣ SecNoChange(1 位)
含义:
SecNoChange表示该 VAD 对应的 Section 映射保护属性不允许被更改。
和 _MMVAD_FLAGS.NoChange 的区别:
-
NoChange:更广义,偏"VAD 保护是否可改"
-
SecNoChange:更偏"由 Section 派生出来的保护不可改"
LongVad(1 位)------最关键的分支开关
含义
LongVad 用于决定 该节点的真实结构体类型:
-
LongVad = 0 → 该节点按 _MMVAD_SHORT 解释(只有公共头)
-
LongVad = 1 → 该节点按 _MMVAD_LONG 解释(有额外扩展字段 u3/u4 等)
CopyOnWrite(1 位)
含义:
CopyOnWrite = 1 表示该 VAD 区间具备 写时复制(COW) 语义:
-
读时共享
-
一旦写入触发私有化(产生 private copy)
-
!vad 常显示为 WRITECOPY / EXECUTE_WRITECOPY
典型场景
-
映像映射(EXE/DLL ImageMap)几乎是它的"主场"
-
共享 section 映射且设置了写时复制属性