.NET Core内存结构体系(Windows环境)底层原理解析
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">物理内存与虚拟内存物理内存</a></li><li><a href="#_label1">物理页4K对齐</a></li><li><a href="#_label2">物理内存与虚拟内存如何映射?</a></li><li><a href="#_label3">Reserved与Commit</a></li><li><a href="#_label4">NT堆</a></li></ul></div><p class="maodian"><a name="_label0"></a></p><h2>物理内存与虚拟内存物理内存</h2><ul><li>物理内存(Physical Memory)<br />定义:物理内存是计算机硬件中的实际RAM(如DDR5内存条),直接通过总线与CPU连接,用于临时存储运行中的程序和数据。</li><li>虚拟内存(Virtual Memory)<br />定义:由操作系统管理的抽象内存层,通过结合物理内存和磁盘空间(如页面文件或交换分区),为程序提供连续且独立的内存空间。</li></ul>
<p><code>用户只需要与虚拟内存地址打交道,而无需关心数据到底分配在哪里</code></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848231.png" /></p>
<p>眼见为实</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848232.png" /></p>
<p class="maodian"><a name="_label1"></a></p><h2>物理页4K对齐</h2>
<p>在Windows系统下,以4K为最小粒度,这个单位叫做<code>物理页</code>,并以4K的整数倍分配内存。比如申请1k分配4k,申请5k分配8k</p>
<p>眼见为实</p>
<div class="jb51code"><pre class="brush:csharp;">void page4k() {
for (int i = 0; i < 200; i++) {
//1k 的占用
LPVOID ptr = VirtualAlloc(NULL, 1024 * 1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
printf("i=%d, 1k, address:%#0.8x \n", i + 1, ptr);
}
for (int i = 200; i < 400; i++) {
//5k 的占用
LPVOID ptr = VirtualAlloc(NULL, 1024 * 5, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
printf("i=%d, 5k, address:%#0.8x \n", i + 1, ptr);
}
getchar();
}</pre></div>
<p>申请1k分配4k</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848233.png" /></p>
<p>申请5k分配8k</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848234.png" /></p>
<p class="maodian"><a name="_label2"></a></p><h2>物理内存与虚拟内存如何映射?</h2>
<p>Windows系统采用<code>二叉树结构</code>(5层)来实现高效映射。</p>
<p>举个例子,某个32bit的内存地址为:0x77b01a42,其二进制为:01110,11110,11000,00001,101001000010</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848235.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848236.png" /></p>
<ul><li><code>前20位</code>用来构建<code>页表树</code>,实现物理页的的高效映射</li><li><code>后12位</code>映射物理页的偏移量</li></ul>
<p>操作系统以4K为一个单位对内存进行分组,4G内存=102410241024*4/(4/1024)=1048576物理页,如此庞大的物理页,,采用5层二叉树来提高索引效率</p>
<p>眼见为实:以notepad为例</p>
<p>任务管理:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848237.png" /></p>
<p>Windbg:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848238.png" /></p>
<p>可以看到非常明显的不同,任务管理器显示占用44.6mb内存,而windbg显示占用489.531mb内存,这是为什么呢?答:显示逻辑不同,任务管理器显示的是Private WorkingSet,指的是物理内存的地址,即<code>内存条上的内存</code>,而Windbg是显示映射到的物理页,Commit指的是虚拟内存地址,这包括<code>内存条上的内存,pagefile,image</code>三种</p>
<p>眼见为实:可视化观察 虚拟地址=>物理地址</p>
<p>使用windbg进入内核态,这很重要,大家可以猜猜原因。</p>
<p>随便找一个字符串的内存地址</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/202502120848239.png" /></p>
<ul><li>使用dp观察虚拟地址</li><li>使用!vtop 观察映射信息</li><li>使用!db观察物理地址</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482310.png" /></p>
<p>眼见为实:空指针区与用户态区</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482311.png" /></p>
<p>windows/linux在默认情况下,会开启ASLR,需要关闭此技术才能复现。ASLR 是一种针对缓冲区溢出攻击等内存攻击技术而设计的安全特性。在没有 ASLR 的情况下,程序加载到内存中的位置通常是固定的,攻击者可以预测程序中各种模块(如可执行文件、动态链接库等)的加载地址,进而利用这些固定地址来构造恶意代码进行攻击,比如在缓冲区溢出攻击中精准定位跳转地址来执行恶意指令。而启用 ASLR 后,操作系统在每次启动程序时会随机化程序的内存布局,包括可执行文件、动态链接库、堆、栈等的加载地址,使得攻击者难以准确预测内存地址,大大增加了攻击的难度。</p>
<p class="maodian"><a name="_label3"></a></p><h2>Reserved与Commit</h2>
<ul><li>Reserved<br />在虚拟地址上申请一段内存空间,此时操作系统也会同步创建<code>页表树</code>,但此时并<code>未映射到物理内存</code>,此时对该虚拟内存的读写会抛异常</li><li>Commit<br />给<code>页表树</code>调配真实的<code>物理内存</code>,此时才能正常写入</li></ul>
<p>眼见为实:Reserved</p>
<div class="jb51code"><pre class="brush:csharp;">voidmem_reserved() {
LPVOID ptr = VirtualAlloc(NULL, 4 * 1024, MEM_RESERVE, PAGE_READWRITE);
*(int*)(ptr) = 10;//在首地址上写入内容。
printf("num=%d", *(int*)ptr);
}</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482312.png" /></p>
<p>眼见为实:Commit</p>
<div class="jb51code"><pre class="brush:csharp;">voidmem_commit() {
LPVOID ptr = VirtualAlloc(NULL, 4 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
*(int*)(ptr) = 10;//在首地址上写入内容。
printf("num=%d", *(int*)ptr);
}</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482313.png" /></p>
<p class="maodian"><a name="_label4"></a></p><h2>NT堆</h2>
<p>NT堆是 Windows NT 内核引入的内存管理组件,主要负责进程内的堆内存分配与释放。在 Windows 系统里,进程可以使用 NT 堆来动态分配和管理内存,比如程序中使用 malloc()(C 语言)、new(C++) 等函数进行内存分配时,底层通常就依赖 NT 堆机制。</p>
<p>上面说到,VirtualAlloc方法它会一次性分配 64k 整数倍的内存段,内部对象按4k的内存页对齐.如果让application直接操作VirtualAlloc,难免会造成大量的内存浪费。为了提高内存性能与使用效率,Windows又提供了一层<code>抽象</code>,以提供更细颗粒度的内存管理。它的名字叫做<code>NT堆</code></p>
<ul><li>在32bit平台上:8byte为一个分配粒度</li><li>在64bit平台上:16btye为一个分配粒度</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482314.png" /></p>
<ul><li>CRT堆:C运行时使用的堆,默认是对NT堆的简单封装</li><li>托管堆:用作特殊用途的,自行实现的一套内存池管理机制。比如GC堆</li></ul>
<p>从图中可以看出,使用NT与否取决于程序员本身。完全可以绕过NT堆,直接使用VirtualAlloc来分配内存,只要你接收内存浪费。</p>
<p>眼见为实:GC堆,底层使用VirtualAlloc分配内存</p>
<div class="jb51code"><pre class="brush:csharp;">static void Main(string[] args)
{
var rand = new Random();
List<string> list = new List<string>();
for (int i = 0; i < 100000; i++)
{
var str = string.Join(",", Enumerable.Range(0, rand.Next(1, 1000)));
list.Add(str);
Console.WriteLine($"i={i},length={str.Length}");
}
Console.ReadLine();
}</pre></div>
<p>在bp KERNELBASE!VirtualAlloc 下断点</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482315.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482316.png" /></p>
<p>眼见为实:CRT堆/NT堆,底层使用VirtualAlloc分配内存</p>
<div class="jb51code"><pre class="brush:cpp;">
#include <iostream>
#include <Windows.h>
void crt_c() {
for (int i = 0; i < 10000000; i++) {
int* ptr = (int*)malloc(sizeof(int) * 1000);
*(ptr) = 10;
printf("第 %d 次分配 \n", i);
}
}
</pre></div>
<p>在 bp ntdll!NtAllocateVirtualMemory 下断点</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202502/2025021208482317.png" /></p>
頁:
[1]