概述

本文介绍如何通过gdb来调试redis的源代码。相对于只是查看源码,通过gdb还能够在实际场景中观察代码如何运行,内存如何变化,这对于理解redis-server的运行机制非常有必要。
在进行redis-server一般命令的运行机制调试时,不需要你精通c/c++编程,只需要知道gdb的一些基本命令就可以了。

gdb介绍

通过gdb对redis-server进行调试时,需要知道一些基本的gdb命令。比如说,如何打断点,如何打印结构体的内容,如何跟踪子进程的运行等等。本节介绍一些基本的gdb命令,通过这些命令就可以进入redis的世界了。

  • 先启动gdb并指定运行代码(redis-server是事先编译好的),如下:

$ gdb ./src/redis-server
(gdb)

  • 通过以下命令来运行gdb

命令名

功能

r

运行刚才加载的二进制程序,可以指定参数

b

打断点,后面的参数可以是函数,或行数

n

单步执行,但不进入函数中

s

单步执行,进入调用函数

bt

查看目前的调用堆栈(很有用,可以查看函数的调用栈,和目前运行的位置)

l

查看代码,参数可以是函数名,或行数

p

打印变量或结构体的值

其他命令可以查看gdb手册。

通过gdb调试redis-server

编译redis-server源码

下载redis-4.0.9,解压后进入redis目录,然后直接make,如下:

cd redis-4.0.9
make

编译好的二进制在src下,还要注意,在redis-4.0.9目录下有一个redis.conf文件,这是运行redis-server时的配置文件。

注意:通过gdb调试代码时,需要在编译的时候在gcc后面加上一个选项:-g -ggdb,在redis-server的编译选项中默认是添加的,所以直接make即可。

编译完成后,在src下,应该还有几个其他编译好的可执行文件:

redis-server: redis服务的主二进制代码
redis-cli   :  redis命令行客户端
redis-benchmark
redis-sentinel
...

本文介绍中,我们要用到的是两个 redis-server和redis-cli

开始调试

好了,编译好代码后,我们开始调试redis-server了

$ gdb src/redis-server 
(gdb)

通过gdb查看set命令的运行过程

  • 通过gdb启动redis-server

要查看set命令的运行过程,首先要对执行set命令的函数入口打上断点。然后进入该处理函数,单步执行。在server.c代码中,有一个命令处理的列表函数,如下:

struct redisCommand redisCommandTable[] = {
    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
... ...

从以上代码我们看到set命令的实现函数是setCommand,我们可以在该函数的入口处打上断点:

// 打上断点
(gdb) b setCommand 

// 运行redis-server
(gdb) r ./redis.conf

此时,gdb会阻塞在redis-server的等待连接的地方,因为没有redis客户端连接服务端,此时我们需要启动redis-cli,并发送set命令。

  • 启动redis-cli

开一个新的终端,进入刚才编译redis的目录,并启动redis-cli。

cd redis-4.0.9/src
$ ./redis-cli 
127.0.0.1:6379> set k1 "v123"
  • 观察set命令的运行机制
    在gdb的界面上,可以看到,代码已经运行到setCommand函数入口了。在gdb终端查看运行栈:
(gdb) bt
#0  setCommand (c=0x102016000) at t_string.c:102
#1  0x000000010000cd53 in call (c=0x102016000, flags=15) at server.c:2229
#2  0x000000010000d61e in processCommand (c=0x102016000) at server.c:2510
#3  0x000000010001dd76 in processInputBuffer (c=0x5b91a431) at networking.c:1354
#4  0x00000001000051ee in aeProcessEvents (eventLoop=0x1005289b0, flags=11) at ae.c:440
#5  0x000000010000552b in aeMain (eventLoop=0x5b91a431) at ae.c:498
#6  0x0000000100010680 in main (argc=1, argv=0x0) at server.c:3894

进入该函数,并单步执行:

(gdb) n
138     c->argv[2] = tryObjectEncoding(c->argv[2]); //为了节约内存:对string的value进行编码

若想进入函数内部,直接通过s命令:

(gdb) s
tryObjectEncoding (o=0x100306600) at object.c:384
384     serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

打印变量的值:

(gdb) p o
$1 = (robj *) 0x100306600 // o是指针

// 通过以下命令打印指针的内容
(gdb) p *o
$2 = {type = 0, encoding = 8, lru = 9544752, refcount = 1, ptr = 0x100306613}
(gdb) p o->type

若要直到命令处理的全过程,可以processCommand函数打上断点。然后,和上面介绍的一样单步执行。

总结

本文描述了如何通过gdb对redis-server进行调试,并对redis-server的内部执行进行观察。但本文并没有介绍更加复杂的gdb的调试技巧,比如多进程的调试。