一、简介
前面我们通过用户模式堆栈跟踪数据库,查找并打印指定内存的堆栈,这个过程首先是拿到一个感兴趣的内存块地址,然后再去查找他的堆栈。上一篇我们用的是一个测试地址,那么实际工作中什么是我们感兴趣的堆内存块呢?一般来说是一些非常大的内存,分配次数非常多的内存,这些都是可以(需要)优化的内存。从这个角度说,首先就得分析整个进程中的堆内存块,得出最大的内存块列表,这就需要遍历整个堆了。今天就简单介绍下如何遍历堆列表,并写一个demo。
二、遍历本进程的堆内存列表
2.1、获取堆列表
每个windows进程都可能会有很多堆,首先每个进程会有自己的默认堆,其次,很多版本的crt库都会有自己建立的堆,另外开发者可能会自己主动调用HeapCreate创建自建堆。所以一个进程是可能存在有多个堆的。我们的第一步是获取进程的堆列表。
DWORD GetProcessHeaps(
[in] DWORD NumberOfHeaps,
[out] PHANDLE ProcessHeaps
);
参数
[in] NumberOfHeaps
可以存储在 ProcessHeaps 指向的缓冲区中的最大堆句柄数。
[out] ProcessHeaps
指向接收堆句柄数组的缓冲区的指针。
getProcessHeaps 函数 (heapapi.h) - Win32 apps | Microsoft Learnhttps://learn.microsoft.com/zh-cn/windows/win32/api/heapapi/nf-heapapi-getprocessheaps这里我们可以通过传空来获取堆个数,然后再分配缓存来获取真正的堆列表
DWORD dwNum = GetProcessHeaps(0, nullptr);
HANDLE* pHeapHandle = new HANDLE[dwNum];
dwNum = GetProcessHeaps(dwNum, pHeapHandle);
2.2、枚举指定堆中的内存块
获取了堆以后,我们就需要枚举这个堆中的内存块了
BOOL HeapWalk(
[in] HANDLE hHeap,
[in, out] LPPROCESS_HEAP_ENTRY lpEntry
);
参数
[in] hHeap
堆的句柄。 此句柄由 HeapCreate 或 GetProcessHeap 函数返回。
[in, out] lpEntry
指向 PROCESS_HEAP_ENTRY 结构的指针,该结构维护特定堆枚举的状态信息。
如果 HeapWalk 函数成功并返回值 TRUE,则此结构的成员包含有关堆中下一个内存块的信息。
若要启动堆枚举,请将 PROCESS_HEAP_ENTRY 结构的 lpData 字段设置为 NULL。 若要继续特定的堆枚举,请重复调用 HeapWalk 函数,不更改 hHeap、 lpEntry 或 PROCESS_HEAP_ENTRY 结构的任何成员。
for (size_t i = 0; i < dwNum; i++)
{
HANDLE h = pHeapHandle[i];
HeapLock(h);
PROCESS_HEAP_ENTRY phe = { 0 };
while (HeapWalk(h, &phe))
{
if ((phe.wFlags & PROCESS_HEAP_ENTRY_BUSY))
{
printf("\t%s!\n", (char*)phe.lpData);
}
}
HeapUnlock(h);
}
我们这里就简单地只统计PROCESS_HEAP_ENTRY_BUSY标记的堆内存,这些是已经分配的堆内存。
将所有busy的 PROCESS_HEAP_ENTRY 保存下来,就可以做进一步统计分析了,这个以后再说。这里我们完成了堆内存的遍历。
三、遍历其他进程的堆列表
上面的遍历接口是非常高效的,不过他只能获取本进程的堆列表,而使用ToolHelp32库,可以获取其他进程的资源,包括枚举进程,枚举线程,枚举模块,枚举内存列表。不过这个接口在枚举内存列表时非常低效。
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
这个接口很有意思,从接口名称便可以看出,它是获取和创建了系统/进程某一时刻某一些资源的快照,这个快照是只读的,你可以通过他获取系统中所有的进程信息,可以通过他获取某个进程中所有的线程,也可以通过他获取某个进程中所有的模块信息,包括我们这里使用他获取堆列表。
参数
[in] dwFlags
要包含在快照中的系统部分。 此参数可使用以下一个或多个值。
值 | 含义 |
---|---|
TH32CS_INHERIT 0x80000000 | 指示快照句柄是可继承的。 |
TH32CS_SNAPALL | 包括系统中的所有进程和线程,以及 th32ProcessID 中指定的进程的堆和模块。 等效于指定使用 OR 操作 (“|”组合的TH32CS_SNAPHEAPLIST、TH32CS_SNAPMODULE、TH32CS_SNAPPROCESS和TH32CS_SNAPTHREAD值) 。 |
TH32CS_SNAPHEAPLIST 0x00000001 | 包括快照 th32ProcessID 中指定的进程的所有堆。 若要枚举堆,请参阅 Heap32ListFirst。 |
TH32CS_SNAPMODULE 0x00000008 | 包括快照 th32ProcessID 中指定的进程的所有模块。 若要枚举模块,请参阅 Module32First。 如果函数失败并 出现ERROR_BAD_LENGTH,请重试该函数,直到成功。 64 位 Windows: 在 32 位进程中使用此标志包括 th32ProcessID 中指定的进程的 32 位模块,而在 64 位进程中使用它包括 64 位模块。 若要从 64 位进程包括 th32ProcessID 中指定的进程的 32 位模块,请使用 TH32CS_SNAPMODULE32 标志。 |
TH32CS_SNAPMODULE32 0x00000010 | 从 64 位进程调用时,包括快照中 th32ProcessID 中指定的进程的所有 32 位模块。 此标志可以与 TH32CS_SNAPMODULE 或 TH32CS_SNAPALL结合使用。 如果函数失败并 出现ERROR_BAD_LENGTH,请重试该函数,直到成功。 |
TH32CS_SNAPPROCESS 0x00000002 | 包括系统中快照中的所有进程。 若要枚举进程,请参阅 Process32First。 |
TH32CS_SNAPTHREAD 0x00000004 | 包括快照系统中的所有线程。 若要枚举线程,请参阅 Thread32First。 若要标识属于特定进程的线程,请在枚举线程时将其进程标识符与 THREADENTRY32 结构的 th32OwnerProcessID 成员进行比较。 |
[in] th32ProcessID
要包含在快照中的进程的进程标识符。 此参数可以为零以指示当前进程。 指定 TH32CS_SNAPHEAPLIST、 TH32CS_SNAPMODULE、 TH32CS_SNAPMODULE32或 TH32CS_SNAPALL 值时,使用此参数。 否则,将忽略它,并且所有进程都包含在快照中。
如果指定的进程是空闲进程或 CSRSS 进程之一,则此函数将失败,并且最后一个错误代码 ERROR_ACCESS_DENIED ,因为它们的访问限制阻止用户级代码打开它们。
如果指定的进程是 64 位进程,调用方是 32 位进程,则此函数将失败,最后一个错误代码 ERROR_PARTIAL_COPY (299) 。
3.1、获取堆内存列表快照
使用TH32CS_SNAPHEAPLIST来指定抓取指定进程id的堆内存列表快照
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST, dwPID);
if (INVALID_HANDLE_VALUE == hSnapShot)
{
printf("CreateToolhelp32Snapshot failed! error:%d\n", GetLastError());
return -1;
}
3.2、遍历快照中的堆列表
从快照中遍历堆列表
// 遍历堆
HEAPLIST32 hplt = {sizeof(hplt)};
if (TRUE == Heap32ListFirst(hSnapShot, &hplt))
{
do
{
printf("HeapId:%Id\n", hplt.th32HeapID);
} while (TRUE == Heap32ListNext(hSnapShot, &hplt));
}
通过接口Heap32ListFirst和Heap32ListNext可以遍历所有的堆。
3.3、遍历指定堆中的内存块
通过接口Heap32First和Heap32Next,传入上面遍历的堆id,可以遍历堆中所有的内存块。
// 遍历堆中的内存块
HEAPENTRY32 hp = { sizeof(hp) };
if (TRUE == Heap32First(&hp, hplt.th32ProcessID, hplt.th32HeapID))
{
do
{
if (!(hp.dwFlags & LF32_FREE))
{
// printf("\t%Id\n", hp.dwAddress);
}
} while (TRUE == Heap32Next(&hp));
}
3.4、完整函数
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST, dwPID);
if (INVALID_HANDLE_VALUE == hSnapShot)
{
printf("CreateToolhelp32Snapshot failed! error:%d\n", GetLastError());
return -1;
}
// 遍历堆
HEAPLIST32 hplt = {sizeof(hplt)};
if (TRUE == Heap32ListFirst(hSnapShot, &hplt))
{
do
{
printf("HeapId:%Id\n", hplt.th32HeapID);
// 遍历堆中的内存块
HEAPENTRY32 hp = { sizeof(hp) };
if (TRUE == Heap32First(&hp, hplt.th32ProcessID, hplt.th32HeapID))
{
do
{
if (!(hp.dwFlags & LF32_FREE))
{
// TODO
// printf("\t%Id\n", hp.dwAddress);
}
} while (TRUE == Heap32Next(&hp));
}
} while (TRUE == Heap32ListNext(hSnapShot, &hplt));
}
四、总结
正如官网所说,HeapWalk性能远高于快照中的Heap32Next接口,实际测试第二种方式也非常拉跨。
使用 HeapWalk 函数行走堆的大小大致是线性的,而使用 Heap32Next 函数遍走堆的大小大致为二次。 即使对于具有 10,000 个分配的适度堆, HeapWalk 的运行速度也比 Heap32Next 快 10,000 倍,同时提供更详细的信息。 随着堆大小的增加,性能差异变得更加明显。