linux gdb backtrace 怎么实现的

kuaidi.ping-jia.net  作者:佚名   更新日期:2024-07-08
gdb的backtrace,watch命令各有什么含义

本文参考《Debug Hacks》一书所作 —— 每天进步一点点。
一. 准备
通过 gcc 的 -g 选项生成调试信息。
$ gcc -Wall -O2 -g 源文件
-g选项可以使用仅可GDB使用的一些附加调试信息;这些附加信息可以使GDB工作的更好,但是也可能令其他调试器崩溃或者不能读该文件。
二. GDB基本用法
启动:
$ gdb 可执行文件名

设置断点:
(gdb) break 断点
程序运行后,到达断点就会自动暂停运行。此时就可以查看该时刻的变量值,显示栈针,重新设置断点或者重新运行等。
断点可以通过函数名,文件内的行号来设置,也可以先指定文件名再指定行号,还可以指定与暂停位置的偏移量,或者用地址来设置。
(gdb) break 函数名
(gdb) break 行号
(gdb) break 文件名:行号
(gdb) break 文件名:函数名
(gdb) break +偏移量
(gdb) break -偏移量
(gdb) break *地址
在设置断点的时候,如果不指定断点位置,就在下一行代码上设置断点。

条件断点
(gdb) break 断点 if 条件
仅在特定条件下中断。对于已存在的断点,可使用condition为其添加条件。
(gdb) break 断点编号 条件
而删除指定编号断点的触发条件同样使用condition。
(gdb) condition 断点编号

查询断点
(gdb) info break

监视点
要想找到变量在何处被改变,可以使用 watch 命令(监视点, watchpoint)。
(gdb) watch
发生变化时暂停运行。的意思是常量或变量等。
(gdb) awatch
被访问、改变时暂停运行。
(gdb) rwatch
被访问时暂停运行。

删除断点和监视点
用 delete 命令删除断点和监视点。
(gdb) delete

运行
(gdb) run 参数
用 run 命令开始运行。执行run,就会执行到设置了断点的位置后暂停运行。
经常用到的一个操作是在main()上设置断点,然后执行到main()函数暂停。按照上面的内容,操作命令有:
(gdb) break main
(gdb) run
对于执行到 main() 函数暂停的操作,我们可以使用 start 命令达到同样的效果。
(gdb) start

显示栈帧
backtrace 命令可以在遇到断点而暂停执行时显示栈帧。此外,backtrace 的别名还有 where 和 info stack。
(gdb) backtrace
显示所有栈帧。
(gdb) backtrace N
只显示开头 N 个栈帧。
(gdb) backtrace -N
只显示最后 N 个栈帧。
(gdb) backtrace full
(gdb) backtrace full N
(gdb) backtrace full -N
不仅显示backtrace,还有显示局部变量。
显示栈帧之后,就可以看出程序在何处停止(即断点的位置),以及程序的调用路径。

显示变量
print 命令可以显示变量。
(gdb) print 变量

显示寄存器
info registers可以显示寄存器。
(gdb) info registers

单步执行
单步执行的意思是根据源代码一行一行地执行。
执行源代码中一行的命令为 next 。执行时如果遇到函数调用,可能想执行到函数内部,此时可以使用 step 命令。
next 命令和 step 命令都是执行源代码中的一行。如果要逐条执行汇编指令,可以分别使用 nexti 和 stepi 命令。

继续运行
调试时,可以使用 continue 命令继续运行程序。程序会在遇到断点后再次暂停运行。如果没有遇到断点,就会一直运行到结束。
(gdb) continue
(gdb) continue 次数
指定次数可以忽略断点。例如, continue 5 则 5 次遇到断点不停止,第 6 次遇到断点时才暂停执行。

程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小
在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址
注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会导致无法正确解析堆栈内容

一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的。

在glibc头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。

[cpp] view plain copy print?
int backtrace(void **buffer,int size)
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小
在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址
注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会导致无法正确解析堆栈内容

[cpp] view plain copy print?
char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值)

