CRT 调试技术

如果调试使用 C 运行时库的程序,则这些调试方法可能会非常有用。

CRT 调试库使用

C 运行时库 (CRT) 提供广泛的调试支持。 要使用 CRT 调试库之一,必须链接 /DEBUG,并使用 /MDd/MTd/LDd 进行编译。

CRT 调试的主要定义和宏可在 <crtdbg.h> 头文件中找到。

CRT 调试库中的函数编译时带有调试信息(/Z7、/Zd、/Zi、/ZI(调试信息格式)),不进行优化。 某些函数包含断言以验证传递给它们的参数,并且提供源代码。 使用此类源代码,可以单步执行 CRT 函数,以确认这些函数按预期方式工作并检查错误的参数或内存状态。 (某些 CRT 技术是专有技术,不提供用于异常处理、浮点和少数其他例程的源代码。)

有关可以使用的各种运行时库的详细信息,请参阅 C 运行时库

用于报告的宏

要进行调试,可以使用 <crtdbg.h> 中定义的宏 _RPTn_RPTFn 来替换 printf 语句的使用。 如果未定义 _DEBUG,则不必将它们括在 #ifdef 指令内,因为它们会在发布版本中自动消失。

说明
_RPT0, _RPT1, _RPT2, _RPT3, _RPT4 向四个自变量输出一个消息字符串和零。 对于通过 _RPT4 进行 _RPT1,消息字符串可充当参数的 printf 样式格式设置字符串。
_RPTF0, _RPTF1, _RPTF2, _RPTF3, _RPTF4 虽然与 _RPTn 相同,但这些宏还会输出其所在位置的文件名和行号。

请考虑以下示例:

#ifdef _DEBUG
    if ( someVar > MAX_SOMEVAR )
        printf( "OVERFLOW! In NameOfThisFunc( ),
               someVar=%d, otherVar=%d.\n",
               someVar, otherVar );
#endif

此代码会将 someVarotherVar 的值输出到 stdout。 可以使用以下对 _RPTF2 的调用报告同样的值另加文件名和行号:

if (someVar > MAX_SOMEVAR) _RPTF2(_CRT_WARN, "In NameOfThisFunc( ), someVar= %d, otherVar= %d\n", someVar, otherVar );

一应用程序可能需要调试通过 C 运行时库提供的宏未提供的报告。 对于这些情况,你可以编写专门设计的宏来满足自己的需求。 例如,在其中一个头文件中,可以包含类似于以下内容的代码来定义名为 ALERT_IF2 的宏:

#ifndef _DEBUG                  /* For RELEASE builds */
#define  ALERT_IF2(expr, msg, arg1, arg2)  do {} while (0)
#else                           /* For DEBUG builds   */
#define  ALERT_IF2(expr, msg, arg1, arg2) \
    do { \
        if ((expr) && \
            (1 == _CrtDbgReport(_CRT_ERROR, \
                __FILE__, __LINE__, msg, arg1, arg2))) \
            _CrtDbgBreak( ); \
    } while (0)
#endif

ALERT_IF2 的一次调用可以执行 printf 代码的所有函数:

