65.9K
CodeProject 正在变化。 阅读更多。
Home

第五部分: Windows 调试技术 - 调试内存泄漏( CRT API)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (8投票s)

2014年2月13日

CPOL

4分钟阅读

viewsIcon

36113

downloadIcon

399

这是关于在 Windows 上调试应用程序的各种技术的第五部分,重点是内存泄漏。

引言

本文以简单易懂的方式解释了 Windows 应用程序中内存泄漏的调试方法。本文的范围仅限于用户模式调试。本文涵盖了使用 CRT API 的调试技术。

注意:这是一个系列文章,分为 5 个部分。
第一部分:Windows 调试技术 - 应用程序崩溃调试 (Windbg)
第二部分:Windows 调试技术 - 应用程序崩溃调试 (DebugDiag, AppVerifier)
第三部分:Windows 调试技术——调试应用程序崩溃(自动生成转储)
第四部分: Windows 调试技术 - 调试内存泄漏( Perfmon)
第五部分: Windows 调试技术 - 调试内存泄漏( CRT API)

背景

正如第四部分所观察到的,仅凭 perfmon 工具很难追踪问题。如果有人能精确指出泄漏的确切位置,那将是多么容易,我们只需要修复它。有一些可用的 CRT API 可以为我们完成这项工作,让生活更轻松。

定义

我们都知道,当我们使用 new/malloc/calloc/realloc 动态分配内存时,它实际上会进入堆内存。调试内存泄漏问题意味着调试堆内存并检查是否存在问题。CRT 调试堆通过提供一组 API 来实现这一点,这些 API 分配的内存比实际请求的多,用于跟踪内存详细信息并在应用程序退出时转储这些信息。

因此,如果我们使用 malloc 在发布模式下分配 10 字节,那么只会分配 10 字节。但是一旦我们使用 CRT API 启用调试,就会分配将近 46 字节。这额外的 36 字节用于跟踪内存泄漏。

除此之外,它还会调用调试版本 "_malloc_dbg" 而不是 malloc。有关更多信息,请参阅:http://msdn.microsoft.com/en-us/library/974tc9t1.aspx

调试技术

让我们一步一步地使用 CRT API

步骤 1:添加 CRT 调试 API 的包含

在所有其他 include 完成后,将以下代码添加到应用程序中。

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

_CRTDBG_MAP_ALLOC 通知编译器将堆函数映射到它们的调试版本,即 malloc 应映射到 _malloc_dbg

步骤 2:将 new 运算符映射到其调试版本

默认情况下,new 不会映射到其调试版本,我们需要强制这样做。为此,请将以下代码添加到应用程序中。默认情况下,malloc/calloc/realloc 会映射到它们的调试版本。New 需要显式完成。

如果您在代码中没有使用 new 运算符,则可以避免此步骤。

#ifdef _DEBUG   
#ifndef DBG_NEW      
#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )     
#define new DBG_NEW   
#endif
#endif

步骤 3:设置调试标志并设置报告模式

需要设置调试标志,它定义了我们要调试的内容。需要设置报告模式来定义输出是重定向到 DebugView 等调试窗口,还是发送到文件。

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);

让我们来理解上面的代码。

  • _CRTDBG_ALLOC_MEM_DF 启用调试分配。
  • _CRTDBG_LEAK_CHECK_DF 将在应用程序退出时执行内存泄漏检查。
  • _CRTDBG_MODE_DEBUG 将导致输出重定向到 DebugView 等调试窗口或 Visual Studio 中的 Output Window。

还有更多可用的调试选项。请参阅:http://msdn.microsoft.com/en-us/library/974tc9t1.aspx

完成以上步骤后,第四部分中提到的代码将变为如下所示。

#include <iostream>
#include <Windows.h>
#include <tchar.h>
#include <string>
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

#ifdef _DEBUG   
#ifndef DBG_NEW      
#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )     
#define new DBG_NEW   
#endif
#endif  // _DEBUG

using namespace std;

bool GetComputerName(wstring& wstrCompName);
bool fileread(const wstring& filepath);
bool GetUserName(wstring& wstrUserName);

