C++服务器开发精髓
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.4 gdb调试

调试是开发过程中不可或缺的工作,在Linux编程中通常使用gdb来调试C/C++程序。本节以调试Redis代码为例,讲解gdb调试的常见知识和经验总结。

2.4.1 被调试的程序需要带调试信息

在调试某个程序时,为了能清晰地看到调试的每一行代码、调用的堆栈、变量名、函数名等信息,我们需要在调试程序中加上调试符号信息,即在使用gcc编译程序时需要加上-g 选项。举个例子,通过以下命令将生成一个带调试信息的 hello_server 程序(hello_server.c为任意cpp文件):

那么如何判断hello_server是否带有调试信息呢?我们使用gdb来调试这个程序,gdb会显示正确读取该程序的调试信息:

在 gdb 加载成功以后,会显示一行“Reading symbols from/root/testclient/hello_server...done.”信息,表示读取符号文件完毕,说明该程序带有调试信息。我们不加-g选项再试试:

细心的读者应该看出差别了,这次不加-g 选项,用 gdb 调试生成的 hello_server2 程序,读取调试符号信息时提示:

当然,这里顺便提一下,除了不加-g选项,也可以使用 Linux的 strip命令移除某个程序中的调试信息。这里对hello_server使用strip命令试试:

可以发现,我们对hello_server使用strip命令之后,这个程序明显变小了(由12416字节减小为6312字节)。我们通常会在程序测试没问题后,将其发布到生产环境或者正式环境中,生成不带调试符号信息的程序,以减小程序体积或提高程序执行效率。

再用gdb验证这个程序的调试信息是否被移除:

这里需要补充两个说明。

(1)这里举的例子虽然以gcc为例,但-g选项实际上同样适用于使用makefile、cmake等工具编译生成的Linux程序。

(2)在实际生成调试程序时,我们一般不仅要加上-g 选项,也被建议关闭编译器程序的优化选项。编译器程序的优化选项一般有 5个级别,即 O0~O4(O0是字母 O加上数字0),其中,O0表示不优化(关闭优化),从O1到O4,优化级别越来越高,O4级别最高。关闭优化的目的是在调试时符号文件显示的调试变量等能与源代码完全对应。举个例子,假设有以下代码:

在以上代码中,因为在 main 函数中调用了 func 函数,由于调用 func 函数得到的返回值可以在编译期间直接算出来,所以如果开启了优化选项,则可能在实际调试时这个函数中的局部变量 a、b、c 被编译器优化,取而代之的是直接的值,甚至连 func 函数也可能被优化。如果出现这种情况,则我们在调试时看到的代码和实际的代码可能有所差异,这会给排查和定位问题带来困难。当然,上面说的优化选项是否一定会出现,不同版本的编译器可能有不同的表现。总之,在生成调试文件时建议关闭编译器优化选项(使用 O0选项)。

2.4.2 启动gdb调试的方法

使用gdb调试一个程序一般有三种方法:gdb filename、gdb attach pid、gdb filename corename,接下来逐一进行介绍。

1.方法一 直接调试目标程序

使用如下命令:

其中,filename是我们需要启动的调试程序文件名,这种方式会直接使用gdb启动一个程序进行调试,也就是说这个程序还没有启动。前面使用gdb调试hello_server时就使用了这种方式。

2.方法二 attach到进程

在某些情况下,一个程序已经启动,我们想调试这个程序,但又不想重启这个程序。假设有这样一个场景,我们的聊天测试服务器程序运行了一段时间,却再也无法接受新的客户端连接,那么我们肯定不能为此重启程序,如果重启,则当前程序的各种状态信息就丢失了。这时我们只需使用gdb attach程序的进程ID将gdb调试器attach到我们的聊天测试服务器程序上。假设我们的聊天程序叫作 chatserver,则使用 ps 命令获取该进程的PID,然后gdb attach上去,就可以调试了:

我们得到 chatserver 的 PID 为 42921,然后使用 gdb attach 42921 把 gdb attach 到chatserver进程上:

为了节约篇幅,在以上代码中删掉了一些无关的信息。当出现提示“Attaching to process 42921”时,说明我们已经成功地将gdb attach到目标进程中了。需要注意的是,由于我们的程序使用了一些系统库(如 libc.so),而这是发行版本的 Linux 系统,是没有调试符号的,所以 gdb会提示找不到这些库的调试符号。我们的目的是调试 chatserver,并不关注对系统API调用的内部实现,所以我们可以不关注这些提示。只要在chatserver文件中有调试信息即可。

