C++ 发布项目调试 - 查找丢失的对象信息






4.92/5 (27投票s)
调试 C++ 发布项目。查找丢失的对象信息
引言
在发布模式下编译 C++ 项目并进行优化时,调试器有时无法显示正确的对象信息。
局部变量通常是最先消失的,有时,this
对象的相关信息也会丢失给调试器。
原因是编译器使用可用的硬件寄存器来保存信息,并利用优化来避免分配局部变量。
在本技巧中,我将展示如何使用 Visual Studio 的工具来查看信息保存在何处。
Using the Code
我将举一个代码抛出异常的简单例子。
我将创建一个简单的类和一个导致除零异常的成员值的情况。
我将添加一个额外的函数,以便调试器能够停在正确的行(由于优化,调试器可能不会停在正确的行,因为实际的代码堆栈并不代表您在代码中的位置——只代表返回地址。这个问题可能在未来的帖子中解释)。
这个额外的函数对于这种情况很重要,以便我们能够模仿我所说的情况。
class SomeClass
{
public:
int a,b;
void MyMethod()
{
int i = 0;
int j = i;
DoStuff();
a = this->a/this->b;
DoStuff();
DoStuff();
}
void DoStuff()
{
int i = 10;
int j = 5;
int k = 4;
for ( ;i * j + k < 40000; i++)
{
int fs = 34;
}
printf("%d %d %d %d %d", i , j , k , 10,8);
}
};
int main(int argc, void* argv[])
{
SomeClass a;
a.a = 5;
a.b = 0;
a.MyMethod();
return 0;
}
现在,如果您在发布模式下运行这个小程序,您将收到一个关于除零错误的异常。这是因为成员 b
被设置为 0
。
调试器将方便地停在正确的行,但不幸的是,所有局部变量和 this
变量都将损坏。
信息显然存在于某处,但调试器似乎没有加载正确的信息。如果仔细观察,您可以看到 this
的值是 01
,这表明它只是从错误的位置/地址读取了对象的信息。
那么,我们如何找到对象正确的地址呢?
使用调用堆栈
第一种方法,也是最简单的方法,就是向上查看调用堆栈,希望在某个层级上调试器能看到正确的地址。这比下面的方法要容易得多……
如果我们查看调用堆栈,我们可以看到调试器正确地解析出我们位于 SomeClass
类的 MyMethod
方法中。
因此,如果我们转到堆栈中的上一个函数(main
),调试器可能仍然持有对象的正确地址(因为该对象在那里被已知并使用)。只要编译器没有决定重用保存对象地址的寄存器,这种情况就会奏效。
所以,如果我们双击当前位置下方的一行,调试器将跳转到当前方法被调用的代码。
在这里,我们可以清楚地看到 b
等于 0
,而我们正在除以 b
,所以谜团解决了——这就是我们收到异常的原因!
但是……在更深的嵌套调用堆栈和更复杂的代码中,这种情况并不总是有效,并且调试器可能会在整个调用堆栈层次结构中使用该信息。
寄存器值和反汇编
关键在于理解 CPU 内部的信息存储在它的寄存器中。尽管实际信息可能存储在内存(堆栈或堆)中,但 CPU 本身只使用寄存器。
编译器会添加汇编代码,将相关信息从内存复制到寄存器中,供 CPU 进行实际计算。
了解这一点后,我们可以假设大部分局部使用的变量和 this
地址将存储在寄存器中(除非它们不再被使用,在这种情况下,寄存器可能会被重用以保存其他信息)。
要了解变量如何精确地映射到寄存器,我们可以使用反汇编窗口和寄存器窗口。
我不会深入研究汇编代码,我只会查看寄存器名称。
让我们看看我们出问题的代码的反汇编窗口。我们想找到一个代码调用当前对象所需的一个局部变量的地方,匹配的汇编代码可以指出它正在使用的寄存器。
我们可以看到,当我们调用局部成员时,匹配的汇编代码调用了一个名为 rdi
的寄存器。
这个寄存器可能保存着我们对象的地址(对象成员通过相对于内存中对象的起始点(地址)的偏移量来访问)。
我们现在想看看这个寄存器保存的值。我们希望它是一个地址(一个很大的数字)。如果我们打开寄存器窗口,我们可以看到它的值。
我们可以看到 rdi
保存的值是 000000000025F950
,这是一个十六进制值,相当大,这看起来像一个内存地址。(较低的值通常代表计数器或其他局部函数成员。)
当然,这对我和几乎所有人来说都没有什么意义,所以让我们让调试器来帮助我们弄清楚。
现在我们可以打开监视窗口。这是一个有用的窗口,其中包含许多功能,允许您输入特定的地址并显示它们的值。
我们将地址转换为我们对象的类型指针,并在数字前添加 0x
前缀,以便调试器知道这是一个十六进制值。
(SomeClass*)(0x000000000025F950)
或者,您也可以直接转换寄存器本身。
(SomeClass*)(rdi)
调试器现在将像我们通常看到的那样显示对象。
关注点
- 调用堆栈:调试 -> 窗口 -> 调用堆栈 (alt+7)
- 局部变量:调试 -> 窗口 -> 局部变量 (alt+4)
- 反汇编窗口:调试 -> 窗口 -> 反汇编 (alt+8)
- 寄存器窗口:调试 -> 窗口 -> 寄存器 (alt+5)
- 监视窗口:调试 -> 窗口 -> 监视 -> 监视 1 (ctrl + alt + w, 1)
历史
- 2014 年 2 月 15 日:初稿