GDB调试方法

GDB最详细的文档请参考GDB Documentation。其中给出的关于GDB的简介非常精炼:

The purpose of a debugger such as GDB is to allow you to see what is going on “inside” another program while it executes—or what another program was doing at the moment it crashed.
GDB can do four main kinds of things (plus other things in support of these) to help you catch bugs in the act:

  • Start your program, specifying anything that might affect its behavior.
  • Make your program stop on specified conditions.
  • Examine what has happened, when your program has stopped.
  • Change things in your program, so you can experiment with correcting the effects of one bug and go on to learn about another.

使用GDB进行调试时,为了查看完整的符号信息,在编译程序时需要加上选项-g -O0,以保留调试符号信息。比如编译 redis 时,我们需要在make时如下操作:

make CFLAGS="-g -O0"

如果是C++的程序则使用编译器选项CXXFLAGS

启动GDB调试

有三种方法启动GDB调试:

  • gdb elf-file —— 直接调试目标程序
  • gdb -p pid —— 附加到某个正在执行的进程
  • gdb elf-file core —— 调试coredump产生的core文件

当我们想要调试某个正在运行的进程而不想重新启动它以致于实时数据丢失,则可以将调试器附加(attach)到这个进程。比如我们的系统上有redis服务器正在运行,进程号时31055:

ps -ef | grep redis
book      31055   2554  0 03:41 ?        00:00:00 redis-server *:6379

那么通过指令sudo gdb -p 31055就可以attach到redis进程:

