使用C++进行Windows开发:使用虚拟变量和格式说明符进行X64调试2010-04-16 MSDN / Kenny Kerr目录处理器体系结构虚拟变量格式说明符可视化调用约定错误代码调试安全环境多年来,Visual C++ 一直包含一组用于调试的虚拟变量和格式说明符。虚拟变量是指这样的内容:可以输入到调试程序监视窗口中,使其显示某种不必与任何 C++ 变量关联的值。遗憾的是,还没有人对虚拟变量进行过详细介绍。我自已掌握的信息尚不足以详细介绍所有虚拟变量,在此仅就我认为最有用的一些虚拟变量和格式说明符与大家共享。我希望此处的讨论可以促使您进一步阅读有关本主题的更多内容。在介绍虚拟变量(有时称为伪寄存器)之前,我需要简要介绍一下处理器体系结构和寄存器,因为了解一点背景知识可以更好地理解虚拟变量的价值。这还有助您从一个侧面理解 64 位版本应用程序与传统 32 位版本应用程序的不同。此专栏的主要内容是 Visual C++,而不是处理器体系结构的细节,所以我将只介绍 x86 和 x64 处理器体系结构。阅读 MSDN 库中的文档,您可以了解更多有关 Itanium 处理器引入的不同之处的内容。处理器体系结构要全面掌握处理器体系结构有一定的难度。这就是 C 语言及其后续语言对软件行业带来如此巨大影响的原因。至少您应该了解 x86 和 x64 处理器体系结构代表什么,因为它们是绝大部分用户使用的处理器类型。Visual C++ 和 Windows 也支持 Itanium 处理器,但是很少有开发人员愿意以这种处理器为目标。不管怎样,在过去的几十年中,源于 8 位 Intel 8080 处理器的 x86 一直在市场上占主导地位。AMD 和 Intel 对 x64 进行了现实的选择,以保留与 x86 的后向兼容性。而 Itanium 引进了一个强大的新体系结构,这种体系结构不受传统的 x86 的限制。当然,这样做的结果是这种体系结构支持的应用程序相当少,但这是 David Cutler(他领导了 Windows NT 开发)及其可移植操作系统观点的实证,这种观点认为 Windows 这些年来已经可以相对轻松地采用新的体系结构。虽然 C++ 编译器后隐藏着处理器体系结构之间的很多不同之处,但开发人员可以清楚地看出这种变化带来的一个好处,即转向单个调用约定。x64 版本的 C++ 编译器只支持单个调用约定,而 x86 版本的编译器支持多个调用约定。这种变化很受欢迎,因为它可以消除由调用约定不匹配引起的多个潜在错误,尤其是在托管代码互操作中。在托管代码互操作中,Microsoft .NET Framework 编译器(如 C# 和 Visual Basic .NET )无法确定 C++ 头文件中的原始调用约定。实际上,它们必须依赖于手动定义的属性,而这些属性很容易被错误定义,从而导致各种堆栈损坏错误。例如,在调用成员函数前,本机 C++ 调用约定 thiscall 使用 ecx 寄存器存储“this”指针。该信息有助于调试,但依赖于调用约定,如果只是查看某些汇编程序代码和寄存器值,调用约定可能并不明显。这种情况在 x64 上就简单得多,因为“this”指针始终作为第一个参数插入,因此存储在 rcx 寄存器中。当然,在调试过程中使用寄存器是特定于处理器的,但令人欣慰的是我们可以探索 x86 和 x64 之间的关系。x86 体系结构引入了子寄存器的概念,子寄存器构成父寄存器的下半部分。例如,ax 是一个 16 位的子寄存器,位于 eax 寄存器的下半部分。x64 体系结构提供了 64 位寄存器来取代 32 位 x86 寄存器,从而实现了寄存器扩展。例如,在 x64 上,32 位 eax 子寄存器构成 64 位 rax 寄存器的后半部分。eax 和 rax 均有各自的作用,分别由 x86 和 x64 用于存储函数返回的指针或整数。毋庸置疑,要使在 x86 上运行的函数返回 64 位值,需要使用两个寄存器。虚拟变量最常见的虚拟变量可能是 $err,该变量显示函数 SetLastError 设置的上一错误值。显示的值代表将由 GetLastError 函数返回的值。虚拟变量还可用于显示处理器寄存器的值。例如,$ecx 显示 x86 体系结构的 ecx 寄存器的值。对于新一代使用 .NET 的开发人员而言,如何查看处理器寄存器的值可能有点高深莫测,但这样做确实可以帮助诊断明显的错误或只了解程序运行时的行为。假设您正在 x86 上使用本机 C++ 调用约定,可以使用 $ecx 显示“this”指针的地址。您还可以在两个体系结构上使用 $eax 显示 32 位返回值。根据您的调试需要,还可以方便地使用更多寄存器。其他有用的虚拟变量包括 $handles 和 $user,前者显示在进程中打开的内核对象的句柄个数,后者显示有关当前进程和线程令牌的十分详细的信息。$user 可用于调试与模拟相关的代码。图 1 列出了一些常见的虚拟变量。

图 1 虚拟变量
虚拟变量 | 说明 |
$handles | 内核对象的句柄个数 |
$vframe | 当前堆栈帧地址 |
$TID | 当前线程标识符 |
$registername | 指定寄存器的内容 |
$clk | 时钟周期表示的时间 |
$user | 进程和线程令牌信息 |