65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (27投票s)

2014 年 2 月 15 日

CPOL

5分钟阅读

viewsIcon

38036

调试 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)

调试器现在将像我们通常看到的那样显示对象。

关注点

  1. 调用堆栈:调试 -> 窗口 -> 调用堆栈 (alt+7)
  2. 局部变量:调试 -> 窗口 -> 局部变量 (alt+4)
  3. 反汇编窗口:调试 -> 窗口 -> 反汇编 (alt+8)
  4. 寄存器窗口:调试 -> 窗口 -> 寄存器 (alt+5)
  5. 监视窗口:调试 -> 窗口 -> 监视 -> 监视 1 (ctrl + alt + w, 1)

历史

  • 2014 年 2 月 15 日:初稿
© . All rights reserved.