内存堆与锁的测试设置详解
在软件开发过程中,内存堆(Heaps)和锁(Locks)相关的问题常常困扰着开发者。下面将详细介绍内存堆和锁的测试设置,帮助开发者更好地调试和解决相关问题。
1. 内存堆测试设置
内存堆测试设置包含强大的工具,能帮助开发者更轻松地解决与堆相关的问题。除了各种验证器停止点外,它还能对每个堆块进行检测,包括堆块填充模式、堆块保护页和堆栈跟踪。堆栈跟踪功能非常有用,它能提供进程中所有内存分配和释放的历史记录。
1.1 可配置选项
内存堆测试设置的可配置选项如下:
| 选项 | 说明 |
| ---- | ---- |
| Full | 堆检测分为两种模式:
- 普通页堆(Normal pageheap):轻量级版本,检测堆问题不如完整页堆及时,但运行速度快,资源需求少。取消“Full”复选框可使用普通页堆。
- 完整页堆(Full pageheap):能在问题发生时立即检测到堆相关问题,便于调试,但资源需求大,运行速度慢。选中“Full”复选框可使用完整页堆。 |
| Dlls | 指定参与堆测试的 DLL 名称,多个 DLL 用空格分隔。 |
| Size | 启用特定大小或大小范围的分配测试。选中此复选框后,需填写“SizeStart”和“SizeEnd”字段。 |
| SizeStart | 若选择测试特定大小的分配,需输入感兴趣的大小范围的起始值。 |
| SizeEnd | 若选择测试特定大小的分配,可输入感兴趣的大小范围的结束值。 |
| Random | 选择是否在分配时引入随机因素,决定使用哪种检测模型。选中此复选框后,需在“RandRate”字段中指定随机率。 |
| RandRate | 若选中“Random”复选框,需在此字段输入 0 到 100 之间的值,表示在普通页堆和完整页堆上进行分配的概率。 |
| Backward | 选中此复选框可检测缓冲区下溢问题。 |
| Unalign | 选中此复选框可生成与堆管理器分配粒度不对齐的分配,有助于捕获堆块溢出问题。但某些组件要求分配与堆管理器的分配粒度对齐,此时不能选中此复选框。 |
| DeCommit | 选中此复选框可在保护页不再使用时释放它们,降低内存使用量。 |
| Traces | 选中此复选框可收集分配和释放的堆栈跟踪信息。此复选框默认开启,建议大多数情况下保持开启。 |
| Protect | 选中此复选框可保护内部堆结构不被破坏,用于检测随机损坏。 |
| NoLock | 选中此复选框,测试设置将不进行关键部分验证。 |
| Faults | 选中此复选框可启用堆故障注入。选中后,需填写“FaultRate”和“TimeOut”字段。 |
| FaultRate | 若选中“Faults”复选框,需在此字段输入 1 到 10,000 之间的值,表示堆管理器中发生故障的概率。 |
| TimeOut | 若选中“Faults”复选框,可更改进程初始化期间不进行故障注入的时间(以毫秒为单位)。 |
| Addr | 控制测试设置工作的地址范围。选中此复选框后,需指定“AddrStart”和“AddrEnd”字段。 |
| AddrStart | 指定测试设置工作范围的起始地址。 |
| AddrEnd | 指定测试设置工作范围的结束地址。 |
| UseLFHGuardPages | 选中此复选框可在低碎片堆上使用保护页。低碎片堆是 Windows 2003 Server 引入的较新的堆,在 Windows Vista 中默认开启。 |
1.2 普通页堆和完整页堆的区别
- 普通页堆 :基本思想是通过在堆块上使用填充模式来捕获某些类型的堆相关问题。当进行堆分配时,堆管理器用“E0”填充整个分配区域。若分配的内存未正确初始化就被引用,很可能会发生访问冲突。当释放分配的内存时,堆管理器用“F0”填充。任何尝试使用已释放的分配内存也可能导致访问冲突。填充模式还包括在每个分配块的前后添加预定义模式,有助于检测堆溢出和下溢问题。
- 完整页堆 :使用保护页来保护分配的内存。每次进行分配时,在分配的末尾添加一页不可访问的内存,任何超出分配末尾的内存访问都会立即失败。同样,也可在分配的开头放置保护页,以捕获在实际分配之前发生的内存写入。当释放内存块时,不会立即将其返回给系统,而是保留一段时间,以便捕获任何尝试访问已删除分配的代码。完整页堆除了使用保护页外,还使用填充模式。
1.3 堆测试设置的停止代码
堆测试设置包含多个停止代码,在进程执行期间进行监控。默认情况下,所有与堆相关的停止代码触发时都会在调试器中中断。具体停止代码如下:
| 停止代码 | 测试 | 描述 |
| ---- | ---- | ---- |
| 00000001 | 未知错误 | 当发生无法归类的堆错误时触发。 |
| 00000002 | 缓冲区溢出 | 启用完整页堆时,应用程序写入超出缓冲区末尾并触及保护页时触发。 |
| 00000003 | 未序列化堆 | 非序列化堆不应被并发线程访问。若两个或多个线程同时访问非序列化堆,此停止代码触发。 |
| 00000004 | 大分配 | 当指定给 HeapAlloc 和 HeapReAlloc API 的分配大小不合理时触发。 |
| 00000005 | 无效堆句柄 | 指定给堆 API 的句柄无效时触发。 |
| 00000006 | 不匹配的堆 | 在一个堆中分配的内存块在另一个堆中释放时触发。 |
| 00000007 | 多次释放块 | 堆块被释放两次时触发。 |
| 00000008 | 通用堆损坏 | 由无法分类的堆损坏引起。 |
| 00000009 | 销毁默认进程堆 | 应用程序尝试销毁默认进程堆时触发。 |
| 0000000A | 堆管理器 | 堆管理器代码引发访问冲突时触发。 |
| 0000000B | 通用堆损坏 | 堆损坏的来源不确定时触发,例如将指向不可访问内存的地址传递给堆释放 API。 |
| 0000000C | 通用堆损坏 | 堆损坏的来源不确定时触发,例如将指向不可访问内存的地址传递给堆释放 API 或多次释放内存。 |
| 0000000D | 释放后使用 | 内存块被释放后又被写入时触发。 |
| 0000000E | 释放后使用 | 通常在普通页堆模式下,使用填充模式检测堆问题时触发。 |
| 0000000F | 缓冲区溢出 | 在普通页堆模式下,通过检查填充模式检测到缓冲区溢出时触发。 |
| 00000010 | 缓冲区下溢 | 发生缓冲区下溢且堆块头的起始标记损坏时触发。 |
| 00000011 | 缓冲区下溢 | 发生缓冲区下溢且堆块头的结束标记损坏时触发。 |
| 00000012 | 缓冲区下溢 | 发生缓冲区下溢且堆块头的前缀损坏时触发。 |
| 00000013 | 首次访问冲突 | 首次发生访问冲突时触发。 |
| 00000014 | 进程堆 | 调用 GetProcessHeaps 导致堆管理器检测到堆管理器结构不一致时触发。 |
2. 完整页堆和普通页堆的选择
完整页堆捕获问题的概率更高,更易于追踪问题,但内存使用量大,可能导致系统变慢,使某些问题无法出现。通常,在开发过程中可始终使用普通页堆,在定期检查点启用完整页堆进行更全面的测试。为减轻完整页堆测试的压力,可将测试范围缩小到特定的 DLL 或分配大小。
下面是选择页堆模式的流程图:
graph TD;
A[开始] --> B{是否需要高精度检测};
B -- 是 --> C[使用完整页堆];
B -- 否 --> D{是否关注性能};
D -- 是 --> E[使用普通页堆];
D -- 否 --> C;
C --> F[进行测试];
E --> F;
F --> G[结束];
3. 锁测试设置
Windows 操作系统是抢占式操作系统,多线程应用程序需要严格控制共享资源的访问,以避免多个线程同时读写资源的问题。常用的同步技术是使用关键部分(Critical Section),它可确保同一时间只有一个线程能访问受保护的资源。
关键部分在使用前必须初始化,不再需要时必须删除,以避免内存泄漏。一个线程可以多次进入关键部分,但每次进入都需要有相应的释放调用。由于关键部分经常被误用,可能导致内存泄漏、死锁等问题。锁测试设置可对关键部分进行一系列检查,节省调试并发问题的时间。
3.1 锁测试设置的测试项
锁测试设置提供的测试项如下:
| 停止代码 | 测试 | 描述 |
| ---- | ---- | ---- |
| 00000200 | 线程状态不允许持有关键部分 | 线程终止、挂起或处于无法持有关键部分的状态时触发。 |
| 00000201 | 卸载 DLL 时存在活动关键部分 | 卸载 DLL 时发现关键部分仍处于活动状态,应用程序验证器停止执行。 |
| 00000202 | 释放包含活动关键部分的堆块 | 释放包含活动关键部分的堆块时,应用程序验证器停止执行,避免关键部分泄漏。 |
| 00000203 | 仅初始化一次 | 关键部分只能初始化一次,多次初始化是未定义行为,代表代码中存在错误。 |
| 00000204 | 忘记释放关键部分 | 释放包含关键部分的内存块但未使用 DeleteCriticalSection API 删除关键部分时触发。 |
| 00000205 | 无效的 DebugInfo 指针 | 关键部分的 DebugInfo 字段指向已释放的内存时触发。 |
| 00000206 | 验证所有者线程 ID 的正确性 | 每次进入关键部分时记录线程 ID,若当前执行上下文中线程 ID 无效,应用程序验证器停止执行。 |
| 00000207 | 验证递归计数的正确性 | 多次进入关键部分的线程会增加关键部分的递归计数,若当前执行上下文中递归计数无效,应用程序验证器停止执行。 |
| 00000208 | 未初始化就删除 | 未初始化关键部分就进行删除操作时,应用程序验证器停止执行。 |
| 00000209 | 释放次数匹配 | 线程释放关键部分的次数多于进入次数时,应用程序验证器停止执行。 |
| 00000210 | 使用前未初始化或使用已删除的关键部分 | 使用未初始化或已删除的关键部分时,应用程序验证器停止执行。 |
| 00000211 | 重新初始化 | 当前线程重新初始化关键部分时,应用程序验证器停止执行。 |
| 00000212 | 无效删除(VirtualFree) | 使用 VirtualFree 释放包含活动关键部分的内存块,但未通过 DeleteCriticalSection API 释放关键部分时触发。 |
| 00000213 | 无效删除(UnmapViewOfFile) | 使用 UnmapViewOfFile API 取消映射包含活动关键部分的内存块,但未通过 DeleteCriticalSection API 释放关键部分时触发。 |
| 00000214 | 离开未拥有的关键部分 | 线程调用 LeaveCriticalSection 离开其未拥有的关键部分时触发。 |
| 00000215 | 私有锁使用 | 线程尝试进入不同 DLL 中定义的私有关键部分时触发。 |
这些测试项涵盖了使用关键部分时常见的编程错误,锁测试设置能在问题发生时及时捕获,避免事后随机出现问题,节省调试时间。在编写需要管理多线程同步的代码时,建议启用此测试设置。
综上所述,合理使用内存堆和锁的测试设置,能帮助开发者更高效地发现和解决软件开发过程中的问题,提高代码的稳定性和可靠性。
内存堆与锁的测试设置详解(续)
4. 内存堆测试设置操作要点总结
为了更好地使用内存堆测试设置,下面总结一些操作要点:
1.
页堆模式选择
- 若追求快速开发且对内存使用敏感,优先考虑普通页堆。其运行速度快、资源需求少,能在一定程度上检测堆问题。操作时,在属性窗口中取消“Full”复选框。
- 若需要高精度检测堆问题,如定位缓冲区溢出、释放后使用等问题,选择完整页堆。虽然它内存消耗大、运行慢,但能在问题发生时立即捕获。操作是在属性窗口中选中“Full”复选框。
2.
DLL 指定
:若只想对特定的 DLL 进行堆测试,在“Dlls”字段中输入 DLL 名称(包含扩展名),多个 DLL 用空格分隔。
3.
分配大小测试
:若要测试特定大小或大小范围的分配,先选中“Size”复选框,然后在“SizeStart”和“SizeEnd”字段中分别输入起始和结束大小。
4.
随机因素引入
:若想在分配时引入随机因素,决定使用普通页堆还是完整页堆,选中“Random”复选框,并在“RandRate”字段中输入 0 到 100 之间的值,表示在普通页堆上进行分配的概率。
5.
其他选项
:根据具体需求,可选择启用“Backward”检测缓冲区下溢,“Unalign”捕获堆块溢出,“DeCommit”降低内存使用量,“Traces”收集堆栈跟踪信息,“Protect”保护内部堆结构,“NoLock”不进行关键部分验证,“Faults”启用堆故障注入等。启用“Faults”时,需填写“FaultRate”和“TimeOut”字段。
5. 锁测试设置操作要点总结
在使用锁测试设置时,以下操作要点需注意:
1.
关键部分初始化与删除
:确保关键部分在使用前正确初始化,使用完后及时使用 DeleteCriticalSection API 删除,避免内存泄漏和未定义行为。
2.
线程状态检查
:注意线程状态,避免在不允许持有关键部分的状态下操作,如线程终止、挂起时。
3.
递归计数与线程 ID 验证
:多次进入关键部分时,确保递归计数正确,且线程 ID 在当前执行上下文中有效。
4.
DLL 卸载与内存释放
:卸载 DLL 时,检查是否存在活动关键部分;释放包含关键部分的内存块时,确保已通过 DeleteCriticalSection API 释放关键部分。
6. 综合应用示例
下面通过一个简单的示例,展示如何综合使用内存堆和锁测试设置来调试多线程程序。
假设我们有一个多线程程序,多个线程会同时访问共享资源,并且会进行内存分配和释放操作。
1.
启用测试设置
- 对于内存堆测试,选择完整页堆模式,以便及时检测堆问题。同时,启用“Traces”选项收集堆栈跟踪信息,帮助定位问题。
- 对于锁测试,启用所有测试项,确保关键部分的正确使用。
2.
编写代码
#include <windows.h>
#include <stdio.h>
CRITICAL_SECTION cs;
HANDLE hHeap = GetProcessHeap();
DWORD WINAPI ThreadFunction(LPVOID lpParam) {
EnterCriticalSection(&cs);
// 进行内存分配
LPVOID pMemory = HeapAlloc(hHeap, 0, 1024);
if (pMemory != NULL) {
// 使用内存
// ...
// 释放内存
HeapFree(hHeap, 0, pMemory);
}
LeaveCriticalSection(&cs);
return 0;
}
int main() {
InitializeCriticalSection(&cs);
HANDLE hThreads[2];
for (int i = 0; i < 2; i++) {
hThreads[i] = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
}
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
for (int i = 0; i < 2; i++) {
CloseHandle(hThreads[i]);
}
DeleteCriticalSection(&cs);
return 0;
}
-
调试过程
- 运行程序,若触发内存堆或锁测试设置的停止代码,调试器会中断。
- 根据停止代码和堆栈跟踪信息,定位问题所在。例如,若触发“00000002”停止代码,说明发生了缓冲区溢出;若触发“00000201”停止代码,说明卸载 DLL 时存在活动关键部分。
7. 总结与建议
内存堆和锁测试设置是调试多线程程序和检测堆问题的强大工具。在开发过程中,建议:
1. 始终启用普通页堆进行日常开发,定期使用完整页堆进行全面检测。
2. 在编写多线程代码时,启用锁测试设置,避免常见的关键部分使用错误。
3. 遇到问题时,结合停止代码和堆栈跟踪信息,快速定位和解决问题。
通过合理使用这些测试设置,能有效提高代码的稳定性和可靠性,减少调试时间和成本。
下面是一个综合使用内存堆和锁测试设置的流程图:
graph TD;
A[开始开发] --> B{是否多线程程序};
B -- 是 --> C[启用锁测试设置];
B -- 否 --> D[不启用锁测试设置];
C --> E{是否关注堆问题};
D --> E;
E -- 是 --> F[选择页堆模式];
E -- 否 --> G[不进行堆测试设置];
F -- 普通页堆 --> H[取消“Full”复选框];
F -- 完整页堆 --> I[选中“Full”复选框];
H --> J[根据需求设置其他堆选项];
I --> J;
J --> K[编写代码];
G --> K;
K --> L[运行程序];
L --> M{是否触发停止代码};
M -- 是 --> N[根据停止代码和堆栈跟踪定位问题];
M -- 否 --> O[程序正常运行];
N --> P[修复问题];
P --> L;
O --> Q[结束开发];
通过以上内容,我们详细介绍了内存堆和锁的测试设置,包括可配置选项、停止代码、操作要点、综合应用示例等。希望这些信息能帮助开发者更好地调试和优化程序,提高代码质量。
超级会员免费看
2575

被折叠的 条评论
为什么被折叠?