在使用gdb attach附加上目标进程后,调试器会暂停下来,此时我们可以使用continue命令让程序继续运行,或者加上相应的断点再继续运行程序(不熟悉这里提到的 continue命令也没有关系,下面会详细介绍这些命令的用法)。

若想调试完程序后结束此次调试,且不对当前进程chatserver有任何影响,也就是说想让这个程序继续运行,则可以在 gdb 的命令行界面输入 detach 命令让程序与 gdb 调试器分离,这样chatserver也可以继续运行:

然后退出gdb就可以了:

3.方法三 调试core文件——定位进程崩溃问题

有时,我们的服务器程序在运行一段时间后会突然崩溃。这当然不是我们所希望看到的,我们需要解决这个问题。只要程序在崩溃时有 core 文件产生,我们就可以使用这个core文件定位崩溃的原因。当然,Linux系统默认是不会开启程序崩溃时产生core文件这一功能的,我们可以使用 ulimit-c 来查看系统是否开启了这一功能(顺便提一句,通过ulimit命令不仅可以查看core文件的生成功能是否开启,还可以查看其他功能,例如系统允许的最大文件描述符的数量等):

如上所示,core file size所在的行默认是0,表示关闭生成core文件的选项,如果我们需要修改某个选项的值,则可以通过命令形式“ulimit 选项名 设置值”来修改。例如,可以将 core 文件生成的大小改成具体的某个值(最大允许的字节数)或不限制大小(unlimited),这里直接改成不限制大小,则执行命令ulimit-c unlimited即可:

注意,这个命令很容易出错,第 1 个 ulimit 是 Linux 命令,-c 选项后面的 unlimited是选项的值,表示不限制大小,当然,我们也可以将其改成具体的数值大小。

还有一个问题就是,这样进行修改,关闭这个Linux会话后,这个设置项的值会被还原成0,而我们的服务器程序一般以后台程序(守护进程)长周期地运行,也就是说当前会话虽然被关闭,但服务器程序仍然在后台运行。这样,这个程序在某个时刻崩溃后,是无法产生 core 文件的,不利于排查问题。所以我们希望这个选项永久生效。设置永久生效的方式有以下两种。

(1)在/etc/security/limits.conf中增加一行:

这里设置的是不限制core文件的大小,也可以将其设置成具体的数值,例如1024表示生成的core文件最大为1024KB。

(2)把ulimit-c unlimited行加到/etc/profile文件中,放到这个文件最后一行即可,修改成功后执行source/etc/profile可以让配置立即生效。当然,这只作用于root用户,如果想仅仅作用于某一用户,则可以把 ulimit-c unlimited 行加到该用户对应的~/.bashrc或~/.bash_profile文件中。

生成的core文件的默认命名方式是core.pid,其位置是崩溃程序所在的目录。举个例子,某个程序在运行时其进程 ID 是 16663,则其崩溃时产生的 core 文件的名称是core.16663。比如我们服务器上的msg_server崩溃,在当前目录下产生了如下core文件:

我们就可以通过这个core.21985文件排查程序崩溃的原因了。调试core文件的命令如下:

其中,filename是程序名,这里是msg_server;corename是core.21985。

我们输入gdb msg_server core.21985启动调试:

可以看到程序在stl_function.h的第235行崩溃,然后通过bt命令查看程序崩溃时的调用堆栈,进一步分析,就能找到崩溃的原因:

堆栈#0~#3是系统库函数的调用序列,是经过反复测试的,一般不存在问题;堆栈#4~#12是我们自己的业务逻辑调用序列,我们可以排查这部分代码进而定位问题。

细心的读者会发现这样一个问题:一个程序在运行时,其 PID是可以获取的,但是在程序崩溃后产生了core文件,尤其是多个程序同时崩溃,我们无法通过 core文件名中的PID来判断对应哪个服务。有以下两种方法解决这个问题。

(1)在程序启动时记录PID:

我们在程序启动时调用上述writePID函数,将程序当时的PID记录到xxserver.pid文件中,这样在程序崩溃时,我们就可以从这个文件中得到进程当时运行的 PID 了,并且可以与默认的core文件名后面的PID匹配。

(2)自定义 core 文件的名称和目录。/proc/sys/kernel/core_uses_pid 可以控制在产生的core文件的文件名中是否添加PID作为扩展,如果添加,则文件的内容为1,否则为0;/proc/sys/kernel/core_pattern可以设置格式化的core文件保存位置或文件名。修改方式如下:

对各个参数的说明如下表所示。

假设我们现在的程序是test,我们设置该程序崩溃时的core文件名如下:

那么最终会在/root/testcore/目录下生成的test的core文件名格式如下:

需要注意的是,用户必须对指定的 core 文件目录有写权限,否则会因为权限不足无法生成core文件。