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 参数
单步调试
next
和step
都是单步调试动作,不过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栈一般比较浅,很容易就追查到系统调用,这个时候通用的系统调用怎么看调用关系呢?