gdb调试

Posted by faxiang1230 on July 16, 2018

gdb调试

Fistly,you should read gdb cheatsheet

工程中有gdb工具来帮助调试,常见的使用场景:

  • 1.看代码逻辑,例如断点,bt等来看代码的调用路径
  • 2.看堆栈,例如程序hang住了,或者已经crash生成core文件
  • 3.看内存,寄存器,变量

其他的例如随机crash使用gdb都是比较无力的,gdb只能适用于一些固定代码逻辑上出问题的点
对于多线程中出现的复杂问题,特别是内存的相互践踏,可以使用addresssantizer.
对于多进程中的问题需要开多个gdb来调试
对于内核层的,对应的有kdb,不过很少在工程中使用这个工具,基本上是通过其他工具来调试,例如 内核本身的工具,调试死锁的lockdep,spin lock debug等,还有crash,dump dmesg等方法,在 不同的平台上和不同的厂家上都是有不同的形式。

gdb通用准备

gdb是一个基于符号调试的工具,类似的工具还有crash,objdump,addr2line,systemtap,perf等 工具,这类工具都是需要查询调试符号表,然后才能解析出可读的内容;
现在经常使用的dwarf(debug with attributed record file)格式的调试信息,当使用-g时会 生成.debug*等节,里面保存了调试信息,可以通过dwarfdump来看调试信息。如果调试的时候看不到 源码就使用这个工具查看调试信息。
调试信息是什么?它记录了符号和源文件中对应的信息,只是类似于符号的东西,没有把整个源文件copy 进来,所以调试的时候仍然需要源码,可以使用directory /path来设置新的源码路径。

常用的操作:

app类:编译时-g-fno-omit-frame-pointer;-g大家都知道,而-fno-omit-frame-pointer这项特性是现代编译器不再使用ESP来保存指向栈帧基地址了,DWARF解栈帧时不是很管用,所以禁止忽略栈帧基地址.
另外一个特性是现代的编译器优化级别特别高,常常会将某些局部变量优化掉,在检查变量环节有可能就度不到正确的值,所以尽量带-O0来避免优化

kernel:本身会生成一个vmlinux文件,原始的带有符号的静态链接elf文件,使用gdb调试的时候就用这个

而另外一些时候有些项目运行不带符号的程序,而将带有符号信息的可执行文件放在另外一个位置,那么需要手动加载符号文件位置:file 带符号文件
可执行程序一般都是动态链接的,执行到动态库的时候也是需要手动加载动态库的符号文件:set solib-search-path 带有符号的动态库目录
另外的C/C++程序遇到错误的时候,我们通常希望能够获得一个coredump来进行debug,coredump是一个运行程序死亡时最后的内存快照,请检查是否可以获取coredump:

#ulimit -­c
unlimited  //core dump的最大值,如果是0,就是disable core dump;执行ulimit -c unlimited
#cat /proc/sys/kernel/core_pattern
core  //当前目录下生成core文件,更多定制参考kernel/Documentation/sysctl/kernel.txt
# gdb -c core

tui模式

gdb参数--tui或者进入之后Ctrl+X+A;再次重复命令Ctrl+X+A就会离开tui(text ui)模式
split layout可以分成两栏:源码+汇编

传参

进入gdb之后run 参数

单步调试

nextstep都是单步调试动作,不过step会进入函数里面,而next会跳到下一行而不会调入函 数里面

恢复执行操作continue

上面三个都可以加上n:

next n,继续下面n行

step n继续stepping into模式n行

continue n继续下去,忽略n个断点

finish:恢复执行直到当前函数返回为止 until:恢复执行直到当前循环体外的下一行程序,就是结束当前for,while循环;实际上until也可以接受break一样的参数:until 16在当前活动源文件侠16行停止

断点

断点添加的基本命令:break,可以针对文件行数,函数名来进行断点

break test.c:16
break main
break 16当前活动源代码文件中的行号

C++中的断点,除了可以使用文件行之外在指定函数处断点命名空间::类::方法

  • 条件断点

有时候发生了偶然出现的bug,不想每次都断住然后continue来,可以使用条件断点break bug() if (condition)condition n if (condition) condition可以有一下几种形式:

相等,逻辑,不等运算符
`==,<,>,>=,<=,||,&&,!=`

换位,移位运算符
`|,&,<<,>>`

算术运算符
`+,-,*,/,%`

自己的函数,只要gdb能够找到
`break test.c:16 if (parse())`

