当您执行“中断函数名称”时,GDB 如何确定要中断的地址?

Fma*_*mag 4 c debugging stack 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;
}
Run Code Online (Sandbox Code Playgroud)

$ clang -o test -O0 -Wall -g test.c

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

(gdb) b foo1  
(gdb) b foo2
Run Code Online (Sandbox Code Playgroud)

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

(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.
Run Code Online (Sandbox Code Playgroud)

到达第二个断点后,我做同样的事情:

(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.
Run Code Online (Sandbox Code Playgroud)

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

Tom*_*mey 5

gdb 使用几种方法来确定这些信息。

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

但是,这并不总是可用。GCC 会发出它,但只有在使用优化时才会发出 IIRC。

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

< function f >
line 23  0xffff0000
line 23  0xffff0010
Run Code Online (Sandbox Code Playgroud)

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

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

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


Cir*_*四事件 5

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

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

break *func
Run Code Online (Sandbox Code Playgroud)

在 Binutils 2.25 上,跳过算法似乎位于:symtab.c:skip_prologue_sal,其中命令定义Breakpoints.c: break_command 间接调用。

序言是函数调用开始时使用的常见“样板”。

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

  • sub $0x10,%rsp

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

    由于 128 字节 ABI 红色区域,叶函数不需要这样做,另请参阅:Why does the x86-64 GCC function prologue allocate less stack than the local Variables?

    foo1然而是一个叶函数。

  • lea 0x400644,%rax

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

    我们知道raxcontains"greetings from foo! i = %i"因为它随后被传递给%rdi的第一个参数printf

    foo1但是没有本地字符串常量。

序言的其他指令对于这两个函数来说是相同的:

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

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