调试
Redis服务器进程调试指南
Redis 的开发重点在于稳定性。我们在每个版本中都尽力确保您能体验到无崩溃的稳定产品。然而,如果您需要调试 Redis 进程本身,请继续阅读。
当Redis崩溃时,它会生成一个详细的报告,说明发生了什么。然而,有时查看崩溃报告是不够的,Redis核心团队也无法独立重现该问题。在这种情况下,我们需要能够重现该问题的用户的帮助。
本指南展示了如何使用GDB来提供Redis开发者所需的信息,以便更轻松地追踪错误。
什么是GDB?
GDB是GNU调试器:一个能够检查另一个程序内部状态的程序。通常,跟踪和修复错误是一个收集更多关于程序在错误发生时的状态信息的练习,因此GDB是一个非常有用的工具。
GDB 可以通过两种方式使用:
- 它可以附加到一个正在运行的程序,并在运行时检查其状态。
- 它可以检查已经终止的程序的状态,使用所谓的核心文件,即程序运行时内存的映像。
从调查Redis漏洞的角度来看,我们需要使用这两种GDB模式。能够重现漏洞的用户将GDB附加到他们正在运行的Redis实例上,当崩溃发生时,他们会创建core
文件,开发者随后将使用该文件来检查崩溃时的Redis内部状态。
这样,开发者可以在他或她的计算机上执行所有检查,而无需用户的帮助,用户可以自由地在他们的生产环境中重新启动Redis。
编译没有优化的Redis
默认情况下,Redis 使用 -O2
开关编译,这意味着启用了编译器优化。这使得 Redis 可执行文件更快,但同时也使得 Redis(像任何其他程序一样)更难使用 GDB 进行检查。
最好使用make noopt
命令(而不是仅仅使用普通的make
命令)将GDB附加到未优化编译的Redis上。然而,如果您在生产环境中已经有一个正在运行的Redis,如果重新编译和重启会带来问题,那么就没有必要这样做。GDB仍然可以用于优化编译的可执行文件。
你不应该过于担心在没有优化的情况下编译Redis导致的性能损失。在你的环境中,这不太可能引起问题,因为Redis并不非常依赖CPU。
将GDB附加到正在运行的进程
如果你已经有一个正在运行的Redis服务器,你可以将GDB附加到它上面,这样如果Redis崩溃,就可以检查内部状态并生成一个core dump
文件。
在你将GDB附加到Redis进程后,它将继续正常运行,不会出现任何性能损失,因此这不是一个危险的操作。
为了附加GDB,首先需要的是正在运行的Redis实例的进程ID(进程的pid)。你可以很容易地使用redis-cli
来获取它:
$ redis-cli info | grep process_id
process_id:58414
在上面的例子中,进程ID是58414。
登录到您的Redis服务器。
(可选但推荐)启动screen或tmux或任何其他程序,以确保如果您的ssh连接超时,您的GDB会话不会关闭。您可以在这篇文章中了解更多关于screen的信息。
通过输入以下命令将GDB附加到正在运行的Redis服务器:
$ gdb <path-to-redis-executable> <pid>
例如:
$ gdb /usr/local/bin/redis-server 58414
GDB 将启动并附加到正在运行的服务器,打印类似以下内容:
Reading symbols for shared libraries + done
0x00007fff8d4797e6 in epoll_wait ()
(gdb)
此时GDB已经附加,但你的Redis实例被GDB阻塞了。为了让Redis实例继续执行,只需在GDB提示符下输入continue,然后按回车键。
(gdb) continue
Continuing.
完成!现在你的Redis实例已经附加了GDB。现在你可以等待下一次崩溃了。:)
现在是时候分离你的屏幕/tmux会话了,如果你正在使用它运行GDB,可以通过按下Ctrl-a a组合键来实现。
崩溃之后
Redis 有一个命令可以模拟分段错误(换句话说,一个严重的崩溃),使用 DEBUG SEGFAULT
命令(当然不要对真正的生产实例使用它!)。因此,我将使用此命令来使我的实例崩溃,以展示在 GDB 端会发生什么:
(gdb) continue
Continuing.
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0xffffffffffffffff
debugCommand (c=0x7ffc32005000) at debug.c:220
220 *((char*)-1) = 'x';
如你所见,GDB检测到Redis崩溃了,甚至能够显示导致崩溃的文件名和行号。这已经比Redis崩溃报告的回溯(仅包含函数名称和二进制偏移量)要好得多。
获取堆栈跟踪
首先要做的是使用GDB获取完整的堆栈跟踪。这就像使用bt命令一样简单:
(gdb) bt
#0 debugCommand (c=0x7ffc32005000) at debug.c:220
#1 0x000000010d246d63 in call (c=0x7ffc32005000) at redis.c:1163
#2 0x000000010d247290 in processCommand (c=0x7ffc32005000) at redis.c:1305
#3 0x000000010d251660 in processInputBuffer (c=0x7ffc32005000) at networking.c:959
#4 0x000000010d251872 in readQueryFromClient (el=0x0, fd=5, privdata=0x7fff76f1c0b0, mask=220924512) at networking.c:1021
#5 0x000000010d243523 in aeProcessEvents (eventLoop=0x7fff6ce408d0, flags=220829559) at ae.c:352
#6 0x000000010d24373b in aeMain (eventLoop=0x10d429ef0) at ae.c:397
#7 0x000000010d2494ff in main (argc=1, argv=0x10d2b2900) at redis.c:2046
这显示了回溯,但我们还想使用info registers命令来转储处理器寄存器:
(gdb) info registers
rax 0x0 0
rbx 0x7ffc32005000 140721147367424
rcx 0x10d2b0a60 4515891808
rdx 0x7fff76f1c0b0 140735188943024
rsi 0x10d299777 4515796855
rdi 0x0 0
rbp 0x7fff6ce40730 0x7fff6ce40730
rsp 0x7fff6ce40650 0x7fff6ce40650
r8 0x4f26b3f7 1327936503
r9 0x7fff6ce40718 140735020271384
r10 0x81 129
r11 0x10d430398 4517462936
r12 0x4b7c04f8babc0 1327936503000000
r13 0x10d3350a0 4516434080
r14 0x10d42d9f0 4517452272
r15 0x10d430398 4517462936
rip 0x10d26cfd4 0x10d26cfd4 <debugCommand+68>
eflags 0x10246 66118
cs 0x2b 43
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
请确保包含这两个输出在你的错误报告中。
获取核心文件
下一步是生成核心转储,即正在运行的Redis进程的内存映像。这是使用gcore
命令完成的:
(gdb) gcore
Saved corefile core.58414
现在你已经有了要发送给Redis开发人员的核心转储文件,但重要的是要理解,这个文件包含了Redis实例在崩溃时的所有数据;Redis开发人员将确保不会与任何人共享这些内容,并且一旦不再用于调试目的,将立即删除该文件,但你被警告,通过发送核心文件,你也在发送你的数据。
发送给开发者的内容
最后,您可以将所有内容发送给Redis核心团队:
- 您正在使用的Redis可执行文件。
- 由bt命令生成的堆栈跟踪,以及寄存器转储。
- 您使用gdb生成的核心文件。
- 有关您正在使用的操作系统、GCC版本和Redis版本的信息。
谢谢
您的帮助极为重要!许多问题只能通过这种方式追踪。所以感谢您!