使用 GDB 调试程序一般有三种方式:

  • gdb filename
  • gdb attach pid
  • gdb filename corename

这也对应着本节课的核心内容:

  • 直接调试目标程序
  • 附加进程
  • 调试 core 文件

接下来我们逐一讲解。

2.1 直接调试目标程序

在开发阶段或者研究别人的项目时,当编译成功生成目标二进制文件后,可以使用 gdb filename 直接启动这个程序的调试,其中 filename 是需要启动的调试程序文件名,这种方式是直接使用 GDB 启动一个程序进行调试。注意这里说的启动一个程序进行调试其实不严谨,因为实际上只是附加(attach)了一个可执行文件,并没有把程序启动起来;接着需要输入run 命令,程序才会真正的运行起来。关于 run 命令后面的课程中会详细介绍。上一课的 GDB 调试 hello_server 系列就是使用的这种方式。

假设现在有一个程序叫 fileserver,使用 gdb fileserver 附加该程序,然后使用run 命令启动该程序。如下图所示:

2.2 附加进程

在某些情况下,一个程序已经启动了,我们想调试这个程序,但是又不想重启这个程序。假设有这样一个场景,我们的聊天测试服务器程序正在运行,运行一段时间之后,发现这个聊天服务器不能接受新的客户端连接了,这时肯定是不能重启程序的,如果重启,当前程序的各种状态信息就丢失了。怎么办呢?可以使用gdb attach 进程 ID 来将 GDB 调试器附加到聊天测试服务器程序上。例如,假设聊天程序叫 chatserver,可以使用 ps 命令获取该进程的 PID,然后使用 gdb attach 就可以调试了,操作如下:

