一个演示我的问题的简单示例:
// 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 test -O0 -Wall -g test.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的情况下自行确定此偏移量?
答案 0 :(得分:4)
gdb使用一些方法来决定这些信息。
首先,最好的方法是编译器发出描述函数的DWARF。然后gdb可以解码DWARF以找到序言的结尾。
但是,这并不总是可用。 GCC会发出它,但只有在使用优化时才会发出IIRC。
我相信还有一个约定,即如果在行表中重复函数的第一行号,那么第二个实例的地址将用作序言的结尾。如果线条看起来像:
< function f >
line 23 0xffff0000
line 23 0xffff0010
然后gdb将假设函数f的序言在0xfff0010处完成。
我认为这是gcc在未优化时使用的模式。
最后,gdb有一些序言解码器,它们知道在许多平台上如何编写常见的序言。当debuginfo不可用时使用这些,虽然我不记得它的目的是什么。
答案 1 :(得分:2)
在像linux这样的ELF平台上,调试信息存储在可执行文件的单独(不可执行)部分中。在这个单独的部分中,调试器需要所有信息。检查DWARF2规范以了解详细信息。
答案 2 :(得分:0)
正如其他人所提到的,即使没有调试符号,GDB也有一个函数序言解码器,即启发式魔法。
要禁用它,您可以在函数名称之前添加星号:
break *func
在Binutils 2.25上,跳过算法似乎位于:symtab.c:skip_prologue_sal,breakpoints.c:break_command,命令定义,间接调用。
序言是在函数调用开始时使用的常见“样板”。
foo2
的序言比foo1
的序言要长两个,因为:
sub $0x10,%rsp
foo2
调用另一个函数,因此它不是叶函数。这可以防止一些优化,特别是它必须在另一次调用之前减少rsp
以节省本地状态的空间。
叶子函数不需要,请参阅:Why does the x86-64 GCC function prologue allocate less stack than the local variables?
foo1
但是是一个叶子函数。
lea 0x400644,%rax
由于某种原因,clang将本地字符串常量的地址(存储在.rodata
中)存储在寄存器中,作为函数序言的一部分。
我们知道rax
包含"greetings from foo! i = %i"
,因为它会传递给%rdi
printf
的第一个参数。
foo1
没有本地字符串常量。
序言的其他说明对两个函数都是通用的:
rbp
操纵讨论于:What is the purpose of the EBP frame pointer register?
mov %edi,-0x4(%rbp)
将第一个参数存储在堆栈中。这在叶子函数上不是必需的,但clang无论如何都会这样做。它使寄存器分配更容易。