GDB在LINUX是一项重要的调试工具,本文介绍它的基本使用方法。
1. 安装GDB
在linux中需要先安装gcc和gdb
# 安装 gcc
yum install gcc
# 安装 g++
yum install gcc-c++
# 安装 gdb
yum install gdb
当要调试某程序时,需要在gcc编译时加上-g,目的是保留编译后的程序中调试符号信息。
当输入gdb hello
时能看到:
Reading symbols from /root/testclient/hello_server...done.
(gdb)
说明保留调试符号信息成功。
2. 启动GDB调试
启动有三种方式:直接调试目标程序,附加进程,调试core文件
2.1 直接调试目标程序
gdb filename
filename
是需要启动的调试文件名,可以直接使用gdb启动一个程序调试,此时并没有完全启动,而是附加了一个可执行文件,需要输入run才能真正运行。
2.2 附加进程号
gdb attach ProcessID
有时候程序已经启动,我们并不想重启它(比如服务器),此时需要利用attach指令将进程号添加到GDB调试器。获取进程号的方法如下所示:
[zhangyl@iZ238vnojlyZ flamingoserver]$ ps -ef | grep chatserver
zhangyl 21462 21414 0 18:00 pts/2 00:00:00 grep --color=auto chatserver
zhangyl 26621 1 5 Oct10 ? 2-17:54:42 ./chatserver -d
得到charserver
的PID为26621,使用gdb attach 26621
将GDB附加到chatserver
中,看到attaching to process 26661
时表示已经附加成功。
[zhangyl@localhost flamingoserver]$ gdb attach 26621
Attaching to process 26661
Reading symbols from /home/zhangyl/flamingoserver/chatserver...done.
Reading symbols from /usr/lib64/mysql/libmysqlclient.so.18...Reading symbols from /usr/lib64/mysql/libmysqlclient.so.18...(no debugging symbols found)...done.
Reading symbols from /lib64/libpthread.so.0...(no debugging symbols found)...done.
[New LWP 42931]
[New LWP 42930]
[New LWP 42929]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Loaded symbols for /lib64/libpthread.so.0
Reading symbols from /lib64/libc.so.6...(no debugging symbols found)...done.
调试完成时,可以使用detach将其分离。
2.3 调试core文件
gdb filename corename
有时候,服务器程序运行一段时间后会突然崩溃,只要程序在崩溃的时候有 core 文件产生,就可以使用这个 core 文件来定位崩溃的原因。
默认情况下,Linux关闭了core文件机制,使用ulimit -c
命令来查看系统是否启动这一机制。0表示关闭,数字表示启动但限制core文件大小,unlimited表示启动且不限制大小。
[zhangyl@localhost flamingoserver]$ ulimit -c
core file size (blocks, -c) 0
使用ulimit -c unlimited
指令来启动。
[zhangyl@localhost flamingoserver]$ ulimit -c unlimited
[zhangyl@localhost flamingoserver]$ ulimit -c
core file size (blocks, -c) unlimited
生成的 core 文件的默认命名方式是 core.pid
,比如某个程序运行时的进程 ID 是 16663,那么它崩溃产生的 core 文件的名称就是 core.16663
,可以通过gdb chatserver core.16663
来启动调试。
多个程序同时崩溃,我们根本没法通过 core 文件名称中的 PID 来区分。因此可以再程序中记录一下自己的PID。
void writePid()
{
uint32_t curPid = (uint32_t) getpid();
FILE* f = fopen("xxserver.pid", "w");
assert(f);
char szPid[32];
snprintf(szPid, sizeof(szPid), "%d", curPid);
fwrite(szPid, strlen(szPid), 1, f);
fclose(f);
}
我们在程序启动时调用上述 writePID
函数,将程序当时的 PID 记录到 xxserver.pid
文件中去,这样当程序崩溃时,可以从这个文件中得到进程当时运行的 PID,这样就可以与默认的 core 文件名后面的 PID 做匹配了。
3. 常用命令
首先下载redis开源代码作为示例
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
Redis 的最新源码下载地址可以在 Redis 官网获得,使用 wget
命令将 Redis 源码文件下载下来:
[root@localhost gdbtest]# wget http://download.redis.io/releases/redis-4.0.11.tar.gz
然后解压:
[root@localhost gdbtest]# tar zxvf redis-4.0.11.tar.gz
使用Makefile
编译,加入MALLOC=libc
防止jemalloc
问题,make distclean
清除上次失败的编译残留文件。
make distclean
make MALLOC=libc
进入src目录,使用gdb redis-server
启动程序。
3.1 流控制语句
run
gdb filename 命令只是附加的一个调试文件,并没有启动这个程序,需要输入 run 命令(简写为 r)启动这个程序
redis-server 启动后能看到一个四方堆叠结构,说明启动成功了。假设程序已经启动,再次输入 run 命令则是重启程序。我们在 GDB 界面按 Ctrl + C 快捷键让 GDB 中断下来,再次输入 r 命令,GDB 会询问我们是否重启程序,输入 yes 确认重启。
continue
当 GDB 触发断点或者使用 Ctrl + C 命令中断下来后,想让程序继续运行,只要输入 continue命令即可(简写为 c)
^C
Program received signal SIGINT, Interrupt.
0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
(gdb) c
Continuing.
next、step、until、finish、return 和 jump 命令
next 命令(简写为 n)是让 GDB 调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。
在 GDB 命令行界面如果直接按下回车键,默认是将最近一条命令重新执行一遍,因此,当使用 next 命令单步调试时,不必反复输入 n 命令,直接回车就可以。
(gdb) n
3736 spt_init(argc, argv);
(gdb)
3738 setlocale(LC_COLLATE,"");
(gdb)
3739 zmalloc_set_oom_handler(redisOutOfMemoryHandler);
next 命令用调试的术语叫“单步步过”(step over),即遇到函数调用直接跳过,不进入函数体内部。而step 命令(简写为 s)就是“单步步入”(step into),顾名思义,就是遇到函数调用,进入函数内部。
在某个函数中调试一段时间后,不需要再一步步执行到函数返回处,希望直接执行完当前函数并回到上一层调用处,就可以使用 finish 命令。与 finish 命令类似的还有 return 命令,return 命令的作用是结束执行当前函数,还可以指定该函数的返回值。
#include <stdio.h>
int func()
{
int a = 9;
printf("a=%d.\n", a);
}
int main()
{
int c = func();
printf("c=%d.\n", c);
return 0;
}
上述代码在func函数里直接输入 return 命令以后,不会返回任何值,如果输入retrun 9999,那么打印出来结果就是9999。
until 命令(简写为 u)可以指定程序运行到某一行停下来,可以代替break+continue,比如当前运行到338行,想要运行到992行,直接u 992
jump命令也是跳转,但中间的步骤不会执行。比如从338行跳转到992行j 992
。从338行到992行之间的所有代码都不会执行(可能会因为没有初始化造成问题),并且会从992行以后持续执行(如果没有碰到断点)
3.2 断点语句
break 命令(简写为 b)即我们添加断点的命令,可以使用以下方式添加断点:
- break functionname,在函数名为 functionname 的入口处添加一个断点;
- break LineNo,在当前文件行号为 LineNo 处添加一个断点;
- break filename:LineNo,在 filename 文件行号为 LineNo 处添加一个断点。
在程序中加了很多断点,而我们想查看加了哪些断点时,可以使用 info break 命令。
如果我们想禁用某个断点,使用“disable 断点编号”就可以禁用这个断点了,被禁用的断点不会再被触发;同理,被禁用的断点也可以使用“enable 断点编号”重新启用。如果 disable 命令和 enable 命令不加断点编号,则分别表示禁用和启用所有断点。
使用“delete 编号”可以删除某个断点,如 delete 2 3 则表示要删除的断点 2 和断点 3。同样的道理,如果输入 delete 不加命令号,则表示删除所有断点。
tbreak 命令也是添加一个断点,第一个字母“t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是一旦该断点触发一次后就会自动删除。
3.2 堆栈和线程查看语句
list
list 命令(简写为 l)可以查看当前断点处的代码。使用 frame 命令切换到刚才的堆栈 #3 处,然后输入 list 命令看下效果:
(gdb) f 4
#4 0x000000000042fa77 in initServer () at server.c:1852
1852 listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
(gdb) l
1847 }
1848 server.db = zmalloc(sizeof(redisDb)*server.dbnum);
1849
1850 /* Open the TCP listening socket for the user commands. */
1851 if (server.port != 0 &&
1852 listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
1853 exit(1);
1854
1855 /* Open the listening Unix domain socket. */
1856 if (server.unixsocket != NULL) {
(gdb)
list语句现实的是目标语句处前后附近的代码,再次输入list命令后,代码继续往后显示 10 行。 list 指令还可以往前和往后显示代码,命令分别是list + (加号)和list - (减号)。
backtrace和frame
backtrace 命令(简写为 bt)用来查看当前调用堆栈。若redis-server 现在中断在 anet.c:452 行,可以通过 backtrace 命令来查看当前的调用堆栈:
(gdb) bt
#0 anetListen (err=0x746bb0 <server+560> "", s=10, sa=0x7e34e0, len=16, backlog=511) at anet.c:452
#1 0x0000000000426e35 in _anetTcpServer (err=err@entry=0x746bb0 <server+560> "", port=port@entry=6379, bindaddr=bindaddr@entry=0x0, af=af@entry=10, backlog=511)
at anet.c:487
#2 0x000000000042793d in anetTcp6Server (err=err@entry=0x746bb0 <server+560> "", port=port@entry=6379, bindaddr=bindaddr@entry=0x0, backlog=511)
at anet.c:510
#3 0x000000000042b0bf in listenToPort (port=6379, fds=fds@entry=0x746ae4 <server+356>, count=count@entry=0x746b24 <server+420>) at server.c:1728
#4 0x000000000042fa77 in initServer () at server.c:1852
#5 0x0000000000423803 in main (argc=1, argv=0x7fffffffe648) at server.c:3862
(gdb)
这里一共有 6 层堆栈,最顶层是 main()
函数,最底层是断点所在的 anetListen()
函数,堆栈编号分别是 #0 ~ #5。比如我们在利用pthread_creat
函数创建线程时可以利用bt查看创建过程。
使用bt以后可以看到如下结果,说明creat线程创建过程是clone->start_thread->MainTcpEpollThread
#0 MainTcpEpollThread (lpParam=0x0) at ./Server.cpp:1035
#1 0x00007ffff7bc6dd5 in start_thread () from /lib64/libpthread.so.0
#2 0x00007ffff68f3ead in clone () from /lib64/libc.so.6
如果想切换到其他堆栈处,可以使用 frame 命令(简写为 f),该命令的使用方法是frame 堆栈编号(编号不加 #)
(gdb) f 1
#1 0x0000000000426e35 in _anetTcpServer (err=err@entry=0x746bb0 <server+560> "", port=port@entry=6379, bindaddr=bindaddr@entry=0x0, af=af@entry=10, backlog=511)
at anet.c:487
487 if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) s = ANET_ERR;
(gdb) f 2
#2 0x000000000042793d in anetTcp6Server (err=err@entry=0x746bb0 <server+560> "", port=port@entry=6379, bindaddr=bindaddr@entry=0x0, backlog=511)
at anet.c:510
510 return _anetTcpServer(err, port, bindaddr, AF_INET6, backlog);
(gdb)
通过查看上面的各个堆栈,可以得出这里的调用层级关系,即:
main()
函数在第 3862 行调用了initServer()
函数initServer()
在第 1852 行调用了listenToPort()
函数listenToPort()
在第 1728 行调用了anetTcp6Server()
函数anetTcp6Server()
在第 510 行调用了_anetTcpServer()
函数_anetTcpServer()
函数在第 487 行调用了anetListen()
函数- 当前断点正好位于
anetListen()
函数中
thread 命令
使用 info thread 命令来查看当前进程有哪些线程
(gdb) info thread
Id Target Id Frame
4 Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
3 Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
2 Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
* 1 Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
通过 info thread 的输出可以知道 redis-server 正常启动后,一共产生了 4 个线程,包括一个主线程和三个工作线程,线程编号(Id 那一列)分别是 4、3、2、1。三个工作线程(2、3、4)分别阻塞在 Linux API pthread_cond_wait
处,而主线程(1)阻塞在 epoll_wait
处。
虽然第一栏的名称叫 Id,但第一栏的数值不是线程的 Id,第三栏括号里的内容(如 LWP 53065)中,53065 这样的数值才是当前线程真正的 Id。 LWP 是Light Weight Process(轻量级进程),之前Linux没有线程,用进程代替后来 Linux 系统有了真正的线程实现,这个名字仍然被保留了下来。
可以通过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输入 thread 2 即可,然后输入 bt 就能查看这个线程的调用堆栈了。当把 GDB 当前作用的线程切换到线程 2 上之后,线程 2 前面就被加上了星号。
3.4 变量信息查看语句
print和ptype命令
通过 print 命令(简写为 p)我们可以在调试过程中方便地查看变量的值,也可以修改当前内存中的变量值。切换当前断点到堆栈 #4 ,然后打印以下三个变量:
(gdb) f 4
#4 0x000000000042fa77 in initServer () at server.c:1852
1852 listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
(gdb) l
1847 }
1848 server.db = zmalloc(sizeof(redisDb)*server.dbnum);
1849
1850 /* Open the TCP listening socket for the user commands. */
1851 if (server.port != 0 &&
1852 listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
1853 exit(1);
1854
1855 /* Open the listening Unix domain socket. */
1856 if (server.unixsocket != NULL) {
(gdb) p server.port
$15 = 6379
(gdb) p server.ipfd
$16 = {0 <repeats 16 times>}
(gdb) p server.ipfd_count
$17 = 0
这里使用 print 命令分别打印出 server.port
、server.ipfd
、server.ipfd_count
的值,其中 server.ipfd
显示 “{0 …}”,这是 GDB 显示字符串或字符数据特有的方式,当一个字符串变量或者字符数组或者连续的内存值重复若干次,GDB 就会以这种模式来显示以节约空间.
此外我们可以输入 p &server.port 来输出 server.port
的地址值。 func()
是一个可以执行的函数,p func()
命令可以输出该变量的执行结果。举一个最常用的例子,某个时刻,某个系统函数执行失败了,通过系统变量 errno 得到一个错误码,则可以使用 p strerror(errno) 将这个错误码对应的文字信息打印出来,这样就不用费劲地去 man 手册上查找这个错误码对应的错误含义了。
ptype ,顾名思义,其含义是“print type”,就是输出一个变量的类型。例如,我们试着输出 Redis 堆栈 #4 的变量 server
和变量 server.port
的类型:
(gdb) ptype server
type = struct redisServer {
pid_t pid;
char *configfile;
char *executable;
char **exec_argv;
int hz;
redisDb *db;
...省略部分字段...
(gdb) ptype server.port
type = int
info args命令
info args可以用来查看当前函数的参数值
(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fec780 (LWP 53062))]
#0 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
(gdb) bt
#0 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
#1 0x00000000004265df in aeApiPoll (tvp=0x7fffffffe300, eventLoop=0x7ffff08350a0) at ae_epoll.c:112
#2 aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
#3 0x0000000000426aeb in aeMain (eventLoop=0x7ffff08350a0) at ae.c:501
#4 0x00000000004238ef in main (argc=1, argv=0x7fffffffe648) at server.c:3899
(gdb) f 2
#2 aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
411 numevents = aeApiPoll(eventLoop, tvp);
(gdb) info args
eventLoop = 0x7ffff08350a0
flags = 11
(gdb)
上述代码片段切回至主线程 1,然后切换到堆栈 #2,堆栈 #2 调用处的函数是 aeProcessEvents() ,一共有两个参数,使用 info args 命令可以输出当前两个函数参数的值,参数 eventLoop 是一个指针类型的参数,对于指针类型的参数,GDB 默认会输出该变量的指针地址值,如果想输出该指针指向对象的值,在变量名前面加上解引用即可,这里使用 p* eventLoop
命令:
(gdb) p *eventLoop
$26 = {maxfd = 11, setsize = 10128, timeEventNextId = 1, lastTime = 1536570672, events = 0x7ffff0871480, fired = 0x7ffff08c2e40, timeEventHead = 0x7ffff0822080,
stop = 0, apidata = 0x7ffff08704a0, beforesleep = 0x429590 <beforeSleep>, aftersleep = 0x4296d0 <afterSleep>}
watch命令
watch 命令可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时,GDB 就会中断下来。被监视的某个变量或者某个内存地址会产生一个 watch point(观察点), watch 命令就可以通过添加硬件断点来达到监视数据变化的目的。它有两种形式:
//形式一:整型变量
int i;
watch i
//形式二:指针类型
char *p;
watch p 与 watch *p
//形式三:watch 一个数组或内存区间
char buf[128];
watch buf
注意:watch p
与 watch *p
是有区别的,前者是查看* (&p),是 p 变量本身;后者是 p 所指内存的内容。我们需要查看地址,因为目的是要看某内存地址上的数据是怎样变化的。
display命令
display 命令监视的变量或者内存地址,每次程序中断下来都会自动输出这些变量或内存的值。例如,假设程序有一些全局变量,每次断点停下来我都希望 GDB 可以自动输出这些变量的最新值,那么使用“display 变量名”设置即可
比如输入(gdb) display $ebx
,那么每次调试的最后无论有没有print最后都会打印出$ebx的结果。
以使用 info display 查看当前已经自动添加了哪些值,使用 delete display 清除全部需要自动输出的变量,使用 delete diaplay 编号 删除某个自动输出的变量。