[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

实际执行如下图所示:

通过以上代码得到 chatserver 的 PID 为 26621,然后使用 gdb attach 26621 把 GDB 附加到 chatserver 进程,操作并输出如下:

[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]
[New LWP 42928]
[New LWP 42927]
[New LWP 42926]
[New LWP 42925]
[New LWP 42924]
[New LWP 42922]
[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.

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

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

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

(gdb) detach
Detaching from program: /home/zhangyl/flamingoserver/chatserver, process 42921

然后再退出 GDB 就可以了:

(gdb) quit
[zhangyl@localhost flamingoserver]$

2.3 调试 core 文件

有时候,服务器程序运行一段时间后会突然崩溃,这并不是我们希望看到的,需要解决这个问题。只要程序在崩溃的时候有 core 文件产生,就可以使用这个 core 文件来定位崩溃的原因。当然,Linux 系统默认是不开启程序崩溃产生 core 文件这一机制的,我们可以使用 ulimit -c 命令来查看系统是否开启了这一机制。

顺便提一句,ulimit 这个命令不仅仅可以查看 core 文件生成是否开启,还可以查看其他的一些功能,比如系统允许的最大文件描述符的数量等,具体可以使用 ulimit -a 命令来查看,由于这个内容与本课主题无关,这里不再赘述。

[zhangyl@localhost flamingoserver]$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15045
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 4096
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

发现 core file size 那一行默认是 0,表示关闭生成 core 文件,可以使用“ulimit 选项名 设置值”来修改。例如,可以将 core 文件生成改成具体某个值(最大允许的字节数),这里我们使用 ulimit -c unlimitedunlimited 是 -c 选项值)直接修改成不限制大小。

[zhangyl@localhost flamingoserver]$ ulimit -c unlimited
[zhangyl@localhost flamingoserver]$ ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15045
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 4096
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

注意,这个命令容易记错,第一个 ulimit 是 Linux 命令, -c 选项后面的 unlimited 是选项的值,表示不限制大小,当然也可以改成具体的数值大小。很多初学者在学习这个命令时,总是把 ulimit 命令和 unlimited 取值搞混淆,如果读者能理解其含义,一般就不会混淆了。

还有一个问题就是,这样修改以后,当我们关闭这个 Linux 会话,设置项的值就会被还原成 0,而服务器程序一般是以后台程序(守护进程)长周期运行,也就是说当前会话虽然被关闭,服务器程序仍然继续在后台运行,这样这个程序在某个时刻崩溃后,是无法产生 core 文件的,这种情形不利于排查问题。因此,我们希望这个选项永久生效,永久生效的方式是把“ulimit -c unlimited”这一行加到 /etc/profile 文件中去,放到这个文件最后一行即可。

具体的例子

生成的 core 文件的默认命名方式是 core.pid,举个例子,比如某个程序当时运行时其进程 ID 是 16663,那么它崩溃产生的 core 文件的名称就是 core.16663。我们来看一个具体的例子,某次我发现服务器上的 msg_server 崩溃了,产生了一个如下的 core 文件:

-rw------- 1 root root 10092544 Sep  9 15:14 core.21985

就可以通过这个 core.21985 文件来排查崩溃的原因,调试 core 文件的命令是:

gdb filename corename

其中,filename 就是程序名,这里就是 msg_server;corename 是 core.21985,我们输入 gdb msg_server core.21985 来启动调试:

[root@myaliyun msg_server]# gdb msg_server core.21985
Reading symbols from /root/teamtalkserver/src/msg_server/msg_server...done.
[New LWP 21985]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `./msg_server -d'.
Program terminated with signal 11, Segmentation fault.
#0 0x00000000004ceb1f in std::less<CMsgConn*>::operator() (this=0x2283878, __x=@0x7ffca83563a0: 0x2284430, __y=@0x51: <error reading variable>)
at /usr/include/c++/4.8.2/bits/stl_function.h:235
235 { return __x < __y; }

可以看到程序崩溃的地方是在 stl_function.h 的第 235 行,然后通过 bt 命令(后续将详细介绍该命令)查看崩溃时的调用堆栈,进一步分析就能找到崩溃的原因。

(gdb) bt
#0 0x00000000004ceb1f in std::less<CMsgConn*>::operator() (this=0x2283878, __x=@0x7ffca83563a0: 0x2284430, __y=@0x51: <error reading variable>)
at /usr/include/c++/4.8.2/bits/stl_function.h:235
#1 0x00000000004cdd70 in std::_Rb_tree<CMsgConn*, CMsgConn*, std::_Identity<CMsgConn*>, std::less<CMsgConn*>, std::allocator<CMsgConn*> >::_M_get_insert_unique_pos
(this=0x2283878, __k=@0x7ffca83563a0: 0x2284430) at /usr/include/c++/4.8.2/bits/stl_tree.h:1324
#2 0x00000000004cd18a in std::_Rb_tree<CMsgConn*, CMsgConn*, std::_Identity<CMsgConn*>, std::less<CMsgConn*>, std::allocator<CMsgConn*> >::_M_insert_unique<CMsgConn* const&> (this=0x2283878, __v=@0x7ffca83563a0: 0x2284430) at /usr/include/c++/4.8.2/bits/stl_tree.h:1377
#3 0x00000000004cc8bd in std::set<CMsgConn*, std::less<CMsgConn*>, std::allocator<CMsgConn*> >::insert (this=0x2283878, __x=@0x7ffca83563a0: 0x2284430)
at /usr/include/c++/4.8.2/bits/stl_set.h:463
#4 0x00000000004cb011 in CImUser::AddUnValidateMsgConn (this=0x2283820, pMsgConn=0x2284430) at /root/teamtalkserver/src/msg_server/ImUser.h:42
#5 0x00000000004c64ae in CDBServConn::_HandleValidateResponse (this=0x227f6a0, pPdu=0x22860d0) at /root/teamtalkserver/src/msg_server/DBServConn.cpp:319
#6 0x00000000004c5e3d in CDBServConn::HandlePdu (this=0x227f6a0, pPdu=0x22860d0) at /root/teamtalkserver/src/msg_server/DBServConn.cpp:203
#7 0x00000000005022b3 in CImConn::OnRead (this=0x227f6a0) at /root/teamtalkserver/src/base/imconn.cpp:148
#8 0x0000000000501db3 in imconn_callback (callback_data=0x7f4b20 <g_db_server_conn_map>, msg=3 '\003', handle=8, pParam=0x0)
at /root/teamtalkserver/src/base/imconn.cpp:47
#9 0x0000000000504025 in CBaseSocket::OnRead (this=0x227f820) at /root/teamtalkserver/src/base/BaseSocket.cpp:178
#10 0x0000000000502f8a in CEventDispatch::StartDispatch (this=0x2279990, wait_timeout=100) at /root/teamtalkserver/src/base/EventDispatch.cpp:386
#11 0x00000000004fddbe in netlib_eventloop (wait_timeout=100) at /root/teamtalkserver/src/base/netlib.cpp:160
#12 0x00000000004d18c2 in main (argc=2, argv=0x7ffca8359978) at /root/teamtalkserver/src/msg_server/msg_server.cpp:213
(gdb)

堆栈 #4 就不是库代码了,我们可以排查这里的代码,然后找到问题原因。

自定义 core 文件名称

但是细心的读者会发现一个问题:一个正在程序运行时,其 PID 是可以获取到的,但是当程序崩溃后,产生了 core 文件,尤其是多个程序同时崩溃,我们根本没法通过 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 做匹配了。

  • 自定义 core 文件的名称和目录

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

echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

各个参数的说明如下:

参数名称 参数含义(英文) 参数含义(中文)
%p insert pid into filename 添加 pid 到 core 文件名中
%u insert current uid into filename 添加当前 uid 到 core 文件名中
%g insert current gid into filename 添加当前 gid 到 core 文件名中
%s insert signal that caused the coredump into the filename 添加导致产生 core 的信号到 core 文件名中
%t insert UNIX time that the coredump occurred into filename 添加 core 文件生成时间(UNIX)到 core 文件名中
%h insert hostname where the coredump happened into filename 添加主机名到 core 文件名中
%e insert coredumping executable name into filename 添加程序名到 core 文件名中

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

echo "/root/testcore/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

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

-rw-------. 1 root root 409600 Jan 14 13:54 core-test-13154-1547445291

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

2.4 小结

本节课介绍了使用 GDB 调试程序的三种方式,理解并熟练使用这三种方式可以帮助读者在遇到问题时准确地选择调试方法。

第二课:启动 GDB 调试的更多相关文章

  1. 第02课:启动GDB调试

    使用GDB调试程序一般有三种方式: gdb filename gdb attach pid dgb filename corename 也对应这本节课的核心内容: 直接调试目标程序 附加进程 调试co ...

  2. GDB调试笔记

    参考资料:GDB调试精粹及使用实例 # 调试实例 #include <iostream> #include <cstring> using namespace std; ][] ...

  3. gdb调试段错误及使用

    在编程调试中,经常出现段错误,此时可用gdb调试.具体方法为注册段错误信号处理函数,在处理函数中启动gdb.具体代码如下: void segv_handler(int no) { ]; ]; FILE ...

  4. Nginx学习之十四-GDB调试Nginx初试

    本文的测试环境: Win7+虚拟机VMWareVMware-workstation-full-7.1.4-385536+Ubuntu12.04 Nginx-1.4.0 要想有效的研究Nginx源码,必 ...

  5. Linux下GDB调试简单示例

    这里介绍对文件first.c的基本GDB调试操作,只有部分命令,只是一个示例,运行环境为装有gcc编译器和gdb调试器的Linux环境,基本GDB调试命令如下表: 命令                 ...

  6. GDB调试——常用的命令

    首先说明一点,如果我们要使用GDB来调试我们的C/C++程序时,在使用GCC编译程序时,应该带上 –g 参数, 它负责生成 与GDB相关的调试信息: 1.如何对一个文件启动GDB调试? 方法一: 命令 ...

  7. 使用gdb调试

    启用gdb进行调试二进制程序,必须在二进制程序在采用gcc或g++编译时加入-g参数 启动gdb进行调试的几种形式: 直接启动gdb程序进行调试program程序 gdb program 启动gdb挂 ...

  8. gdb调试快速入门

    编译指令 gcc test.c -o test -g -g是加入调试信息,加入源码信息 启动gdb调试 gdb test 进入gdb中 设置参数 set args 10 20 显示参数show age ...

  9. GDB调试指南-启动调试

    前言 GDB(GNU Debugger)是UNIX及UNIX-like下的强大调试工具,可以调试ada, c, c++, asm, minimal, d, fortran, objective-c, ...

随机推荐

  1. 基于 Clusternet 与 OCM 打造新一代开放的多集群管理平台

    背景 随着 5G.物联网设备的爆炸性增长以及智能终端不断增强的计算能力,带来了前所未有的数据量,传统的中心集中式计算捉襟见肘."新基建"战略的实施,工业互联网.车联网/自动驾驶.智 ...

  2. springboot整合javafx

    原文(原码)参考地址: https://github.com/roskenet/springboot-javafx-support https://github.com/spartajet/javaf ...

  3. [JAVA]关于excel的jxl包的操作-创建sheet

    前言 最近玩的游戏需要记录数据, 手打excel太麻烦了 于是就学了学java的jxl操作.本来记录在有道云笔记就可以的,由于乐于分享的精神 就在博客园造了个号, 之前自己本来有做个人的网站的,奈何网 ...

  4. Linux命令(五)之service服务查找、启动/停止等相关操作

    .personSunflowerP { background: rgba(51, 153, 0, 0.66); border-bottom: 1px solid rgba(0, 102, 0, 1); ...

  5. Run Clojure Script with External Dependencies without leiningen

    The normal way of deploy clojure files is using leiningen. But if we have no leiningen, or the scrip ...

  6. Linux 学习Shell一部分指令

    接下来就是shell命令的一些演示了 set (超级多的变量和系统默认值) echo $?查看上一条指令是否执行成功 返回0意味着成功,返回1意味着失败 echo 是个 env 设置变量 解释一下上面 ...

  7. Linux 开机关机在线求助与指令输入

    由于那本书版本稍稍有点早,我就跳过学习第二章第三章了(分别是主机规划和虚拟机安装)下图为在自己电脑上安装好的redhat7 4.1 我们来登入 其中第一行是Linux发行商和发行版本(欸,我的这个怎么 ...

  8. MySQL-20-MySQL优化

    MySQL优化哲学 1 为什么优化? 为了获得成就感? 为了证实比系统设计者更懂数据库? 为了从优化成果来证实优化者更有价值? 但通常事实证实的结果往往会和你期待相反!优化有风险,涉足需谨慎! 2 优 ...

  9. Java Swing 空布局

    Swing 空布局 试了盒布局,说实话不太会用,很多地方都没法更加的细节,又翻了翻资料,知道了还有一个空布局,一看,真不错,很适合我这种菜鸡 用坐标就可以完成界面的布局,不错 话不多说,直接代码 pa ...

  10. ETL需求要求

    前言 ETL落地dw层,dw层各表一般是由多个表关联取数得到的大宽表,在ETL需求中的dw设计应该考虑以下内容,目的是确保需求更清晰,开发和测试才能更高效的进行. 业务需求为基础 基于业务需求做足够多 ...