......
Attaching to process 31055
[New LWP 31056]
[New LWP 31057]
[New LWP 31058]
[New LWP 31059]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f53c3d9fd67 in epoll_wait (epfd=5, events=0x7f53c3925b40, 
    maxevents=4192, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30	../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.

此时调试器会将进程暂停,待我们完成调试后,可以输入detach指令使进程继续正常执行。

coredump的设置与调试

如果程序崩溃时产生了核心转储文件“core”(比如出现段错误、栈溢出),则会使相关问题的调试工作顺利许多。但首先我们需要通过ulimit -a指令来查看当前Linux环境是否允许产生core文件:

$ ulimit -a
core file size          (blocks, -c) unlimited		# <----- 不能是0,最好为 unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7641
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7641
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

一般来说系统默认core file size为0,我们可以直接通过指令ulimit -c unlimited将其设置为无限大,因为一个复杂程序的core文件往往都是比较大的。

除此之外,core 文件默认出现在可执行文件所在的目录,我们也可以手动设置其产生目录,并且可以配置core文件的命名格式以便于区分。方法就是在/etc/sysctl.conf文件中添加如下配置项:

kernel.core_pattern=home/core_dump/core-%e-%p-%t

注意根据自己的环境设置正确的目录,如果只指定core文件命名而不带路径则默认生成到执行文件所在目录。然后再执行sudo sysctl -p /etc/sysctl.conf后,通过cat /proc/sys/kernel/core_pattern就可以查看配置是否正常生效。

而命名中的格式控制参数如下:

符号含义
%p<pid>
%u<uid>
%g<gid>
%s导致dump的信号的编号
%t出现dump的时间戳
%e可执行文件的名称
%hhostname

常用命令列表

命令缩写说明
runr开始运行
continuec暂停的程序继续运行
nextn运行至下一行(单步执行)
steps如果有调用函数,进入调用的函数内部
until <n>u运行到当前文件指定行n再停下来
finishfi执行完当前函数,返回到上一层函数调用处暂停
return [n]-直接结束当前函数并可返回指定值n,到上一层函数调用处
jump <n> | <*addr>j将当前程序执行流跳转到指定行n或地址addr
print <val>p打印变量或寄存器值
backtracebt查看当前线程的调用堆栈
framef切换到当前调用线程的指定栈帧,栈帧号通过bt查看
thread <id>-切换到指定线程
break <line> | <file:line> | <function>b添加断点到某行或某个函数入口
tbreaktb添加临时断点,只起效一次
delete <n>del删除断点,断点号n通过 info break 查看
enable <n>-启用某个断点
disable <n>-禁用某个断点
watch <val> | <*addr>-监视某一个变量或内存地址的值是否发生变化
listl显示源码
info-查看断点/线程等信息,可以查看的内容非常多
ptype-查看变量类型
disassembledis查看汇编代码
set args-设置程序启动时的命令行参数
show args-查看设置的命令行参数

部分命令详解

break/b

  • break function —— 在名为 function 的函数入口处添加断点(函数第一行语句)
  • break line —— 在当前文件的第 line 行添加一个断点
  • break file:line —— 在指定文件的第 line 行添加一个断点

我们可以先用list来直接查看代码,然后再通过break来给特定的行打断点,这样有助于我能观察程序进入了某一个分支。

list 命令默认只能显示10行代码,可以通过指令set listsize N来修改该参数。

此外,还可以添加条件断点,即命令break [line] if [condition]。此处的 condition 语法与C语言一致。

info break以及断点的enable、disable和delete

通过info break来查看都打了哪些断点,以及某个断点经过了几次;每个断点会有对应的编号,enable、disable和delete则可以设置某个编号的断点起效、失效以及删除该断点。

(gdb) info break
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x0000555555597fd2 in aeApiPoll at ae_epoll.c:113
	breakpoint already hit 1 time

其中的Enb字段就是指示断点是否被Enable。

如果 disable 命令和 enable 命令不加断点编号,则分别表示禁用和启所有断点。delete 不加编号号 ,则表示删除所有断点 。

print/p 和 ptype

print用于查看变量,甚至可以查看整个结构体的所有成员以及结构体中的某个成员变量。对于变量的操作方式与C语言中是一致的,可以取指针的内容,也可以取变量的内存地址。

查看eventLoop指针指向的结构体的内容:

(gdb) print *eventLoop
$1 = {maxfd = 7, setsize = 10128, timeEventNextId = 1, events = 0x7ffff6c7d040, fired = 0x7ffff6cce2c0, timeEventHead = 0x7ffff6c15140, stop = 0, apidata = 0x7ffff6c7bd80, 
  beforesleep = 0x55555559bcf0 <beforeSleep>, aftersleep = 0x55555559bf40 <afterSleep>, flags = 0}

查看结构体中的整形变量maxfd的值:

(gdb) p eventLoop->maxfd
$2 = 7

如果在 C++ 对象中,可以通过 p this 来显示当前对象的地址,也可以通过 p *this 来列出当前对象的各个成员的值,还可以使用p a + b + c来打印三个变量的结果值。
p func()命令打印函数的返回结果,比如可以用p strerror(errno)将错误码对应的文字信息打印出来。

ptype命令则可以输出某个变量的类型。

(gdb) ptype eventLoop
type = struct aeEventLoop {
    int maxfd;
    int setsize;
    long long timeEventNextId;
    aeFileEvent *events;
    aeFiredEvent *fired;
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata;
    aeBeforeSleepProc *beforesleep;
    aeBeforeSleepProc *aftersleep;
    int flags;
} *		# <---最后这里有个 *,表示这是一个指针

thread及info thread

从下面的运行结果可以看到,redis-server运行时建立了5个线程,当前我们位于Id为1的线程。

(gdb) info thread
  Id   Target Id         Frame 
* 1    Thread 0x7ffff7fdbf80 (LWP 5233) "redis-server" aeMain (eventLoop=0x7ffff6c230f0) at ae.c:484
  2    Thread 0x7ffff6521700 (LWP 5234) "bio_close_file" 0x00007ffff74199f3 in futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x55555595fcc8 <bio_newjob_cond+40>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  3    Thread 0x7ffff5d20700 (LWP 5235) "bio_aof_fsync" 0x00007ffff74199f3 in futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x55555595fcf8 <bio_newjob_cond+88>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  4    Thread 0x7ffff551f700 (LWP 5236) "bio_lazy_free" 0x00007ffff74199f3 in futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x55555595fd28 <bio_newjob_cond+136>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  5    Thread 0x7ffff4d1e700 (LWP 5237) "jemalloc_bg_thd" 0x00007ffff74199f3 in futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x7ffff6e073b0)
    at ../sysdeps/unix/sysv/linux/futex-internal.h:88

通过thread n可以切换到其他线程,然后通过bt去查看当前该线程的栈帧信息。

watch

对某个变量或地址添加watch可以监听该变量或地址,当变量或内存地址发生变化时GDB将暂停,类似与break的效果。

watch可以监听整形变量、指针、数组等等,通过info watch可以查看设置了哪些watch,然后通过delete来删除某个编号的watch。

常用调试技巧

使 print 打印显示完整

当使用 print 命令打印一个字符串或者数组时, 如果该输出太长导致显示不全,我们可以通过set print element 0命令使打印变得完整。

多线程下锁定当前调试线程

GDB调试过程中也有可能发生线程切换,如果我们希望只调试某个线程,不希望线程切换导致函数中的某个局部变量发生变化,可以通过set scheduler-locking on来锁定当前调试线程,同时通过set scheduler-locking off可以关闭锁定。

调试多进程

通过fork()产生子进程时,GDB默认会继续跟踪父进程,那么有两种方法可以调试子进程:

  1. 先调试父进程,在fork()出子进程后,再另开一个终端 attach 上子进程进行调试。
  2. GDB提供了一个选项为follow-fork,可以使用通过show follow-fork mode来查看:
 (gdb) show follow-fork mode
Debugger response to a program call of fork or vfork is "parent".

通过set follow-fork child可以使调试器跟踪子进程。


版权声明:本文为jiang_T原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。