int main()
{
	_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
	_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);

	int nChoice = 0;
	wstring wstrCompName;
	wstring wstrUserName;
	char ccontinue = '\0'; 

	do
	{
		wcout<<L"Enter your choice"<<endl;
		wcout<<L"1.Read Computer name"<<endl;
		wcout<<L"2.Read User Name"<<endl;
		cin>>nChoice;

		switch(nChoice)
		{
		case 1:
			GetComputerName(wstrCompName);	
			wcout<<L"Computer name read is"<<wstrCompName.c_str()<<endl;
			break;
		case 2:
			GetUserName(wstrUserName);
			wcout<<L"user name read is"<<wstrUserName.c_str()<<endl;
			break;
		default:
			wcout<<L"Invalid option";
		}
		wcout<<"Do you want to continue Y or N";
		cin>>ccontinue;
	}while(ccontinue == 'y' || ccontinue == 'Y');
	return 0;
}

bool GetComputerName(wstring& wstrCompName)
{
	wchar_t* pwszCompName = NULL;
	DWORD dwSize = 0;
	bool bRet = false;

	if (!GetComputerNameEx(ComputerNameDnsHostname, pwszCompName, &dwSize))
	{
		if(GetLastError() != ERROR_MORE_DATA)
		{
			wcout<<L"GetComputerNameEx Failed with Error Code"<<GetLastError();
			return false;
		}
		pwszCompName = new wchar_t[dwSize + 1];
		if(pwszCompName == NULL)
		{
			wcout<<L"unable to allocate memory"<<endl;
			return false;
		}
		memset(pwszCompName, L'\0', dwSize + 1);
		if (!GetComputerNameEx(ComputerNameDnsHostname, pwszCompName, &dwSize))
		{	
			wcout<<L"GetComputerNameEx Failed with Error Code"<<GetLastError();
			return false;
		}
	}
	char* test = new char[1024*1024];
	wstrCompName.assign(pwszCompName);
	return true;			
}


bool GetUserName(wstring& wstrUserName)
{
	wchar_t* pwszUserName = NULL;
	DWORD dwSize = 0;

	if(!GetUserName(pwszUserName, &dwSize))
	{
		if(GetLastError() != ERROR_INSUFFICIENT_BUFFER)		
		{
			wcout<<"GetUserName returned with Error"<<GetLastError();
			return false;
		}
		pwszUserName = new wchar_t[dwSize + 1]();
		if(pwszUserName == NULL)
		{
			return false;
		}
		if(!GetUserName(pwszUserName, &dwSize))
		{
			if(pwszUserName)
			{
				delete [] pwszUserName;
				pwszUserName = NULL;
			}
			wcout<<"GetUserName returned with Error"<<GetLastError();
			return false;
		}
	}
	wstrUserName.assign(pwszUserName);
	if(pwszUserName)
	{
		delete [] pwszUserName;
		pwszUserName = NULL;
	}
	return true;
}

步骤 4:在调试模式下重新生成项目

此方法的一个缺点是它要求在调试模式下重新生成代码,否则它无法报告内存泄漏。因此,请在调试模式下重新生成源文件。

步骤 5:启动 DebugView,运行用例并查看输出

启动 DebugView。如果不可用,请从此链接下载:http://technet.microsoft.com/en-in/sysinternals/bb896647.aspx

在这种情况下,在调试模式下构建上述代码,运行应用程序,选择选项 1,然后选择 y。重复此操作三到四次,然后按 n 并退出应用程序。观察 DebugView 输出。它看起来会像这样:

通过这些,我们现在可以确切地知道哪个文件和哪一行代码导致了内存泄漏以及泄漏了多少。当它说 source.cpp 第 73 行时,它实际上是 pwszCompName = new wchar_t[dwSize + 1];。类似地,第 86 行指向 char* test = new char[1024*1024];。现在问题出在哪里非常清楚,我们可以轻松地修复这个问题。

摘要

上述技术是调试内存泄漏的最简单有效的方法之一。CRT 调试 API 中还有许多其他选项,应使用它们来理解其他调试方法。此方法仅限于堆内存泄漏。它不包括句柄泄漏或线程数量的增长。对于这些,我们仍然必须依赖 perfmon 并根据用例追踪代码并修复它们。

参考文献

历史

  • 2014-02-10:文章上传
© . All rights reserved.