本课的核心内容如下:
run命令
continue命令
break命令
backtrace与frame命令
info break、enable、disable和delete命令
list命令
print和ptype命令
为了结合实践,这里以调试Redis源码为例来介绍没一个命令,这里先介绍一些常用命令的基础用法,某些命令的高级用法会在后面讲解。
Redis的最新源码下载地址可以在Redis官网(Redis中文网)获得,使用wget命令将Redis源码文件下载下来:
解压:tar zxvf redis-5.0.3.tar.gz
进入生成的 redis-5.0.3 目录使用makefile命令进行编译。makefile命令是Linux程序编译基本的命令,由于本课程的重点是Linux调试,如果读者不熟悉Linux编译可以通过互联网或相关书籍补充一下知识。
步骤:cd redis-5.0.3/
make -j 4
cd src
make test
然后启动 redis-server
4.1 run命令
默认情况下,前面的课程中我们说gdb filename 命令知识个附加的一个调试文件,并没有启动这个程序,需要输入run命令(简写为r)启动这个程序。
(gdb) r Starting program: /home/wzq/Desktop/redis-5.0.3/src/redis-server [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 21463:C 09 Jan 2019 14:33:20.281 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 21463:C 09 Jan 2019 14:33:20.281 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=21463, just started 21463:C 09 Jan 2019 14:33:20.281 # Warning: no config file specified, using the default config. In order to specify a config file use /home/wzq/Desktop/redis-5.0.3/src/redis-server /path/to/redis.conf 21463:M 09 Jan 2019 14:33:20.282 * Increased maximum number of open files to 10032 (it was originally set to 1024). [New Thread 0x7ffff67ff700 (LWP 21467)] [New Thread 0x7ffff5ffe700 (LWP 21468)] [New Thread 0x7ffff57fd700 (LWP 21469)] _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 5.0.3 (00000000/0) 64 bit .-`` .-```. ```/ _.,_ ''-._ ( ' , .-` | `, ) Running in standalone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 21463 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' 21463:M 09 Jan 2019 14:33:20.283 # Server initialized 21463:M 09 Jan 2019 14:33:20.283 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. 21463:M 09 Jan 2019 14:33:20.283 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 21463:M 09 Jan 2019 14:33:20.283 * Ready to accept connections
4.2 continue命令
当GDB触发断点或者使用Ctr+C命令中断下来后,想让程序继续运行,只要输入continue命令即可(简写为c)。当然,如果continue命令继续触发断点,GDB就再次中断下来。
4.3 break命令
break命令(简写为b)即我们添加断点的命令,可以使用以下方式添加断点:
break functionname,在函数名为functionname的入口处添加一个断点。
break LineNo,在当前文件行号为LineNo处添加一个断点。
break filename:LineNo,在filename文件行号为LineNo处添加一个断点。
(gdb) b main Breakpoint 1 at 0x555555584cf0: file server.c, line 4003.
添加好之后,使用run命令重启程序,就可以触发这个断点了,GDB就会停在断点处。
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/wzq/Desktop/redis-5.0.3/src/redis-server [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main (argc=1, argv=0x7fffffffccc8) at server.c:4003 4003 int main(int argc, char **argv) { (gdb)
redis-server默认端口号是6379,我们知道这个端口号肯定是通过操作系统的socket API bind()函数创建的,通过文件搜索,我们找到调用这个函数的文件,其位于anet.c441行。
我们使用break命令在这个地方加一个断点:
(gdb) b anet.c:441 Breakpoint 2 at 0x55555558862b: file anet.c, line 441.
由于程序绑定端口号是redis-server启动时初始化的,为了能触发这个断点,再次使用run命令启动下这个程序,GDB第一次会触发main()函数处的断点,输入continue命令继续运行,接着触发anet.c:441处的断点:
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/wzq/Desktop/redis-5.0.3/src/redis-server [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main (argc=1, argv=0x7fffffffccc8) at server.c:4003 4003 int main(int argc, char **argv) { (gdb) c Continuing. 23268:C 09 Jan 2019 15:02:04.276 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 23268:C 09 Jan 2019 15:02:04.276 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=23268, just started 23268:C 09 Jan 2019 15:02:04.276 # Warning: no config file specified, using the default config. In order to specify a config file use /home/wzq/Desktop/redis-5.0.3/src/redis-server /path/to/redis.conf 23268:M 09 Jan 2019 15:02:04.277 * Increased maximum number of open files to 10032 (it was originally set to 1024). Breakpoint 2, anetListen (err=0x5555559161e0 <server+576> "", s=6, sa=0x555555b2b240, len=28, backlog=511) at anet.c:441 441 if (bind(s,sa,len) == -1) { (gdb)
anet.c:441处的代码如下:
现在断点停在第441行,所以当前文件就是anet.c,可以直接使用“break 行号”添加断点,例如,可以在第444行、450行、452行分别加一个断点,看看这个函数执行完毕后走哪个return语句退出,则可以执行:
添加好这三个断点后,我们使用continue命令继续运行程序,发现程序运行到第452行中断下来(即触发Breakpoint 5):
说明redis-server绑定端口并设置侦听(listen)成功,我们可以再打开一个SSH窗口,验证一下,发现6379端口确实已经处于侦听状态了:
wzq@wzq-PC:~/Desktop/redis-5.0.3/src$ lsof -i -Pn | grep redis
redis-ser 23268 wzq 6u IPv6 114334 0t0 TCP *:6379 (LISTEN)
4.4 backtrace 与 frame命令
backtrace命令(简写为bt)用来查看当前调用堆栈。接上,redis-server现在中断在anet.c:452行,可以通过backtrace命令来查看当前的调用堆栈:
(gdb) bt #0 anetListen (err=0x5555559161e0 <server+576> "", s=6, sa=<optimized out>, len=<optimized out>, backlog=511) at anet.c:452 #1 0x00005555555887a4 in _anetTcpServer (err=0x5555559161e0 <server+576> "", port=<optimized out>, bindaddr=<optimized out>, af=10, backlog=511) at anet.c:487 #2 0x000055555558cf07 in listenToPort (port=6379, fds=0x55555591610c <server+364>, count=0x55555591614c <server+428>) at server.c:1924 #3 0x0000555555591ed0 in initServer () at server.c:2055 #4 0x0000555555585103 in main (argc=<optimized out>, argv=0x7fffffffccc8) at server.c:4160
这里一共有5层堆栈,最顶层是main()函数,最底层是断点所在的anetListen()函数,堆栈编号分别是#0-#4,如果想切换到其他堆栈处,可以使用frame命令(简写f),改命令的使用方法是“frame堆栈编号(编号不加#)”。在这里一次切换至堆栈顶部,然后再切换回#0联系一下:
通过查看上面的各堆栈,可以得出这里的调用层级关系,即:
main()函数在4160行调用了initServer()函数
initServer()在第2055行调用了listenToPort()函数
listenToPort在1924行调用了anetTcp6Server()函数
_anetTcp6Server()在487行调用了anetListen()函数
当前断点正好位于anetListen()函数中
4.5 info break、enable、disable、和delete命令
在程序中加了很多段点,而我们想查看加了那些断点时,可以使用info break命令(简写 info b)。
通过上面的内容可以知道,目前一共增加了5个断点,其他信息比如每个断点的位置(所在的文件和行号)、内存地址、断点启用和禁用状态信息一目了然。如果我们想禁用某个断点使用“disable 断点编号”既可以禁用这个断点了,被禁用的断点不会再被触发;同理,被禁用的断点也可以使用“enable 断点编号”重新启用。
使用disable 1以后,第一个断点的End一栏的值由y变成n,重启程序也不会再次触发。
如果disable命令和enable命令不加断点编号,则分别表示禁用和启用所哟断点。
使用“delete 编号”可以删除某个断点,如delete 2 3则表示删除的断点2和断点3。
同样道理,如果输入delete不叫命令号,则表示删除所有断点。
4.6 list命令
list命令和后面介绍的print命令都是GDB调试中用到的频率最高的命令,list命令(简写为l)可以查看当前断点处的代码。使用frame命令切换到刚才的堆栈#3处,然后输入list命令看下效果:
断点停在2055行,输入list命令以后,会显示第2055行前后的10行代码,再次输入list命令试一下:
代码继续往后显示10行,也就是说,第一次输入list命令会显示断点处前后的代码,继续输入list指令会以递增行号的形式继续显示剩下的代码行,一直到文件结束为止。当然list指令还可以往前和往后显示代码,命令分别是“list +”和“list -”;
list默认显示多少行可以通过修改相关的GDB配置,由于我们一般不会修改这个默认显示行数,这里就不再浪费篇幅介绍了。list不仅可以显示当前断点处的代码,也可以显示其他文件某一行的代码,更多的用法可以在GDB中输入help list查看。
使用GDB的目的是调试,因此更关心的是断点附近的代码,而不是通过GDB阅读代码,GDB并不是一个好的阅读工具。以我自己为例,调试Redis时用GDB调试,而阅读代码使用的却是Visual Studio,如下图所示:
4.7 print 和ptype 命令
通过print命令(简写为p)我们可以在调试过程中方便地查看变量的值,也可以修改当前内存中的变量值。切换当前断点到堆栈#3,然后打印以下三个变量。
这里使用print命令分别打印处serve.port、server.ipfd、server.ipfd_count的值,其中server.ipfd显示“{0}”,这是GDB显示字符串或字符数据特有的方式,当一个字符串变量或者字符数组或者连续的内存值重复若干次,GDB就会以这种模式来显示以节约空间。
print命令不仅可以显示变量值,也可以显示进行一定运算的表达式计算结果值,甚至可以显示一些函数的执行结果值。
举个例子,我们可以输入p &server.port来输出server.port的地址值,如果在C++对象中,可以通过p this来显示当前对象的地址,也可以通过p *this来列出当前对象的各个成员变量值,如果有三个变量可以相加(假设变量名分别叫a,b,c),可以使用p a+b+c来打印这三个变量的结果值。
假设func()是一个可以执行的函数,p func()命令可以输出该变量的执行结果。举一个最常用的例子,某个时刻,某个系统函数执行失败了,通过系统变量errno得到一个错误码,则可以使用p strerror(error),将这个对应的文字信息打印出来,这样就不用费劲地去man手册上查找这个错误码对应的错误含义了。
print命令不仅可以输出表达式结果,同时也可以修改变量的值,我们尝试将上问中的端口号从6379改成6400试试:
(gdb) p server.port = 6400 $4 = 6400 (gdb) p server.port $5 = 6400
GDB还有另一个命令叫ptype,顾名思义,其含义是“print type”,就是输出一个变量的类型。例如我们试着输出Redis堆栈#3的变量server和变量server.port的类型:
type = struct redisServer { pid_t pid; char *configfile; char *executable; char **exec_argv; int dynamic_hz; int config_hz; int hz; redisDb *db; dict *commands; dict *orig_commands; aeEventLoop *el; unsigned int lruclock; int shutdown_asap; int activerehashing; int active_defrag_running; char *requirepass; char *pidfile; int arch_bits; int cronloops; char runid[41]; int sentinel_mode; size_t initial_memory_usage; int always_show_logo; dict *moduleapi; list *loadmodule_queue; int module_blocked_pipe[2]; int port; int tcp_backlog; char *bindaddr[16]; int bindaddr_count; char *unixsocket; mode_t unixsocketperm; int ipfd[16]; ---Type <return> to continue, or q <return> to quit---q Quit (gdb) ptype server.port type = int
可以看到,对于一个复合数据类型的变量,ptype不仅列出了这个变量的类型,而且还详细地列出了每个成员变量的字段名,有了这个功能,我们在调试时就不用可以去代码文件中查看某个变量的类型定义了。