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

此参数可能是以下值之一:

含义
AvrfResourceHandleTraceAPI 从当前进程的句柄表中枚举最近保存的句柄上的操作。
AvrfResourceHeapAllocationAPI 枚举堆分配,包括堆元数据块。

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,能够分析进程中内存的分布和分配情况

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙语者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值