缘起
我在《调试实战 | 记一次有教益的内存碎片转储文件分析》中分析了一个由于内存碎片导致的崩溃转储。发现一个很“奇怪”的现象——程序是 32
位的,但是在查看堆空间大小的时候,居然有将近 4GB
。
相信各位小伙伴儿应该听过下面这种说法:32
位进程有 4GB
的虚拟内存,其中低 2GB
是用户空间,应用程序可以访问,高 2GB
是内核空间,应用程序不能访问,但是内核可以访问。在系统开启 /3GB
的情况下,用户空间可以提升至 3GB
,内核空间被压缩到 1GB
。
这也是我学到的关于 32
位进程虚拟内存相关的知识,但是上述描述不够准确。比如,开启了 /3GB
,进程用户空间就一定可以提升至 3GB
吗?在 64
位系统上,上述说法还成立吗?
带着上述疑问,我翻看了微软官方文档,本文尽可能全面的总结进程的虚拟内存空间划分。因为能力有限,本文只涉及 x86/x64
平台,不涉及 ARM
平台。
说明: 本文总结的限制主要指的是虚拟内存的限制。物理内存与虚拟内存的关系可以简单想象成
1v1
的关系。如果用户空间代码只能访问2GB
大小的虚拟内存,那么最多访问2GB
的物理内存,即使机器上装了16GB
的内存。
32 位程序用户态内存空间的限制
32
位进程用户空间大小受两方面的限制:
- 系统级别的限制:是否开启
4GT
(4 gigabyte tuning),也就是/3GB
开关。 - 进程本身的限制:是否设置
IMAGE_FILE_LARGE_ADDRESS_AWARE
标志。
说明: 只有以上两个条件同时满足,在
32
位操作系统上的应用程序用户态虚拟内存空间才可以达到3GB
。
32 位程序在 32 位系统上的情况
下面的表格总结了各种设置下进程用户空间大小的限制:
虚拟地址范围 | 未开启 IMAGE_FILE_LARGE_ADDRESS_AWARE |
开启 IMAGE_FILE_LARGE_ADDRESS_AWARE |
---|---|---|
未开启 4GT |
低 2GB (0x00000000 ~ 0x7FFFFFFF) | 低 2GB (0x00000000 ~ 0x7FFFFFFF) |
开启 4GT |
低 2GB (0x00000000 ~ 0x7FFFFFFF) | 低 3GB (0x00000000 ~ 0xBFFFFFFF) |
开启 4GT 并设置 USERVA 为 M |
低 2GB (0x00000000 ~ 0x7FFFFFFF) | 低 (0x00000000 ~ M-1) |
说明: 开启
4GT
的情况下,用户空间上限默认是3GB
,但是可以手动指定一个2GB~3GB
之间的值作为上限。
32 位程序在 64 位系统上的情况
32
位程序在 64
位系统上的情况比较单一。如果设置了 IMAGE_FILE_LARGE_ADDRESS_AWARE
标志,那么用户空间大小可以达到 4GB
,否则只能使用低 2GB
。
虚拟地址范围 | 未开启 IMAGE_FILE_LARGE_ADDRESS_AWARE |
开启 IMAGE_FILE_LARGE_ADDRESS_AWARE |
---|---|---|
不论是否开启 4GT |
低 2GB (0x00000000 ~ 0x7FFFFFFF) | 4GB (0x00000000 ~ 0xFFFFFFFF) |
上面两个表格的关键内容是我从微软官方文档中摘录的,关于内存和地址空间的更多限制的可以参考下图(同样截自微软官方文档):
如何开启 4GT
因为 4GT
只对 32
位系统有意义,所以下述设置方法仅针对 32
位系统生效。
xp server2003
在 xp
或者 server2003
上,可以通过修改 Boot.ini
文件来开启 4GT
。
1 | [boot loader] |
倒数第 2
行表示开启 4GT
,并且用户地址空间最大值是默认的 3GB
。
最后一行表示开启 4GT
,并且设置用户地址空间的最大地址值为自定义的 2.5GB
。
Vista 及以上
在 vista
及以上的操作系统上,可以通过 bcdedit
进行设置。我以 win7
为例。
以管理员权限运行 cmd
,输入 bcdedit /set increaseuerva target_user_virtual_address_mb
。
比如,如果想设置最大地址值为 2.5GB
,则可以输入 bcdedit /set increaseuerva 2560
。如果想设置最大地址值为 3GB
,则可以输入 bcdedit /set increaseuerva 3072
。
如果不想设置了,可以输入 bcdedit /deletevalue increaseuserva
。
说明:以上设置需要管理员权限,并且重启后生效
介绍完调整系统设置的方法后,我们看看如何设置应用程序。
如何设置 IMAGE_FILE_LARGE_ADDRESS_AWARE
如果有源码,可以直接调整 vs
的工程设置。
在工程文件上右键,属性,打开工程属性设置对话框。找到 Configuration Property -> Linker -> System
选项,设置 Enable Large Addresess
的值为 Yes
。如下图:
当然,也可以直接在 Linker
下的 Command Line
中手动加上 /LARGEADDRESSAWARE
选项。效果是一样的。
通过以上设置,可以为新编译出来的程序设置 Large Address Aware
标志。
如果没有源码,或者程序已经生成了,该如何修改呢?直接修改 PE
文件中对应的标志位即可。下图是我使用 CFF Explorer
修改应用程序的 Large Address Aware
标志的截图。
验证
为了加深自己的印象,也为了验证参考资料的准确性,我特意写了一个非常简单的程序,该程序可以获取到用户态内存最高地址。代码如下:
1 |
|
相关的测试工程已经上传到了 gitee,有需要的小伙伴儿可以自行下载。
验证 32
位程序在 32
位系统下的运行效果
第一张图是未开启 4GT
的运行结果:
第二张图是开启 4GT
并且使用默认值(3GB
)的的运行结果:
第三张图是开启 4GT
并且指定 /USERVA
大小为 2.5GB
的运行结果:
验证 32
位程序在 64
位系统下的运行效果
我直接在我的 win10
系统中运行了,结果如下:
从以上输出结果可知,微软的文档没有骗我们。
PAE
除了 /3GB
,各位小伙伴儿可能还听过一个叫 PAE
的东东,全称是 Physical Address Extension,直译过来就是物理地址扩展。从名字可以看出是跟物理地址有关的。
我认为 PAE
解决的最重要的一个问题是,让 32
位操作系统可以使用更多的物理内存。假设,一台装有 16GB
的物理内存的机器上,装了 32
位的 windows
操作系统,在没开启 PAE
情况下,只能使用低 4GB
的物理内存,无法使用高 12GB
的物理内存。如果开启了 PAE
,那么这 16GB
都可以使用。
我目前理解这个选项是通过控制分页模式来达到访问更多物理内存的效果的。如果不开启 PAE
,那么将使用 10-10-12
分页,开启 PAE
后,会使用 2-9-9-12
分页。关于分页模式,强烈推荐大家听一听海哥的内核课程。
开启或关闭 PAE
在 xp
或者 server2003
上,可以通过修改 Boot.ini
文件来开启或关闭 PAE
。
1 | ; 开启 PAE |
在 vista
及以上的操作系统上,可以通过 bcdedit /set
命令来开启或关闭 PAE
。
1 | ; 开启 PAE |
总结
应用程序开启了 Large Address Aware
标志后可以访问更大的虚拟内存空间。如果可能,一定要开启!
可以在 vs
工程中通过 /LargeAddressAware
选项开启,也可以直接修改 PE
文件中对应的标志位进行修改。
参考资料
- https://docs.microsoft.com/en-us/windows/win32/memory/virtual-address-space
- https://docs.microsoft.com/en-us/windows/win32/memory/4-gigabyte-tuning
- https://docs.microsoft.com/en-us/windows/win32/memory/physical-address-extension
- https://docs.microsoft.com/en-us/windows/win32/memory/memory-limits-for-windows-releases
- https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/bcdedit--set
- https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/boot-parameters-to-configure-dep-and-pae