标准库里的函数
`break test.c:main if(size(argv))`

查看断点:info b
删除断点:delete breakpoint n
禁用断点disable n
启用断点enable n

  • 在特定线程中中断

你可以定义你的断点是否在所有的线程上,或是在某个特定的线程

    break <linespec> thread <threadno>
    break <linespec> thread <threadno> if ...

linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID,注意,这个ID是GDB分配的, 你可以通过”info threads”命令来查看正在运行程序中的线程信息。如果你不指定thread 则表示你的断点设在所有线程上面。你还可以为某线程指定断点条件。如:

     (gdb) break frik.c:13 thread 28 if bartab > lim

当你的程序被GDB停住时,所有的运行线程都会被停住。这方便你你查看运行程序的总体情况。 而在你恢复程序运行时,所有的线程也会被恢复运行。那怕是主进程在被单步调试时。

断点命令列表

commands n
>......
>continue
>end

检查变量

最简单的打印出某个局部变量或全局变量的值print local 打印结构体:

在这之前可以先设定格式化输出:
(gdb) set print pretty on
(gdb) p *node_data
$2 = (struct pglist_data *) 0xffff880007e81000
(gdb) p **node_data

打印结构体类型:

(gdb) ptype struct pglist_data
type = struct pglist_data {
    struct zone node_zones[4];
    struct zonelist node_zonelists[2];
    int nr_zones;
    unsigned long node_start_pfn;
    unsigned long node_present_pages;
    unsigned long node_spanned_pages;
    int node_id;
    nodemask_t reclaim_nodes;
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd;
    int kswapd_max_order;
    enum zone_type classzone_idx;
}

打印结构体偏移:

(gdb) p &((struct task_struct *)0)->prio
$1 = (int *) 0x30
(gdb) p &((struct task_struct *)0)->state
$2 = (volatile long int *) 0x0

打印结构体的size:

(gdb) p sizeof(struct page)

线程操作

查看当前线程info threads
切换当前调试的线程为指定ID的线程:thread ID
在所有线程中相应的行上设置断点:break thread_test.c:123 thread all
让一个或者多个线程执行GDB命令command:thread apply ID1 ID2 command
让所有被调试线程执行GDB命令command:thread apply all command
set scheduler-locking off|on|step 估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

监视点

在变量读、写或变化时中断,这类方式常用来定位bug。

    watch <expr>    变量发生变化时中断
    rwatch <expr>    变量被读时中断
    awatch <expr>     变量值被读或被写时中断

可以通过info watchpoints [n]命令查看当前观察点信息

遍历栈帧

最简单的命令backtrace就可以打印出当前栈帧的回溯
也可以通过frame来指定某个帧,当前帧为0,父帧为1,祖父帧为2,类推,通过frame n回到某个帧上;但是不改变当前的执行点;

远程调试

例如在debug curses/qemu虚拟机里运行kernel,远程机器上的程序的时候,你不能在直接用gdb来调试了,往往是需要另外的设置.

调试qemu中的kernel时,通过tcp协议来通信
调试curses时需要通过/dev/pts/x来调试

help target remote
help tty

设置通信方式的方法:

(gdb) target remote:1234
(gdb) set inferior-tty /dev/pts/6
(gdb) target remote:1234

通过coredump来定位bug原因

  • 1.获取core dump并载入gdb

获取可执行文件对应的带符号表文件并参考gdb通用准备,在ubuntu中可以通过apt-get install xxx-dbg来装载符号表文件而不必下载源码重新编译

  • 2.查看bt到达崩溃地址的backtrace

看崩溃的最后地址以及崩溃数据的由来

  • 3.disas查看更加寄存器级别崩溃原因

一般会有小的提示来看崩溃的寄存器,一般都是访问非法地址;然后对照源码,查看究竟是哪个地址错误
这一步非常重要,是继续追查数据由来的起始点,如果这一步追查错误将会浪费大量的时间

  • 4.追查非法数据的由来,最终分析出谁调用的?

对照源码和bt来追查这一步是在哪一步错误的?可能的原因是什么?追查的深度越深越接近root真相.
example

内核crash工具分析也是这个流程,不过也有部分不同:
1.kernel管理着所有的硬件,而有时候是硬件原因导致的错误,查不到root原因,只能靠一些外围的证据来佐证自己的猜测.
2.kernel栈一般比较浅,很容易就追查到系统调用,这个时候通用的系统调用怎么看调用关系呢?