ALERT_IF2(someVar > MAX_SOMEVAR, "OVERFLOW! In NameOfThisFunc( ),
someVar=%d, otherVar=%d.\n", someVar, otherVar );

你可以轻松更改自定义宏,以便向不同目标报告更多或更少的信息。 随着调试需求不断演变,此方法会特别有用。

编写调试挂钩函数

你可以编写多种类型的自定义调试挂钩函数,以支持你在调试器的正常处理中将代码插入某些预定义的点。

客户端块挂钩函数

如果想要验证或报告存储在 _CLIENT_BLOCK 块中的数据的内容,可以专为此目的编写函数。 根据 <crtdbg.h> 中的定义,所编写的函数必须具有类似于以下内容的原型:

void YourClientDump(void *, size_t)

换句话说,挂钩函数应接受 void 指针(指向分配块的起始),以及一个 size_t 类型值(指示分配大小),并返回 void。 否则,其内容将由你决定。

使用 _CrtSetDumpClient 安装挂钩函数后,每次转储 _CLIENT_BLOCK 块时都将调用该函数。 然后,可以使用 _CrtReportBlockType 获取有关转储块的类型或子类型的信息。

根据 <crtdbg.h> 中的定义,传递给 _CrtSetDumpClient 的函数的指针类型为 _CRT_DUMP_CLIENT

typedef void (__cdecl *_CRT_DUMP_CLIENT)
   (void *, size_t);

分配挂钩函数

每次分配、重新分配或释放内存时,将会调用通过使用 _CrtSetAllocHook 安装的分配挂钩函数。 这种挂钩可用于多种不同用途。 可用它测试应用程序处理内存不足情况的方式,例如检查分配模式,或记录分配信息以供将来分析。

注意

注意有关在分配挂钩函数中使用 C 运行时库函数的限制,详见《分配挂钩和 crt 内存分配》中的说明。

分配挂钩函数应具有类似于以下示例的原型:

int YourAllocHook(int nAllocType, void *pvData,
        size_t nSize, int nBlockUse, long lRequest,
        const unsigned char * szFileName, int nLine )

传递给 _CrtSetAllocHook 的指针的类型为 _CRT_ALLOC_HOOK,如 <crtdbg.h> 中所定义:

typedef int (__cdecl * _CRT_ALLOC_HOOK)
    (int, void *, size_t, int, long, const unsigned char *, int);

当运行时库调用挂钩时,nAllocType 参数会指示即将进行的分配操作(_HOOK_ALLOC_HOOK_REALLOC_HOOK_FREE)。 在释放或重新分配中,pvData 具有一个指向即将释放的块的用户文章的指针。 但是,对于分配,该指针为空,因为分配尚未发生。 其余参数包含分配大小、其块类型、顺序请求编号和指向文件名的指针。 如果可用,参数还包括在其中进行分配的行号。 挂钩函数执行其作者需要的任何分析和其他任务后,必须返回 TRUE(指示分配操作可以继续)或 FALSE(指示操作应会失败)。 此类型的简单挂钩可以检查到目前为止分配的内存量,并在内存量超过一个较小的限制时返回 FALSE。 然后应用程序将会遇到分配错误,这种错误通常只会在可用内存不足时才会发生。 更复杂的挂钩可能会跟踪分配模式,分析内存使用情况,或在特定情况发生时进行报告。

分配挂钩和 CRT 内存分配

对分配挂钩函数的一个重要的限制是,函数必须显式忽略 _CRT_BLOCK 块。 这些块是 C 运行时库函数在内部进行的内存分配的结果(前提是调用分配内部内存的 C 运行时库函数)。 将以下代码添加到分配挂钩函数开始处即可忽略 _CRT_BLOCK 块:

if ( nBlockUse == _CRT_BLOCK )
    return( TRUE );

如果分配挂钩不忽略 _CRT_BLOCK 块,则在挂钩中调用的任何 C 运行时库函数都可能会使程序陷入无限循环。 例如,printf 执行内部分配。 如果挂钩代码调用 printf,则生成的分配会导致再次调用挂钩,而这会再次调用 printf,以此类推,直到堆栈溢出。 如果需要报告 _CRT_BLOCK 分配操作,则避免该限制的一种方法是使用 Windows API 函数而不是 C 运行时函数进行格式化和输出。 Windows API 不使用 C 运行时库堆,因此不会让分配挂钩陷入无限循环。

如果检查运行时库源文件,将会看到默认分配挂钩函数 _CrtDefaultAllocHook(它仅返回 TRUE)位于其自己的单独文件 debug_heap_hook.cpp 中。 如果希望调用分配挂钩(即使分配是由在应用程序的 main 函数之前执行的运行时启动代码执行的),则可将此默认函数替换为你自己的一个函数,而不是使用 _CrtSetAllocHook

报表挂钩函数

每次 _CrtDbgReport 生成调试报告时,将会调用使用 _CrtSetReportHook 安装的报告挂钩函数。 可以使用报告挂钩函数以及其他项筛选报告以集中于特定类型的分配。 报告挂钩函数应具有类似以下示例的原型:

int AppReportHook(int nRptType, char *szMsg, int *retVal);

传递给 _CrtSetReportHook 的指针的类型为 _CRT_REPORT_HOOK,如 <crtdbg.h> 中定义:

typedef int (__cdecl *_CRT_REPORT_HOOK)(int, char *, int *);

当运行时库调用挂钩函数时,nRptType 参数包含报告类别(_CRT_WARN_CRT_ERROR_CRT_ASSERT),szMsg 包含指向完全汇编的报告消息字符串的指针,而 retVal 则指定 _CrtDbgReport 是应在生成报告后继续正常执行还是启动调试器。 (retVal 值为零会继续执行,值为 1 则启动调试器。)

如果挂钩完全处理了所讨论的消息,因而不需要进一步的报告,则应返回 TRUE。 如果返回 FALSE_CrtDbgReport 将以正常方式报告消息。

本节内容

  • 堆分配函数的调试版本

    讨论堆分配函数的特殊调试版本,包括:CRT 如何映射调用、显式调用它们的好处、如何避免转换、跟踪客户端块中单独的分配类型和不定义 _DEBUG 的结果。

  • CRT 调试堆详细信息

    介绍内存管理和调试堆、调试堆上的块类型、堆状态报告函数,以及如何使用调试堆跟踪分配请求。

  • 使用 CRT 库查找内存泄漏

    介绍有关使用调试器和 C 运行库检测和隔离内存泄漏的方法。

另请参阅