c语言 - 当您执行"break function-name"时,GDB 如何确定要中断的地址?



演示我的问题的一个简单示例:

// test.c
#include <stdio.h>
int foo1(int i) {
    i = i * 2;
    return i;
}
void foo2(int i) {
    printf("greetings from foo! i = %i", i);
}
int main() {
    int i = 7;
    foo1(i);
    foo2(i);
    return 0;
}

$clang-o测试-O0-Wall-g测试.c

在GDB中,我执行以下操作并开始执行:

(gdb) b foo1  
(gdb) b foo2

到达第一个断点后,我反汇编:

(gdb) disassemble 
Dump of assembler code for function foo1:
   0x0000000000400530 <+0>:     push   %rbp
   0x0000000000400531 <+1>:     mov    %rsp,%rbp
   0x0000000000400534 <+4>:     mov    %edi,-0x4(%rbp)
=> 0x0000000000400537 <+7>:     mov    -0x4(%rbp),%edi
   0x000000000040053a <+10>:    shl    $0x1,%edi
   0x000000000040053d <+13>:    mov    %edi,-0x4(%rbp)
   0x0000000000400540 <+16>:    mov    -0x4(%rbp),%eax
   0x0000000000400543 <+19>:    pop    %rbp
   0x0000000000400544 <+20>:    retq   
End of assembler dump.

我在到达第二个断点后也这样做:

(gdb) disassemble 
Dump of assembler code for function foo2:
   0x0000000000400550 <+0>:     push   %rbp
   0x0000000000400551 <+1>:     mov    %rsp,%rbp
   0x0000000000400554 <+4>:     sub    $0x10,%rsp
   0x0000000000400558 <+8>:     lea    0x400644,%rax
   0x0000000000400560 <+16>:    mov    %edi,-0x4(%rbp)
=> 0x0000000000400563 <+19>:    mov    -0x4(%rbp),%esi
   0x0000000000400566 <+22>:    mov    %rax,%rdi
   0x0000000000400569 <+25>:    mov    $0x0,%al
   0x000000000040056b <+27>:    callq  0x400410 <printf@plt>
   0x0000000000400570 <+32>:    mov    %eax,-0x8(%rbp)
   0x0000000000400573 <+35>:    add    $0x10,%rsp
   0x0000000000400577 <+39>:    pop    %rbp
   0x0000000000400578 <+40>:    retq   
End of assembler dump.

在设置断点时,相对于函数的开头,GDB显然使用了不同的偏移量(foo1中的+7和foo2中的+19)。如何在不使用GDB的情况下自行确定此偏移量?

gdb使用一些方法来决定这些信息。

首先,最好的方法是编译器发出描述函数的DWARF。然后gdb可以解码DWARF以找到序言的结尾。

然而,这并不总是可用的。GCC发出它,但IIRC仅在使用优化时发出。

我相信还有一个约定,如果函数的第一个行号在行表中重复,那么第二个实例的地址将用作序言的结尾。如果线条看起来像:

< function f >
line 23  0xffff0000
line 23  0xffff0010

然后gdb将假定函数f的序言在0xfff0010处完成。

我认为这是gcc在不进行优化时使用的模式。

最后,gdb有一些序言解码器,它们知道在许多平台上如何编写常见的序言。当debuginfo不可用时会使用这些,尽管我不记得它的目的是什么。

正如其他人所提到的,即使没有调试符号,GDB也有序言解码器的功能,即启发式魔术。

要禁用它,您可以在函数名称前添加一个星号:

break *func

在Binutils 2.25上,上的跳过算法似乎位于:symtab.c:skip_prologue_sal,它是断点.c:break_command(命令定义)间接调用的。

序言是在函数调用开始时使用的常见"样板文件"。

foo2的序言比foo1的序言长两条指令,因为:

  • sub $0x10,%rsp

    foo2调用另一个函数,因此它不是叶函数。这阻止了一些优化,特别是它必须在另一个调用之前减少rsp,以为本地状态节省空间。

    由于128字节的ABI红色区域,叶函数不需要这样做,另请参阅:为什么x86-64 GCC函数prolog分配的堆栈比本地变量少?

    CCD_ 6是一个叶函数。

  • lea 0x400644,%rax

    由于某些原因,clang将本地字符串常量的地址(存储在.rodata中)存储在寄存器中,作为函数prolog的一部分。

    我们知道rax包含"greetings from foo! i = %i",因为它随后被传递给%rdi,即printf的第一个自变量。

    然而,foo1没有本地字符串常量。

序言的其他指令对这两个函数都是通用的:

  • rbp操作讨论在:EBP帧指针寄存器的用途是什么?

  • mov %edi,-0x4(%rbp)存储堆栈上的第一个参数。这在叶函数上不是必需的,但clang无论如何都可以做到。它使寄存器分配更容易。

在像linux这样的ELF平台上,调试信息存储在可执行文件的一个单独(不可执行)部分中。在这个单独的部分中,有调试器所需的所有信息。有关详细信息,请查看DWARF2规范。

最新更新