Windows内存分配堆栈打印
一、简介
前面我们使用gflags和UMDH工具分析了内存泄漏问题,gflags为我们创建了用户模式堆栈跟踪数据库(下面简称 ust ),那么这个数据库除了使用UMDH来访问外,我们程序员能否使用代码获取相关数据呢?有一个方法,就是使用windows系统自带的verifier.dll库。
二、接口说明
verifier.dll中有一个VerifierEnumerateResource接口,该接口可以允许我们枚举某个进程在系统中的资源,当我们开启ust时,ust中的数据,也是可以被枚举出来的。
microsoft learn | VerifierEnumerateResource
ULONG VerifierEnumerateResource(
HANDLE Process,
ULONG Flags,
ULONG ResourceType,
AVRF_RESOURCE_ENUMERATE_CALLBACK ResourceCallback,
PVOID EnumerationContext
);
参数
Process
枚举资源的进程句柄。
当 ResourceType 参数为 AvrfResrouceHeapAllocation 时,必须使用PROCESS_VM_READ打开句柄,并PROCESS_QUERY_INFORMATION访问权限。
如果 ResourceType 为 AvrfResrouceHeapAllocation 并且 Flags 参数包含AVRF_ENUM_RESOURCES_FLAGS_SUSPEND,则还必须使用 PROCESS_SUSPEND_RESUME 标志。
Flags
如果 ResourceType 为 AvrfResourceHandleTrace,则不定义任何标志,Flags 参数的值必须为 0。
如果 ResourceType 参数为 AvrfResourceHeapAllocation, 则 Flags 参数可以是 0 或以下值的组合。
Value | 含义 |
---|---|
AVRF_ENUM_RESOURCES_FLAGS_DONT_RESOLVE_TRACES | 堆分配的堆栈回溯(如果存在)不会通过 ReturnAddresses 数组复制。 这可能会加快枚举过程。 |
AVRF_ENUM_RESOURCES_FLAGS_SUSPEND | 在执行堆分配枚举之前,进程会暂停。这最大限度地减少了更改堆可能影响枚举的可能性。 |
ResourceType
此参数可能是以下值之一:
值 | 含义 |
---|---|
AvrfResourceHandleTrace | API 从当前进程的句柄表中枚举最近保存的句柄上的操作。 |
AvrfResourceHeapAllocation | API 枚举堆分配,包括堆元数据块。 |
ResourceCallback
由 API 调用的应用程序定义的函数。
原型与所枚举的资源类型无关。 使用将传递适合所执行的枚举类型的原型
EnumerationContext
传递回回调函数的特定于应用程序的指针。
返回值
此函数返回 系统错误代码之一。
注解
此函数没有关联的导入库。 必须使用 LoadLibrary 和 GetProcAddress 函数动态链接到Verifier.dll。
三、Demo演示
下面我们就使用一个简单的demo来说明这个接口的用法。
🍉 首先我们申请一块内存作为测试
char* pszTest = new char[111111];
🍉 封装一个接口,以内存地址作为查找条件,打印内存的分配堆栈
PrintHeapStackTraceFromAddr(GetCurrentProcess(), pszTest);
🍉 实现PrintHeapStackTraceFromAddr接口
void PrintHeapStackTraceFromAddr(HANDLE hProcess, PVOID pAddr)
{
// 记录下地址
HMODULE hMod = LoadLibrary("verifier.dll");
if (hMod != NULL)
{
decltype(VerifierEnumerateResource)* VerifierEnumerateResource = (decltype(VerifierEnumerateResource))GetProcAddress(hMod, "VerifierEnumerateResource");
if (VerifierEnumerateResource != NULL)
{
VerifierEnumerateResource(hProcess, 0, AvrfResourceHeapAllocation, (AVRF_RESOURCE_ENUMERATE_CALLBACK)AvrfHeapAllocCallback, pAddr);
};
FreeLibrary(hMod);
}
}
该接口主要就是功能动态加载verifier.dll,调用VerifierEnumerateResource接口,这里我们将回调函数和内存地址pAddr传入参数中。
这里需要包含头文件#include <avrfsdk.h>
由于verifier.dll没有关联的导出库( verifier.lib ) ,所以只能使用LoadLibrary动态加载了。
🍉接着实现回调函数AvrfHeapAllocCallback
ULONG WINAPI AvrfHeapAllocCallback(PAVRF_HEAP_ALLOCATION pstHeapAllocation, PVOID pszTest, PULONG EnumerationLevel) {
if (pstHeapAllocation->BackTraceInformation->Depth > 0 && pstHeapAllocation->UserAllocationState == eUserAllocationState::AllocationStateBusy)
{
if ((ULONG64)pszTest == pstHeapAllocation->UserAllocation)
{
//TODO
*EnumerationLevel = HeapEnumerationStop;// 停止遍历
}
}
return 0;
}
通过比较地址的值,确定我们需要的堆栈信息。一般我们new和malloc都是拿到的用户数据区的地址,所以这里需要和pstHeapAllocation->UserAllocation比较,相对的,前面还有堆头数据,这个堆块的真正起始地址就是pstHeapAllocation->Allocation。注意,debug编译的话,这里也会存在问题,用户数据区还有一个crtdebug的头,所以new和malloc等接口真正拿到的并不是堆块用户区地址,我们demo代码只能release编译。
*EnumerationLevel = HeapEnumerationStop;// 停止遍历
当我们找到内存,打印信息后,就可以停止遍历了,让接口提前结束,提高效率。
🍉打印堆栈信息
printf("stacktrace:\n");
auto hProcess = GetCurrentProcess();
SymInitialize(hProcess, 0, true);
for (ULONG i = 0; i < pstHeapAllocation->BackTraceInformation->Depth; i++) {
DWORD64 dwDisplacement = 0;
IMAGEHLP_SYMBOL64_PACKAGE sp = { 0 };
sp.sym.SizeOfStruct = sizeof(sp.sym);
sp.sym.MaxNameLength = sizeof(sp.name);
if (TRUE == SymGetSymFromAddr(hProcess, pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], &dwDisplacement, &sp.sym))
{
IMAGEHLP_LINE line = { sizeof(line)};
DWORD dwLineDisplacement = 0;
if (TRUE == SymGetLineFromAddr(hProcess, pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], &dwLineDisplacement, &line))
{
printf("\t%Iu %s+%Id(%s at %d)\n", pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], sp.sym.Name, dwDisplacement, line.FileName, line.LineNumber);
}
else
{
printf("\t%Iu %s+%Id\n", pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], sp.sym.Name, dwDisplacement);
}
}
}
SymCleanup(hProcess);
打印堆栈符号,我们用到了前面文章中提到的dbghelp库。
🍉完整demo
// ConsoleApplication3.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <stdio.h>
#include <windows.h>
#include <avrfsdk.h>
#include "DbgHelp.h"
#pragma comment(lib, "DbgHelp.lib")
#pragma warning(disable:4996)
ULONG WINAPI AvrfHeapAllocCallback(PAVRF_HEAP_ALLOCATION pstHeapAllocation, PVOID pszTest, PULONG EnumerationLevel) {
if (pstHeapAllocation->BackTraceInformation->Depth > 0 && pstHeapAllocation->UserAllocationState == eUserAllocationState::AllocationStateBusy)
{
if ((ULONG64)pszTest == pstHeapAllocation->UserAllocation)
{
printf("stacktrace:\n");
auto hProcess = GetCurrentProcess();
SymInitialize(hProcess, 0, true);
for (ULONG i = 0; i < pstHeapAllocation->BackTraceInformation->Depth; i++) {
DWORD64 dwDisplacement = 0;
IMAGEHLP_SYMBOL64_PACKAGE sp = { 0 };
sp.sym.SizeOfStruct = sizeof(sp.sym);
sp.sym.MaxNameLength = sizeof(sp.name);
if (TRUE == SymGetSymFromAddr(hProcess, pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], &dwDisplacement, &sp.sym))
{
IMAGEHLP_LINE line = { sizeof(line)};
DWORD dwLineDisplacement = 0;
if (TRUE == SymGetLineFromAddr(hProcess, pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], &dwLineDisplacement, &line))
{
printf("\t%Iu %s+%Id(%s at %d)\n", pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], sp.sym.Name, dwDisplacement, line.FileName, line.LineNumber);
}
else
{
printf("\t%Iu %s+%Id\n", pstHeapAllocation->BackTraceInformation->ReturnAddresses[i], sp.sym.Name, dwDisplacement);
}
}
}
SymCleanup(hProcess);
*EnumerationLevel = HeapEnumerationStop;// 停止遍历
}
}
return 0;
}
void PrintHeapStackTraceFromAddr(HANDLE hProcess, PVOID pAddr)
{
// 记录下地址
HMODULE hMod = LoadLibrary("verifier.dll");
if (hMod != NULL)
{
decltype(VerifierEnumerateResource)* VerifierEnumerateResource = (decltype(VerifierEnumerateResource))GetProcAddress(hMod, "VerifierEnumerateResource");
if (VerifierEnumerateResource != NULL)
{
VerifierEnumerateResource(hProcess, 0, AvrfResourceHeapAllocation, (AVRF_RESOURCE_ENUMERATE_CALLBACK)AvrfHeapAllocCallback, pAddr);
};
FreeLibrary(hMod);
}
}
#define TEST_STR "HelloWorld!"
int main(int argc, char* argv[])
{
printf("Enter Main!\n");
char* pszTest = new char[111111];
if (pszTest)
{
strcpy(pszTest, TEST_STR);
printf("%s\n", pszTest);
PrintHeapStackTraceFromAddr(GetCurrentProcess(), pszTest);
}
delete[]pszTest;
printf("Leave Main!\n");
return 0;
}
这份demo代码需要在以下编译选项下编译
x64
release
多字节编码
禁止优化
🍉运行Demo
编译完成后,运行demo。如果直接运行就会是这样的:
Enter Main!
HelloWorld!
Leave Main!
别忘了,我们需要开启ust哦:
gflags /i ConsoleApplication3.exe +ust
再次运行:
Enter Main!
HelloWorld!
stacktrace:
140714047821429 RtlNotifyFeatureUsage+51237
140714001563814 malloc_base+54
140699903334583 operator new+31(D:\a\_work\1\s\src\vctools\crt\vcstartup\src\heap\new_scalar.cpp at 36)
140699903333301 main+37(D:\Test\VS2022Test\ConsoleApplication3\ConsoleApplication3\ConsoleApplication3.cpp at 68)
140699903334032 __scrt_common_main_seh+268(D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl at 288)
140714028913632 BaseThreadInitThunk+16
140714047064155 RtlUserThreadStart+43
Leave Main!
完成了!!!
下一篇介绍下如何遍历进程的堆,这样配合ust,能够分析进程中内存的分布和分配情况