本文内容
HPHA 内存调试
HPHA 内存分配器提供内存调试功能,可检测和跟踪常见内存问题。
启用 HPHA 内存调试
为避免性能问题,默认情况下调试功能是禁用的。
要启用 HPHA 内存调试
- 在系统分配器源文件 SystemAllocator.cpp 文件中,将 HphaSchema 改为使用 HphaSchemaBase,并将调试选项设置为 true:
m_subAllocator = AZStd::make_unique<HphaSchemaBase<true>>();
保存文件。
在调试模式下执行构建。更多信息,请参阅 构建 O3DE 项目。
特性和限制
由于某些限制,HPHA 调试器可以帮助查找内存问题,但不能保证不存在内存问题。使用 HPHA 内存调试功能时,请注意以下几点:
- 要使 HPHA 调试器工作,分配必须使用 HPHA 分配器。HPHA 内存调试不包括由其他分配器(如
PoolAllocator
)创建的分配。 - HPHA 的大多数内存调试功能都会在检测到内存问题时断言。在可能的情况下,调试器会打印堆栈跟踪,指出分配发生的位置。堆栈跟踪会打印到调试器输出中(而不是日志中),以便 Visual Studio 可以识别。这样就可以双击跟踪,直接导航到相应的文件和行号。
- HPHA 内存调试器目前不包括以下内存问题:
- 缓冲区底溢出。
- “远 “缓冲区溢出。在检测缓冲区溢出时,O3DE 会检测内存块后 16 字节以内的变化。如果缓冲区溢出写入第 17 个字节,O3DE 不会检测到。
- O3DE 通过终止应用程序而不是销毁对象来关闭。由于 O3DE 依赖于操作系统来恢复内存,因此无法检测与关闭或销毁对象相关的问题。为了重现、隔离和调试此类内存问题,我们建议您使用单元测试。
内存调试如何工作
一些内存调试功能会在分配被释放时检测内存问题,另一些则会在 HPHA 分配器被销毁时检测内存问题。内存调试的工作原理是为每次分配保存一组调试记录。请求或返回内存时,调试器会将分配或取消分配操作与调试记录进行比较。一旦发现异常,调试器就会通过断言执行规则。下文将介绍不同内存操作中出现的断言。
启用超限检测
在执行 IAllocator GetDebugConfig() 函数时,可使用 AZ::AllocatorDebug::UsesMemoryGuard 属性打开内存超限检测。
分配
对于内存分配操作,调试器会执行以下任务:
- 如果之前的分配具有相同的指针,调试器会断言并打印之前分配的堆栈跟踪。这种情况通常发生在进程覆盖了分配器跟踪结构的内存时。由于分配器使用的内存靠近其分配的数据块,因此相邻数据块中的内存溢出或内存不足可能会覆盖 HPHA 用于内存跟踪的内存。发生这种情况时,HPHA 可能会将已使用的内存块视为 “未使用”。
- 使用
quiet NaN (qNaN) pattern (
0xFF, 0xC0, 0xC0, 0xFF
) 模式填充内存。这对检测未初始化内存的特定使用模式非常有用,可以检测到大多数(但不是全部)情况。有关 qNaN 模式的更多信息,请参阅 释放内存。
释放内存
对于内存释放内存操作,调试器会执行以下任务:
如果未找到调试记录,则发出断言。出现这种情况的原因可能是重复释放内存或释放内存无效指针。
如果保护无效,则置位。分配时,额外的 16 个字节(“保护”)会放在分配的末尾。例如,如果请求 40 字节,则分配 56 字节,16 字节用于保护。内存调试会为这 16 个字节分配随机值,并将其放入调试记录中。当发生释放内存时,将根据调试记录中存储的 16 个字节检查这 16 个字节。如果两者不匹配,调试器会发出断言。这种断言通常表示内存溢出(即试图写入超出请求大小的内容)。
Note:这种检查无法检测到溢出写入完全相同的随机字节或写入超出 16 字节保护的情况。如果释放的大小与分配的大小不匹配,则置信。在分配过程中,请求的大小存储在调试记录中。如果释放的大小与分配的大小不一致,则说明在释放内存过程中出现了问题。
用 qNaN 模式重新填充释放的内存。这样,在内存被重新分配后,就能更容易地检测内存访问。如果没有这项功能,内存内容通常是可用的,直到某些代码重新使用内存。使用 qNaN 模式填充释放的内存有助于及早发现这种异常使用。
重新分配
重新分配是使用新块还是现有块,取决于是否有连续内存。
重新分配到新区块
当连续内存不可用时,内存会被重新分配到一个新块。调试器会执行以下任务:
- 如果找不到之前的分配,则发出断言。通常情况下,前一个分配仍然存在。分配器会使用新的内存地址创建新的分配,然后将之前分配的内容复制到新的分配中。如果调试记录中没有指向上一个分配的指针,调试器会断言。
- 如果上一次分配的保护无效,则发出断言。有关保护的信息,请参阅 释放内存。
- 断言先前分配的地址是否与新分配的地址相同。更多信息,请参阅 分配。
- 用 qNaN 模式填充新分配的内存。前一个数据块被复制过来。新分配的剩余未使用部分应使用 qNaN 模式。
重新分配给现有区块
当有连续内存时,则使用指向现有内存块的指针。调试器执行以下任务:
- 如果未找到分配,则发出断言。
- 如果上一次分配的保护无效,则发出断言。有关保护的信息,请参阅 释放内存。
- 更新调试记录栈。
- 因为大小发生了变化,所以要写入一个新的 guard。