【问题标题】:gdb in Windows: different behaviour when debugging compiled C and C++ codeWindows 中的 gdb:调试编译的 C 和 C++ 代码时的不同行为
【发布时间】:2013-12-27 01:19:40
【问题描述】:

我注意到 GDB 7.5 在 Windows 上的一个奇怪行为。考虑以下 C 程序:

int foo(void){
    int i = 5;
    return i;
}

int main(int argc, char** argv){
    foo();
    return 0;
}

当编译为经典 C 或 C++ 时,GDB disass foo 命令给出相同的汇编代码,如下所示:

Dump of assembler code for function foo:
0x00401954 <+0>:     push   %ebp
0x00401955 <+1>:     mov    %esp,%ebp
0x00401957 <+3>:     sub    $0x10,%esp
0x0040195a <+6>:     movl   $0x5,-0x4(%ebp)
0x00401961 <+13>:    mov    -0x4(%ebp),%eax
0x00401964 <+16>:    leave
0x00401965 <+17>:    ret
End of assembler dump.

但是,在“leave”命令处插入断点后,如下所示:br *0x00401964,将代码运行到该行,并尝试打印出变量 i,即通过将其编译为 C 和 C++ 生成的可执行文件行为不同。 C 可执行文件按预期工作并打印出 $i = 5,而 C++ 可执行文件 GDB 则表示“当前上下文中没有符号 i”。

所以出于好奇,我想知道这是否是 GDB 错误或功能?或者编译器(GCC)是否做了一些微妙的不同,所以行之间发生了一些事情?谢谢。

编辑: 好吧,我不认为编译器完全删除了该函数,因为在“离开”之前的行中断并打印 i 的值确实有效。

【问题讨论】:

    标签: c++ c x86 gdb


    【解决方案1】:

    这既不是错误/功能,也不是编译器优化的副作用。 反汇编显然是非优化构建的输出(i 写成 到foo+6 中的堆栈,然后在foo+13 中从堆栈重读一步)。

    虽然在这种情况下 C 和 C++ 的汇编输出相同,但调试符号输出却略有不同。 i 的范围在 C++ 中受到更多限制。我只能推测原因。我猜这与 C++ 中的作用域更复杂(想想构造函数、析构函数、异常)这一事实有关,因此 gcc 的 C++ 部分在作用域上比 gcc 的 C 部分更严格。

    详情

    (我在 32 位版本上检查了所有内容,但在使用 gcc 4.8 和 gdb 7.6 的 64 位 Linux 上进行了检查。虽然 Windows 上的一些细节会有所不同,但我希望一般机制是相同的)

    请注意,我的地址不同。

    (gdb) disas foo
    Dump of assembler code for function foo:
       0x080483ed <+0>:     push   %ebp
       0x080483ee <+1>:     mov    %esp,%ebp
       0x080483f0 <+3>:     sub    $0x10,%esp
       0x080483f3 <+6>:     movl   $0x5,-0x4(%ebp)
       0x080483fa <+13>:    mov    -0x4(%ebp),%eax
       0x080483fd <+16>:    leave  
       0x080483fe <+17>:    ret    
    End of assembler dump.
    

    从技术上讲,foo+0foo+1 是函数序言,foo+3foo+13 是函数体,foo+16foo+17 是函数尾声。所以只有foo+3foo+13 代表{} 之间的代码。我会说 C++ 版本更正确的说法是 i 在函数体之前和之后超出范围。

    要查看这实际上是调试符号的问题,您可以使用maintenance print symbols output_file_on_disk 转储 gdb 的调试结构内部。对于 C,它看起来像:

    块 #000,对象位于 0x1847710,0x80483ed..0x804840e 中的 1 个符号/存储桶 int foo();块对象 0x18470d0, 0x80483ed..0x80483ff int main(int, char **);块对象 0x18475d0、0x80483ff..0x804840e 部分 .text 块 #001,对象位于 0x18476a0 下 0x1847710,1 syms/buckets 在 0x80483ed..0x804840e typedef int int; typedef char 字符; block #002,0x18476a0 下 0x18470d0 处的对象,0x80483ed..0x80483ff 中的 1 个 syms/buckets,函数 foo int i;在运行时计算 块 #003,0x18476a0 下 0x18475d0 处的对象,0x80483ff..0x804840e 中的 2 个 syms/buckets,函数 main 整数 argc;在运行时计算 字符 **argv;在运行时计算

    虽然这是 C++

    块 #000,0x1a3c790 处的对象,0x80483ed..0x804840e 中的 1 个符号/存储桶 int foo();块对象 0x1a3c0c0, 0x80483ed..0x80483ff int main(int, char**);块对象 0x1a3c640, 0x80483ff..0x804840e 部分 .text 块 #001,0x1a3c720 下 0x1a3c790 下的对象,0x80483ed..0x804840e 中的 1 个符号/存储桶 typedef int int; typedef char 字符; block #002,0x1a3c0c0 下 0x1a3c720 下的对象,0x80483ed..0x80483ff 中的 0 个 syms/buckets,函数 foo() block #003,0x1a3c050 下 0x1a3c0c0 的对象,0x80483f3..0x80483fd 中的 1 个 syms/buckets int i;在运行时计算 块 #004,0x1a3c640 下 0x1a3c720 下的对象,0x80483ff..0x804840e 中的 2 个符号/存储桶,函数 main(int, char**) 整数 argc;在运行时计算 字符 **argv;在运行时计算

    因此,C++ 代码的调试符号区分了整个函数(块#002)和函数体的范围(块#003)。这会产生您的观察结果。

    (看到这真的不是 gdb 只是处理错误,您甚至可以在 Linux 上使用 objdump 或在 Windows 上使用 dumpbin 分析二进制文件。我在 Linux 上完成了它,实际上它是 DWARF 调试符号不同:-))

    【讨论】:

      【解决方案2】:

      这不是真正的错误或功能。允许编译器替换功能等效的代码,如果它可以找到更好的方法来做事,通常会这样做。示例代码相当于什么都不做,所以编译器可以随意删除它。这样调试器就没有什么可调试的了,这很好,因为调试什么都不做的代码无论如何都是浪费时间。

      【讨论】:

      • 感谢您的回答,但我怀疑编译器完全摆脱了该函数,因为在 leave 指令之前添加断点确实有效
      • 如果disass foo 在C 和C++ 中生成相同的汇编代码,那么这是否意味着foo 没有 被编译掉了?这可能根本不是 gdb 问题。根据 C/C++ 命令行选项,C++ 可能没有生成调试信息。
      • 它可能无法完全编译掉,因为编译器不知道代码不会从另一个编译单元调用。尽管如此,只要代码正常工作,编译器可以随意保留尽可能少的内容。有时在编译专门用于调试的代码时关闭编译器优化会很有帮助。
      猜你喜欢
      • 1970-01-01
      • 2013-01-07
      • 1970-01-01
      • 2020-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多