Win32堆的调试支持
为了帮助程序员及时发现堆中的问题,堆管理器提供了以下功能来辅助调试。
1:堆尾检查(Heap Tail Check) HTC,在堆尾添加额外的标记信息,用于检测堆块是否溢出。
2:释放检查(Heap Free Check)在释放堆块时进行检查,防止释放同一个堆块。
3:参数检查,对传递给堆的各种参数进行更多的检查。
4:调用时验证(Heap Validate On Call)HVC,每次调用堆函数时都对整个堆进行验证和检查。
5:堆块标记(Heap Tagging)为堆块增加附加标记,以记录堆块的使用情况。
6:用户态栈回溯(User Mode Stack Trace)UST,将每次调用堆函数的函数调用信息记录到一个数据库中。
7:专门用于调试的页堆(Debug Page Heap)DHP堆,页堆比较常用,且需要专门开启,我们会专门对其进行介绍。
创建堆时,堆会根据当前进程的全局标志来决定是否 启用堆的调试功能。操作系统在加载一个进程时会在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Image File Execution Option表键下寻找以该程序命名的子键。如果存在该子键则读取下面的GlobalFlag键值。
可以使用gflags.exe来编辑系统的全局标志或某个文件的全局标志。
如果在调试器运行一个程序,但注册表中并没有设置GlobalFlag键值 ,那么操作系统的加载器会默认将全局标志置为0x70,也就是启用htc、hfc和hpc三项调试功能。
如果注册表中设置了GlobalFlag键值,则使用注册表中的设置,不再默认提供其他调试选项。
如果是附加到一个已经运行的程序上,则它的全局标志就是注册表中的值。如果注册表中不存在全局标志,则为0。
gflags.exe
gflags.exe被称为全局标志编辑器是,windows调试工具集的一部分。该程序是用于对各个全局标志选项的集中式配置工具。它有gui和控制台两种模式。
- GUI模式开启调试选项
运行gflag.exe,打开gui模式:
可以看到程序分为三个标签页:system Registry、Kernel flags 、Image File。
标签页System Registry用于设置针对整个系统的选项。设置之后需要重启系统才能生效。
Image File用于设置针对单个进程的配置。设置之后重启进程后才会生效。
Kernel Flags用于设置只对内核产生影响的选项。
在gflags中包含了针对操作系统的各个方面的配置信息,这些信息是被保存在注册表相应的位置上。
单击Image File标签页,在Image:(Tab to Refresh)编辑框内输入要设置的进程名称。如calc.exe。设置完成后点击Tab进行刷新,刷新后下面的各个控件变为可用状态。在需要设置的调试选项后点勾,确认后即可。
- 控制台方式
在cmd窗口输入一下命令来开启相应的调试功能。
开启堆尾检查:gflags /i calc.exe +htc
开启释放检查:gflags /i calc.exe +hfc
开启调用时验证:gflags /I calc.exe +hvc
开启参数检查: gflags /I calc.exe +hpc
开启用户态栈回溯:gflags /I calc.exe +ust
开启页堆:gflags /p /enable 程序名 /full
或者gflags /I 程序名 +hpa
需要关闭时只需要将+变为-即可。
在windbg中输入!gflag开查看开启的调试选项。
注意,一旦调试之前设置页堆注册表相应位置便不再为0,默认便不会开启hfc、hpc和htc。这一点要特别注意,调试以前要通过!gflag命令查看到底开启了何种调试选项。
因为在调试器运行一个程序且注册表中没有设置GlobalFlag键值 时,操作系统会启用htc、hfc和hpc三项调试功能,且它们原理非常简单,因此我们此处将主要精力放在经常使用,且需要手动开启的页堆上。
页堆DPH
利用堆尾检查可以在释放堆块时或在下次分配时检查到堆结构的破坏。但是这些检查都是滞后的,我们很难知道堆是何时发生的破坏。· 今天我们介绍的页堆(Debug Page Heap DPH)可以解决这个问题。启用DPH后堆管理器会在堆块后增加用于检测溢出的栈栏页,一旦用户数据溢出触及栈栏页将立即引发异常,从而让我们在第一个时间知道堆破坏。
前面介绍的win32堆使用用户数据区前面的_HEAP_ENTRY结构来描述堆块,一旦用户数据超出分配的空间将会覆盖堆块后面的数据。有可能覆盖下一个堆块的_HEAP_ENTRY结构或是空闲堆块的_HEAP_FREE_ENTRY结构导致堆被破坏。为了防止堆块的管理信息被覆盖后使堆发生不可恢复的破坏。页堆管理器除了在堆块用户区前面存储堆块管理信息外,还会将这些管理器信息存储在节点池内。
启用页堆
开启页堆可以使用gflags.exe来实现。
- Gui方式开启DPH。
- 控制台方式
gflag.exe /I calc.exe +hpa
设置完成后,使用windbg打开进程进行调试。为了验证是否开始DHP,可以执行!gflag /p命令:
也可以查看全局变量ntdll!RtlpDebugPageHeap的值来验证是否开启。当该值为1时表示开启dhp。
与普通堆相比页堆有很大的不同,每个堆块至少占用两个内存页。在存放用户数据的第一个内存页后面,堆管理器会额外多分配一个内存页。这个内存页是用来检测溢出的,被称为栅栏页。栅栏页的属性为PAEG_NOACCESS,因此一旦用户数据发生溢出触及到栅栏页便会引发异常,使调试人员第一时间发现问题,从而可以迅速定位到导致溢出的代码。
有人也许会有疑问,当堆块非常小时,难道也是占用两个内存页么?答案是肯定的。为了及时检测溢出,堆块被放到第一个内存页的末尾紧邻栅栏页,因此第一个内存页前面的大半部分有可能都是没有被使用的。由于分配粒度为8byte,堆块和栅栏页之间可能会有填充字段。对于很小的堆块也需要占用两个内存页,这是很耗费空间的。
测试页堆在调试中的效果
使用下面的代码生成HeapTest.exe,并使用windbg调试。
- HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 1024*1024);
- char * p = (char*)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 1012);
- char *q = p;
- for(int i = 0 ;i < 1048; i++)
- {
- *q++ = 0;
- }
- bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);
这段代码首先创建一个私有堆然后从私有堆分配1012个字节。但却访问分配地址后的1048byte的地址,很明显发生了堆溢出。
第一次我们不开启任何选项,观察全局标志:
发现默认开启了htc、hfc、hpc。
按F5继续运行,可以看到程序抛出异常而中断。
第一行的调试信息显示在访问堆块00420648时超过了它的大小3f4 = 1012Byte。而导致了访问违规。
查看堆栈调用:
可以发现是在对堆块释放时检测到堆块被破坏。显然这种方法不能在第一时间中断到出现为问题的地方。
第二次我们开启开启dph来观察:
首先开启dhp。gflags /i HeapTest.exe +hpa
在windbg中重新开始HeapTest.exe。可以看到程序发生内存访问异常。
最后一条指令由于访问eax所代表的地址而导致。
查看局部变量的值:
可以看到i等于1016时发生了异常。细心的同学可能会发现,我们申请的空间只有1012byte,为什么访问到1016时才会导致异常。这是因为在堆中的分配粒度是8byte,即分配的空间大小必须为8的倍数。因此此处填充了4个字节。
启用页堆后我们在第一时间发现了堆溢出的问题,结合源码分析便可很容易的找到导致堆溢出的地方。
准页堆
使用页堆确实是非常方便的,但是美中不足的是页堆要为每个堆块都分配两个内存页且只利用第一个内存页得后半部分,利用率是非常低的。在调试需要使用大量内存的应用程序时有可能会导致一些问题。为此引入了准页堆。
准页堆弥补了页堆的不足,同时还具有页堆的一些功能。准页堆也被称为常规页堆,页堆也被称为完全页堆。
准页堆不再为每个堆块分配栅栏页,只是在堆块的前后添加一些类似于安全Cookie的附加标记。当释放堆块时,堆管理器会检测这些标记的完好性。一旦检测到这些标记被破坏,便会产生异常。这种机制与释放时检查hfc类似。
由于是在释放时检测,因此准页堆并不具备页堆第一时间便能检测到堆破坏的优点。因此本文并不准备详细介绍。
启用准页堆
准页堆也需要手动开启,开启命令与页堆很像:gflags /p /enable calc.exe
本文介绍了win32堆的调试支持,重点介绍了页堆,其他调试功能调试器默认是开启的,仅以非常小的篇幅介绍。对于一些非常复杂的堆破坏问题,使用其他方式很难发现问题。即使有错误报告,错误报告处往往距离问题发生地十万八千里。而使用页堆却能很好的解决这个问题。仅仅使用一个命令打开页堆的调试功能便可以使困扰很久的问题迎刃而解,这也是本文之所以如此推崇页堆的原因。
下一篇文章将会介绍CRT堆。