内存管理之堆
堆的定义#
堆其实就是预定于的一块虚拟地址空间,一个进程在初始化时系统会自动创建一个默认堆,当然进程可以含有多个堆,并且一个堆是可以自动增长的。
堆与虚拟内存的区别#
虚拟内存通过VirtualAlloc函数传入MEM_RESERVE预定一块区域,但此时并不能访问此块区域,因为此块内存地址空间并未调拨物理内存,也就是此块内存区域现在是无效的,访问会出错。
只有在VirtualAlloc函数预定完区域后再次调用VirtualAlloc并传入MEM_COMMIT来为预定的区域调拨物理内存。
堆则是在创建的时候就已经预定并为其调拨物理内存了。
从堆中分配内存的一些细节#
HANDLE hHeap1 = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 1024 * 4, 0);
LPVOID lpByte = HeapAlloc(hHeap1, HEAP_ZERO_MEMORY, 1024);
((LPBYTE)lpByte)[1048] = 1;
LPVOID lpByte1 = HeapAlloc(hHeap1, HEAP_ZERO_MEMORY, 1024);
此代码先创建一个私有堆,然后在此私有堆中通过HeapAlloc()申请内存1024个字节的内存,然后我们利用返回的地址向此地址的后的第1049的字节处写入1,虽然这块内存还没有申请,但是因为我们的堆申请了1024*4个字节所以此处内存是可以访问的。但是我们调试的时候发现当我们在继续HeapAlloc时失败,显示堆被破坏。
我们用OD分析此代码,我们发现其在第一次HeapAlloc分配1024个字节的内存后,第二次调用HeapAlloc分配内存其并没有直接在第一次分配的内存块后分配而是中间隔了24个字节。
因为代码((LPBYTE)lpByte)[1048] = 1;
将第一次分配内存地址后的第1049个字节改写为了1,所以第二次需要分配的内存的首地址处的第一个字节被改写为了1。当第二次调用HeapAlloc分配内存时我们发现其会先判断待分配内存开始两个双字节所指向的内容进行比较。
其会比较第一个双字节所指向的内容,与第二个双字节偏移4所指向的内容是否相等,我们在内存中查看发现其刚好等于带分配内存的首地址。
因为我们改写了第一个字节的内容为1,所以这时比较其就不会相等从而发现堆被破坏。其会调用DbgPrintEx打印Critical error detected c0000374。
如果我们不更改待分配内存的前两个双字节,我们更改第三个字节呢?我们修改代码为'((LPBYTE)lpByte)[1056] = 1'继续用OD调试,我们发现此次其不会因为前两个字节被更改而发现堆被破坏,但是接着发现其会调用ntdll.RtlCompareMemoryUlong函数来检测我们待申请内存中的数据所在的区域的连续0xEFEEEFEE的数量。
因为我们改写了第9个字节为1,所以其连续0xEFEEEFEE的数量为0,所以会调用DbgPrint打印HEAP: Free Heap block XXXX modified at XXXX after it was freed ,表示堆被破坏,当我们申请内存释放后又使用也会产生此种错误。
所以我们知道了在调试状态下为什么带申请的内存都被0xEFEEEFEE填充,目的就是为了方便我们在调试的时候发现堆被破坏的错误。只有在调试状态下待申请的内存才会被填充0xEFEEEFEE,所以有的反调试就是利用这种机制来检测堆中的这种标志达到反调试的目的。
c/c++中堆的使用#
c中使用malloc 从堆中申请内存,c++使用标准的new从堆中申请内存。实际c++中标准的new在底层调用的malloc,而malloc在更底层调用的HeapAlloc来从堆中申请的内存。
HANDLE dwqq = GetProcessHeap();
PVOID pDword = (PVOID)new DWORD[10]();
利用上面代码我们调试看看new在底层调用HeapAlloc是不是进程的默认堆中申请的内存,我们跟踪代码发现new底层调用HeapAlloc是在一个_crtheap的堆中申请的内存,此堆是c运行库创建的堆。也就是说我们c++中标准的new,c中malloc都是在c运行库创建的_crtheap这个堆中申请的内存。
注意:HeapAlloc在进程预留的堆够用的情况下是不调用VirtualAlloc的,但是当堆内存不够用时就会调用VirtualAlloc继续申请并提交内存。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】