赞
踩
在一个C\C++程序中,变量的储存位置可以分为以下几类:
Malloc
或者New
运算符申请的一块连续的内存空间,该空间需要程序编写者自己进行申请或者回收;第三种申请到的内存空间就属于堆分配的内存空间,通过堆分配的内存空间和全局变量、局部变量有较大的不同,主要体现在以下几点:
对于操作系统来说,某个时刻(一般是进程被创建时),操作系统收到了一个请求大量内存块的请求,收到请求后,操作系统的内存管理器将一块大的内存块返回给这个进程,确切的说,提出请求的是进程中的堆管理设施,也是堆管理设施对这一大块内存进行管理。而对于用户来说,当某一时刻申请一块内存空间时,实际上是向对管理器申请的,堆管理器会从他说管理的内存中划分出一小块分给用户。
从这两个角度来说,堆管理器更像一个批发商:从操作系统批发一大块内存存放在自己的仓库,并面向用户售卖这一大块内存。
另外对应Malloc
和New
的堆管理器叫做CRT堆,该堆有三种工作模式,其中一个工作模式是对Win32堆的简单封装,并且大部分情况下CRT堆也工作在这种模式中。1
这里说一下在Windbg中常用的三种命令2:
.
符号开头;!
开头除此之外,在Windbg中关于堆的扩展指令可以在MSDN中找到,传送门。
我们首先要找到计算器的位置,这里推荐一下微软自带的更加详细的任务管理器,可以看到每个进程加载好的DLL和对应的文件目录地址,名字叫做procexp,对应的传送门,安装好后有两个,64位和32位的区别主要是是否显示WOW64(Windows-on-Windows 64-bit)对应的转接层DLL,这里影响不大。
先打开计算器进程,并打开procexp进程监视器,即可找到对应的进程的硬盘位置:
由于Windbg是面向高手的,从另外一个角度来说也就是门槛比较高,其中一个门槛就是Windwos的符号加载,不同于Ollydbg,Windbg可以显示许多和Windows实现细节有关的数据结构,这就需要Windows编译时所生成的符号表,而Windows的版本千变万化,这时就需要从微软的官方符号服务器中下载对应的符号表以显示Windows的相关运行时需要的数据结构。而Windbg的符号加载是手动的,也就是由用户选择是否加载某个模块的符号。在本实验中查看PEB结构和堆结构是需要加载符号的,否则会出现如下错误:
通过如下途径设置符号服务器地址:
在菜单中选择File->Symbol File Path,打开如下对话框:
填入如下指令:
srv*c:\\symbol*https://msdl.microsoft.com/download/symbols
其中c:\symbol为本机缓存的地址。需要注意的是,由于符号服务器被墙了,需要开一下代理,否则无法下载对应的符号,也就没法实验了。
上述的指令也可以在命令行中输入.symfix
来实现。如果在符号设置过程中发生问题,可以通过!sym noisy
指令来打开Debug模块看到具体哪里出现了问题。
通过扩展指令!peb
指令即可观察到一个进程的环境块。
可以看到,PEB的环境块极多,由于微软并没有开源Windows系统,所以PEB块的各个部分并没有官方解释,但是网络上已经有很多关于PEB块的解释了。
堆的详细结构及其实现和面向用户提供的模型可以在《软件调试》的23章中进行查看,简单来说,一个堆的基本单位是堆块,多个堆块组成段,段组成堆。
通过!heap
指令可以展示一个进程所拥有的堆:
其中的Segment Heap
是在微软2020年5月新更新的堆设施3,而NT Heap
则是传统的堆结构。
使用dt ntdll!_HEAP address
指令来观察一个堆的数据结构
其实第一次看到这个结构还是挺惊讶的,因为之前在书上看到的堆数据结构并没有这么复杂,而且传统的Segments数组没了,这就很迷了,后来查到好像是在20205月份的时候Windows进行了一波大更新…
通过!heap -hf
指令可以看到进程的默认堆所拥有的所有段
通过dt ntdll!_HEAP_SEGMENT Address
指令确实看到了段的对应的数据结构和魔数。
但是这里有一个小疑问就是在以前的结构中段结构的头部是通过HEAP结构中的Segments字段找到的,扩展指令!haep -hf
就是以比较友好的形式将段解释了出来,但是在新的Windows中似乎Segment的记录结构完全找不见了,即使在+-0x10000
的范围内搜索段起始地址还是找不到对应的踪迹。
但是在Windbg的扩展指令中还是能使用相应的指令,所以肯定是新版本的Windows将这个结构搬到了某个地方,希望某位大佬能够解释一下。
HEAP_ENTRY
结构尽管在新版本中堆的管理结构变化比较大,但幸好无论是_HEAP
结构还是_HEAP_SEGMENT
结构都是放在一个堆块里面的
而一个堆块的管理结构就是HEAP_ENTRY
,找到对应的地址,就可以查看对应的HEAP_ENTRY
结构了。
先说一点题外话就是我在尝试进行堆分配观察的时候花费了很长时间想观察X64结构的堆,并尝试熟悉Windbg工具,但是最后发现效果并不好,由于时间关系,现在暂时先使用Windbg(x86)+Ollydbg进行观察,但是以后还是要有一套能调试x64的工具的。
首先附上源代码
#include <windows.h> #include <stdio.h> #include<intrin.h> typedef unsigned __int32 ptr32; constexpr unsigned int FreeLists_Index = 0xc0; //该偏移随版本不同而不同!!! void Show_FreeLists(ptr32 HeapHandle) { ptr32 Entry = (HeapHandle + FreeLists_Index); printf("%08xh", Entry); ptr32 NowNode = *(ptr32*)Entry; while (NowNode!=Entry) { printf("->%08xh", NowNode); NowNode = *(ptr32*)NowNode; } printf("\n"); } int main() { HLOCAL h1, h2, h3, h4, h5, h6,h7; HANDLE hp = NULL; hp = HeapCreate(0, 0x1000, 0x1000); Show_FreeLists((ptr32)hp); h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h4 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h5 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); h6 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); HeapFree(hp, 0, h1); Show_FreeLists((ptr32)hp); HeapFree(hp, 0, h3); Show_FreeLists((ptr32)hp); HeapFree(hp, 0, h5); Show_FreeLists((ptr32)hp); h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); Show_FreeLists((ptr32)hp); printf("Finish!"); return 0; } }
对应的Win32堆的API可以在网上百度到,这里就不在赘述了。
另外还有一点就是在原文4中提到的设置也不需要进行设置,直接使用就好了。还有就是要注意到的一点是我在调试过程中发现堆结构及其管理结构、方法是和操作系统密切相关的,这也导致了以下的调试信息全部为在Win10 20H0版本下的结果。
首先在对应的HeapCreate
函数下断点,找到对应的堆结构的首部(函数的返回地址就是堆结构的首部,如我这里的:0x11F0000)
接下来就可以在内存窗口里面查看到对应的堆结构了,
而堆的管理数据结构和Windows版本密切相关,所以需要在Windbg上下载对应的符号表,进行查看
如我这里的空闲链表对应的就是偏移为0xc0的位置。在内存窗口中找到位置:
此时该双向链表入口的前向节点在初始化时永远指向一个从未被分配过的地址(在Windows中如果一个堆的内存空间没有被用户所占有,那么内存被填充为0xfeeefeee,因为和Free单词很像)。
当使用HeapAlloc
函数分配了一次堆块后,FreeLists的指针变为
其含义是一个新的从未被分配过的地址块(在刚才地址块的下面一点)。
接下来源码中的四次分配也是如此。
比较有趣的是使用了HeapFree
函数后,此时的FreeLists
字段变成了如下值:
其中的前向节点指向刚刚释放掉的空间
而后向节点指向一个从未被申请过的空间
而从最后一个HeapAlloc
的函数申请中可以发现,即使申请的空间小于以释放的堆块,堆管理器仍然从一个从未被访问过的空间中切割一个堆块给他。
具体示意图如下:
当没有堆块释放时的双向链表:
分配时的链表变化情况
而当有堆块被释放时,假设释放的节点大小相等,不连续(也就是说释放的两个节点之间还有未被释放的节点),并且释放顺序为1->2->3
链表的链接情况如下所示:
而当释放节点的时候,如果释放的节点的左右节点是已被释放的节点,那么堆管理器会将空闲链表节点进行释放。如下图所示:
对应的示例代码为:
HeapFree(hp, 0, h1);
Show_FreeLists((ptr32)hp);
HeapFree(hp, 0, h3);
Show_FreeLists((ptr32)hp);
HeapFree(hp, 0, h2);
Show_FreeLists((ptr32)hp);
HeapFree(hp, 0, h5);
Show_FreeLists((ptr32)hp);
h7 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 9);
Show_FreeLists((ptr32)hp);
另外需要注意的是空闲链表中的释放过程并没有分配。
通过实验,现在总结一下堆的分配和释放的一些小小体会:
_HEAP
结构中会保留一个FreeLists
字段,该字段用于指出空闲链表的头节点;SafeUnLink
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。