函数返回值是一个指向字符串数组的指针,它的大小同buffer相同.每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和实际的返回地址
现在,只有使用ELF二进制格式的程序才能获取函数名称和偏移地址.在其他系统,只有16进制的返回地址能被获取.另外,你可能需要传递相应的符号给链接器,以能支持函数名功能(比如,在使用GNU ld链接器的系统中,你需要传递(-rdynamic), -rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果你的链接器支持-rdynamic的话,建议将其加上!)

该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针.

注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL

[cpp] view plain copy print?
void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况

下面是glibc中的实例(稍有修改):
[cpp] view plain copy print?
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>

/* Obtain a backtrace and print it to @code{stdout}. */
void print_trace (void)
{
void *array[10];
size_t size;
char **strings;
  size_t i;
 
size = backtrace (array, 10);
strings = backtrace_symbols (array, size);
if (NULL == strings)
{
  perror("backtrace_synbols");
Exit(EXIT_FAILURE);
}

printf ("Obtained %zd stack frames.\n", size);

for (i = 0; i < size; i++)
printf ("%s\n", strings[i]);

free (strings);
  strings = NULL;
}

/* A dummy function to make the backtrace more interesting. */
void dummy_function (void)
{
print_trace ();
}

int main (int argc, char *argv[])
{
dummy_function ();
return 0;
}
输出如下:
[cpp] view plain copy print?
Obtained 4 stack frames.
./execinfo() [0x80484dd]
./execinfo() [0x8048549]
./execinfo() [0x8048556]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x70a113]

我们还可以利用这backtrace来定位段错误位置。
通常情况系,程序发生段错误时系统会发送SIGSEGV信号给程序,缺省处理是退出函数。我们可以使用 signal(SIGSEGV, &your_function);函数来接管SIGSEGV信号的处理,程序在发生段错误后,自动调用我们准备好的函数,从而在那个函数里来获取当前函数调用栈。
举例如下:
[cpp] view plain copy print?
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <execinfo.h>
#include <signal.h>

void dump(int signo)
{
void *buffer[30] = {0};
size_t size;
char **strings = NULL;
size_t i = 0;

size = backtrace(buffer, 30);
fprintf(stdout, "Obtained %zd stack frames.nm\n", size);
strings = backtrace_symbols(buffer, size);
if (strings == NULL)
{
perror("backtrace_symbols.");
exit(EXIT_FAILURE);
}

for (i = 0; i < size; i++)
{
fprintf(stdout, "%s\n", strings[i]);
}
free(strings);
strings = NULL;
exit(0);
}

void func_c()
{
*((volatile char *)0x0) = 0x9999;
}

void func_b()
{
func_c();
}

void func_a()
{
func_b();
}

int main(int argc, const char *argv[])
{
if (signal(SIGSEGV, dump) == SIG_ERR)
perror("can't catch SIGSEGV");
func_a();
return 0;
}

编译程序:
gcc -g -rdynamic test.c -o test; ./test
输出如下:
[cpp] view plain copy print?
Obtained6stackframes.nm
./backstrace_debug(dump+0x45)[0x80487c9]
[0x468400]
./backstrace_debug(func_b+0x8)[0x804888c]
./backstrace_debug(func_a+0x8)[0x8048896]
./backstrace_debug(main+0x33)[0x80488cb]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x129113]
(这里有个疑问: 多次运行的结果是/lib/i368-Linux-gnu/libc.so.6和[0x468400]的返回地址是变化的,但不变的是后三位, 不知道为什么)
接着:
objdump -d test > test.s
在test.s中搜索804888c如下:

[cpp] view plain copy print?
8048884 <func_b>:
8048884: 55 push %ebp
8048885: 89 e5 mov %esp, %ebp
8048887: e8 eb ff ff ff call 8048877 <func_c>
804888c: 5d pop %ebp
804888d: c3 ret
其中80488c时调用(call 8048877)C函数后的地址,虽然并没有直接定位到C函数,通过汇编代码, 基本可以推出是C函数出问题了(pop指令不会导致段错误的)。
我们也可以通过addr2line来查看
[cpp] view plain copy print?
addr2line 0x804888c -e backstrace_debug -f
输出:
[cpp] view plain copy print?
func_b
/home/astrol/c/backstrace_debug.c:57

以下是简单的backtrace原理实现: