CRC 哈希值的调试器支持






4.36/5 (6投票s)
在调试器中使用表达式计算器将 CRC 散列值转换为字符串。
引言
应用程序经常需要间接引用对象。这些对象可以是游戏引擎中的纹理、网格或其他资源,CAD 系统中的插件,数据库中的记录,GUI 系统中的控件等等。这是通过为每个对象分配一个唯一 ID 来完成的。
一种方法是使用文本字符串作为 ID。使用文本字符串的最大优点是它们可以很容易地被我们读取——无论是在源代码中还是在调试过程中。然而,它们有许多问题。字符串的长度不同。为了在结构或类中拥有这样的 ID,您要么需要一个足够大的缓冲区来存储最长的字符串,要么需要从堆中分配字符串——这两种方法都有其缺点。比较两个字符串是一个代价高昂的操作。如果您通过名称在数据结构中搜索对象,比较键是花费时间最多的操作。
您可以使用字符串的哈希值而不是文本字符串。相同的字符串会产生相同的哈希值,而不同的字符串通常会产生不同的值。CRC32 是最常用的哈希值之一。它是一个 32 位值,非常适合任何结构。32 位值很容易存储在寄存器中。比较两个 CRC 也很快。有关字符串与 CRC 的更多信息,请查看 [1]。
在 Pandemic Studios(我的工作单位)中,我们多年来一直在我们的游戏中使用 CRC。我们发现它们是一个非常通用的工具。我们将它们用作资源名称、对象名称、文件名称、用于识别 AI 脚本以及许多其他目的。CRC 的主要问题是在调试期间您只能看到一个纯数字。如果您可以在调试器中查看原始字符串,那不是很好吗?
让我们首先从一个简单的 CRC 类实现开始。
CRC 类实现
所呈现的 CRC
类非常简单。您可以从字符串或直接数字值构造 CRC
对象,可以使用复制构造函数,还可以比较两个 CRC
对象。它还有一个 Append
方法,允许您向 CRC
对象添加更多字符。
CRC crc1("Hello World");
CRC crc2("Hello");
crc2.Append(" World"); // Now crc1 and crc2 have the same value
一旦定义了 CRC_STRINGS
,CRC
类就会维护一个全局树结构(一个 std::map
),其中包含应用程序中的所有字符串。当从字符串构造新的 CRC
对象时,数据将添加到树中。这样,树结构在应用程序运行期间会不断增长。每个新的唯一字符串都会添加到其中,并且不会删除任何内容。您可以使用 GetStr
函数从 CRC
值中查找字符串。
#ifdef CRC_STRINGS
// Resolves the CRC to a string
const char *CRC::GetStr( void ) const
{
// default string if the CRC is not found in the map
static const char *null="NULL";
CCRCMap::const_iterator it=s_CRCMap.find(m_Crc);
if (it!=s_CRCMap.end())
return it->second;
else
return null;
}
#endif
这是它的用法
printf(crc2.GetStr()) => Hello World
现在来了棘手的问题...
在调试期间如何实现呢?
首先尝试在监视窗口中直接调用 GetStr()
在调试期间,您可以在监视窗口中键入 crc1.GetStr()
以获取字符串。这不是很方便,因为您必须为要检查的每个 CRC 对象键入 GetStr()
。由于此方法在被调试进程内部执行代码,因此可能会产生意外的副作用。因此,在检查事后崩溃转储时,它不起作用。
使用 Autoexp.dat 进行改进
Visual Studio 通过文件 *Autoexp.dat* 支持用户定义的规则来显示自定义类型。如果您添加
CRC=<GetStr()>
在 [AutoExpand] 部分的末尾,调试器将在显示 CRC
对象时调用 GetStr
函数。乍一看,对于上面的简单情况,一切似乎都正常。但是,在更复杂的情况下它会失败。让我们来看一个包含 CRC
成员的结构
struct A
{
CRC a1;
CRC a2;
} a;
a.a1=CRC("aaa");
a.a2=CRC("bbb");
如果您将对象 a
放入监视窗口,您将得到(Visual Studio 2003 和 Visual Studio 2005)
a.a1
作为单独的表达式可以正确求值,但当 a
展开时则不能。奇怪的是,在 VC6 中它工作正常。
使用这种方法,您还有一个缺点,即在被调试进程中执行代码,并且它不适用于事后调试。
目前最好的解决方案:自定义表达式计算器 DLL
表达式计算器 (EE) 是一个 DLL,它通过支持新类型来扩展 Visual Studio 调试器。有关详细信息,请查看 [2]。计算器必须检索 32 位 CRC 值,找到字符串树,找到给定值的节点并返回字符串。不幸的是,Visual Studio 中的 EE 系统非常有限。它只允许您通过 ReadDebuggeeMemory
函数从给定地址检索数据。那么,如何找到字符串树呢?这就是 VSHelper 插件的作用,请查看 [3]。
查看 CRCTest 源代码中的文件 *DebugData.cpp*。它创建一个名为 g_DebugData
的 4K 缓冲区。该缓冲区包含成对的 INT_PTR
,第一个是标识符,第二个是指向某个数据结构的指针。在我们的例子中,第一个是 'CRCV',第二个是指向字符串树头节点的指针。最后一对的终止标识符为 0。
当调试器进入中断模式时,无论是命中断点、按下 Ctrl+Break 还是进行单步调试,它都会触发 OnEnterBreakMode
事件。VSHelper 插件捕获该事件并计算 g_DebugData
缓冲区的地址。EE DLL 可以与 VSHelper DLL 通信并获取该值。
CRCView DLL 就是这样工作的。一旦它获得字符串树的头节点,其余的就很容易了。它遍历二叉树,直到找到正确的键并返回字符串。要激活计算器,请将其添加到 *Autoexp.dat* 中
CRC=$ADDIN(<path to the DLL>\CRCView.dll,CRCView)
此解决方案适用于所有情况——对于结构成员、调试崩溃转储、调试工具提示,甚至远程调试都非常有效。
在 Pandemic Studios,我们使用类似的插件已经好几年了,没有出现任何问题。
关于大端序呢?
使用 Visual Studio,您可以调试在不同 CPU 类型上运行的远程系统。它可以是嵌入式系统、手机或游戏机。有时远程机器可能是大端序。最新版本的 CRCView.dll 将通过搜索“CRCV”和“VCRC”标识符来检测到这一点。如果找到“CRCV”,则目标是小端序。如果找到“VCRC”,则目标是大端序,并且评估器将交换从调试器获取的所有值的字节顺序。
void BSwap( DWORD *data )
{
__asm
{
mov esi,data
mov eax,[esi]
bswap eax
mov [esi],eax
}
}
扩展 DebugData 系统
g_DebugData
缓冲区支持 511 对数据。您可以通过调用 AddDebugData
注册自己的数据。给它一个唯一的 FourCC 标识符和指向您数据的指针。然后,编写您自己的 EE DLL,该 DLL 搜索该 FourCC 标识符。有关如何执行此操作的示例,请查看 CRCView 项目中的 FindDebugData
函数。
关于 VC6 呢?
VC6 不支持 OnEnterBreakMode
事件。我能找到的唯一解决方案是将 g_DebugData
数组放在一个固定地址(例如 0x3FFF0000)。当我测试时它工作正常,但我不确定该地址是否始终可用。
安装与使用
首先下载并安装 VSHelper 插件 [3]。然后,下载 *CRCView.zip*。在 *CRCView\Release* 文件夹中,您将找到 *CRCView.dll*。然后将其添加到 *Autoexp.dat* 的 [AutoExpand] 部分中。
CRC=$ADDIN(<path to the DLL>\CRCView.dll,CRCView) <- notice there is no space
between , and CRCView
在 VS 2003 和 2005 中,*Autoexp.dat* 文件位于 *
最后一步是将 CRC
类包含到您自己的项目中。只需从 *CRCTest* 文件夹复制文件 *CRC.cpp*/*h* 和 *DebugData.cpp*/*h*。在项目设置中定义 CRC_STRINGS
。如果您不定义它,字符串树将被禁用,并且 GetStr
将不可用。评估器也将无法工作。您可能希望在调试版本中使用 CRC_STRINGS
,并在发布版本中禁用它以节省内存。
故障排除提示:如果 CRC 未正确显示怎么办?
有时您在调试器中看到的不是正确的文本,而是 {m_Crc=<某个数字>} 或 {???}。如果您看到 {m_Crc=<某个数字>},则 Autoexp.dat 未正确修改。可能是
- CRC=$ADDIN... 行未添加到 [AutoExpand] 部分。如果您将该行添加在末尾,则很可能它位于 [hresult] 部分内。
- 也许您将 CRC 类放在了某个命名空间中。您必须在 Autoexp.dat 中使用完整的类名。
- 可能有多个 Autoexp.dat 文件。例如,嵌入式系统在 Visual Studio 中使用备用调试器并拥有自己的 Autoexp.dat 文件。
如果您在调试器中看到 {???},则表示在 Autoexp.dat 中找到了 CRC 类,但存在另一个问题。可能是
- Autoexp.dat 中有拼写错误(逗号和“CRCView”之间不能有空格 - 见上文)
- 未找到 CRCView.dll(检查 Autoexp.dat 中列出的路径是否正确)
- CRCView.dll 未导出
CRCView
函数,或以修饰名导出 - 如果您自己编译 DLL 并且忘记在链接器设置中包含 DEF 文件,则可能发生这种情况。使用“dumpbin /exports CRCView.dll”验证导出了哪些符号 - CRCView 函数返回错误(不再是这种情况,见下文)
最新版本的 CRCView.dll 附带了一些故障排除功能。它从不返回错误。相反,如果检测到错误,它会将错误消息放入输出文本并返回 S_OK。可能的错误消息是
- 无法访问 CRC 值 -
ReadDebuggeeMemory
失败。很可能 CRC 值的地址无效 - VSHelper 被禁用 - 评估器检测到 VSHelper 插件,但它没有提供有效的
g_DebugData
值。很可能该功能已被禁用。检查 VSHelper 设置 [3]。 - 找不到 'CRCV' 数据 - 未找到
g_DebugData
。很可能您的项目设置中未定义CRC_STRINGS
。 - 无法访问 CRC 表 - 找到
g_DebugData
,但它指向的字符串表(std::map)已损坏。 - 无可用文本 - CRC 不在字符串表中。如果您直接从数字值创建 CRC 对象(例如 CRC crc(10);),则可能会发生这种情况。
- 未能检索文本 -
ReadDebuggeeMemory
未能从字符串表中访问文本。最有可能的是表已损坏。
许可
源代码、二进制文件和本文档归 Pandemic Studios 所有。它们可以在 MIT 许可证 的条款下自由用于商业和非商业目的。许可证的副本包含在 CRCView.zip 中的 readme.rtf 文件中。
未来发展
目前,字符串数据库、map 节点和字符串本身的全部内存都是从 CRT 堆中分配的。这个数据结构的大小会不断增长,并在关闭时释放。更优化的方法是消除 CRT 堆的这种负载,并使用某种针对这种行为优化的自定义分配器。一种方法是从堆请求大块内存,并在其中进行多次顺序分配。另一种方法是使用 VirtualAlloc 保留大块地址空间,并根据需要增加物理分配的页面数量。将 *CRCView.dll* 添加到 VSHelper 安装程序中会很好。安装程序还必须在 *Autoexp.dat* 文件中注册 DLL。
DebugData 系统为 EE 插件提供了一种从应用程序访问任意数据的方法。也许有人可以想出这种功能的其他酷炫用途。
特别感谢
特别感谢 Pandemic Studios 和以首席程序员 Alex Boczar 为首的 Full Spectrum Warrior 工程团队。
链接
[1] 实用哈希 ID 作者 Mick West,Game Developer Magazine,2005 年 12 月[3] VSHelper - Visual Studio IDE 增强功能
历史
- 2006 年 1 月:第一个版本
- 带 Visual Studio 表达式评估器的简单 CRC32 实现
- 2006 年 10 月:新功能
- 支持大端目标
- 故障排除功能
- 2007 年 2 月:根据 MIT 许可证发布