在Docker内使用GDB调试Apollo项目的核心转储(Core Dump)文件

严正声明:本文系作者davidhopper原创,未经许可,不得转载!

在Linux系统中,若程序异常终止,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为叫做Core Dump(中文一般译为“核心转储”)。实际上,除内存信息之外,核心转储还会记录程序的一些关键运行状态,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息等。核心转储对于程序员调试程序非常有益,因为有些程序错误是很难重现的,例如指针异常,而核心转储文件可以重现程序出错时的情景。

一、Ubuntu 16.04系统中打开核心转储功能

Ubuntu 16.04系统默认关闭了核心转储功能,需重新设置打开。

1. 检查核心转储是否打开

按快捷键“Ctrl+Alt+T”打开命令终端,输入命令:

ulimit -c

若输出的结果为 0,则说明默认是关闭核心转储功能的,即当程序异常终止时,不会生成核心转储文件。

2. 在当前命令终端中打开核心转储

使用命令:

ulimit -c unlimited

可开启当前命令终端的核心转储功能,并且不限制核心转储文件大小; 若需限制文件大小,将 unlimited 修改为你所需文件的大小,注意单位为KB。

3. 永久打开核心转储

若想永久打开核心转储功能,则使用命令:

sudo vi /etc/security/limits.conf

修改配置文件(我这里是使用vi编辑器修改,你可以换成自己熟悉的编辑器,但建议修改配置文件还是采用vi较好,因为它是所有Unix/Linux系统标配的编辑器,并且简单的操作并不困难),增加如下所示的一行内容:

#Each line describes a limit for a user in the form:
#
#<domain>   <type>   <item>   <value>
    *          soft     core   unlimited

4. 配置核心转储文件名是否添加PID

默认的核心转储文件名称为 core。通过修改 /proc/sys/kernel/core_uses_pid 文件可以让生成的core 文件名是否自动加上 pid 号。使用命令:

sudo vi /proc/sys/kernel/core_uses_pid

/proc/sys/kernel/core_uses_pid 文件里的“0”修改为“1”,然后保存退出,这样生成的 core 文件名将会变成 core.pid,其中 pid 表示该进程的 PID

说明:使用vim编辑器修改/proc/sys/kernel目录下的文件,可能会出现如下错误,导致无法保存:

"/proc/sys/kernel/core_uses_pid"
WARNING: The file has been changed since reading it!!!
Do you really want to write to it (y/n)?y
"/proc/sys/kernel/core_uses_pid" E667: Fsync failed
Press ENTER or type command to continue

解决办法如下:
不使用Vim编辑,直接通过命令行写入:

echo "1" | sudo dd of=/proc/sys/kernel/core_uses_pid

5. 配置核心转储文件的生成位置及文件名格式

默认的核心转储文件保存在可执行文件所在的目录下,可以通过修改 /proc/sys/kernel/core_pattern 文件来控制 core 文件的生成位置以及文件名格式。使用命令:

sudo vi /proc/sys/kernel/core_pattern

可对 core 文件的生成位置以及文件名格式进行配置,以下是几种配置示例:

# 示例1:将生成的core文件保存在/apollo/data/core目录下,
# 文件名格式:“core_进程名.进程PID”
/apollo/data/core/core_%e.%p
# 示例2:将生成的core文件保存在/tmp/core目录下,
# 文件名格式:“core_进程名_进程PID.时间戳”
/tmp/core/core_%e_%p.%t
# 示例3:这是Ubuntu默认的core文件生成方式。
# “apport”是一个用python写的脚本程序,
# 其作用是在可执行文件目录下生成core文件,
# %p %s %c %d %P分别表示:<pid> <signal number> 
# <core file ulimit> <dump mode> [global pid]
/usr/share/apport/apport %p %s %c %d %P

注意:以上示例只能使用其中一个,关于core文件的详细命名格式,可以通过man core命令查看。
说明:使用vim编辑器修改/proc/sys/kernel目录下的文件,可能会出现如下错误,导致无法保存:

"/proc/sys/kernel/core_pattern"
WARNING: The file has been changed since reading it!!!
Do you really want to write to it (y/n)?y
"/proc/sys/kernel/core_pattern" E667: Fsync failed
Press ENTER or type command to continue

解决办法如下:
不使用Vim编辑,直接通过命令行写入:

echo "/apollo/data/core/core_%e.%p" | sudo dd of=/proc/sys/kernel/core_pattern

二、Ubuntu 16.04系统中调试核心转储文件的一个示例

1. 生成核心转储文件

首先撰写一个C++测试程序,代码如下:

#include <iostream>

int main()
{
    // Attention!
    // The following code will cause a core dump file.
    double *pointer;
    *pointer = 10;

    return 0;
}

Linux系统中使用GCC编译器的编译命令如下:

g++ -g -Wall -std=c++11 *.cpp -o test

注意,上述命令一定要加“-g”选项,生成调试信息,否则后面使用GDB调试核心转储文件时,仍然无法定位程序崩溃点。
运行该程序:

./test

输出结果为:

段错误 (核心已转储)

ls -l的结果如下(我使用示例3所示的core文件生成方式):

总用量 584
-rw------- 1 davidhopper davidhopper 565248 Mar 19 17:49 core
-rw-rw-r-- 1 davidhopper davidhopper    163 Mar 19 16:27 main.cpp
-rwxrwxr-x 1 davidhopper davidhopper  25968 Mar 19 17:49 test

可见,已经在当前可执行文件目录中生成了一个核心转储文件:core

2. 使用GDB调试器调试core文件

借助GDB调试器,使用如下命令,可调试core文件:

gdb ./test core

输出信息如下:

GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./test...done.
[New LWP 4340]
Core was generated by `./test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00000000004006c6 in main () at main.cpp:8
8	    *pointer = 10;
(gdb) 
# 说明:
在gdb命令行中,使用bt命令可以查看完整的调用堆栈,这个非常有用。使用q命令退出gdb命令行。

可见,GDB调试器根据生成的core文件,已经找到了程序崩溃点为原程序的第8行。看到这里,是不是感觉到了GDB调试核心转储文件的威力?

三、Ubuntu 16.04系统中调试Apollo项目核心转储文件的方法

由于Apollo项目是在Docker中运行,因此不能直接在Ubuntu 16.04系统中直接生成核心转储文件并使用GDB对其进行调试,所有的工作必须在Docker中完成。具体操作步骤如下:

1. 启动并进入Apollo项目的Docker

# 进入Apollo项目根目录(我的路径为:~/code/apollo,你需要修改为自己的路径)
cd ~/code/apollo
# 启动Apollo项目的Docker(注意:2.0以上版本在后面加上一个“-C”选项,
# 表示从中国服务器拉取镜像文件,以加快下载速度)
bash docker/scripts/dev_start.sh
# 进入Docker
bash docker/scripts/dev_into.sh

2. 在Docker内部检查并设置核心转储功能

在Docker内部,使用本文 第一部分内容检查并设置核心转储功能。在我的机器上,使用ulimit -c命令检查的结果为:unlimited,表明已打开核心转储功能。假如在你的Docker内部发现未开启核心转储功能,该怎么办?那就按照本文第一部分内容重新打开呗。
同样在我的Docker内部,使用命令:cat /proc/sys/kernel/core_pattern查看核心转储文件的生成位置及文件名格式,得到的结果为:/apollo/data/core/core_%e.%p,表明Docker内部的核心转储文件被保存在/apollo/data/core目录下,文件名格式:core_进程名.进程PID。当然,你也可以按照本文第一部分内容对核心转储文件的保存位置及文件名格式进行定制。

3. 在Docker内部调试各功能模块生成的核心转储文件的方法

在Apollo项目Docker内部,所有功能模块的可执行文件均被放置于/apollo/bazel-bin/modules。下面以规划(planning)模块为例进行说明。最近,我修改了规划模块内部的RTKReplayPlanner类。在通过Dreamview调试规划模块时,经常发现该模块莫名其妙地退出,看日志文件没有任何可用信息,根据我的编程经验,这一定是我在某处的指针使用存在问题,要么是引用了空指针,要么是指针越界,如此等等,不一而足。是时候让核心转储文件发挥作用了。我打开/apollo/data/core目录,果然找到了规划模块崩溃时生成的核心转储文件:core_planning.695,于是立刻在Docker内部(即使用bash docker/scripts/dev_into.sh命令进入Docker后的命令行终端内操作)借助GDB调试该文件,命令如下所示。注意:若需定位程序崩溃位置,必须在构建Apollo项目时,添加调试信息。也就是说,构建命令不能使用“build_opt”或“build_opt_gpu”等优化选项,而应使用“build”或“build_gpu”等带调试信息的选项。

gdb /apollo/bazel-bin/modules/planning/planning /apollo/data/core/core_planning.695

2019年4月16日更新
Apollo 3.5以上版本使用Cyber RT进行任务调度与通信,调试核心转储文件的命令更新为(进入GDB后的操作方法相同):

gdb /apollo/bazel-bin/cyber/mainboard/mainboard /apollo/data/core/core_mainboard.297

2019年5月7日更新
如果不知道核心转储文件到底是由哪个程序生成,可使用如下命令查询:

# 在Docker内部
gdb -c core_mainboard.297
# 显示的信息中,会出现“Core was generated by 
# `mainboard -d /apollo/modules/planning/dag/planning.dag'”之类的信息,
# 显然,mainboard就是生成核心转储文件程序。

# 如果想查找mainboard的具体路径,可在GDB内部使用如下命令:
info auxv      
# 显示的信息中,索引31对应的就是生成core dump文件的应用程序    

如下图所示:
gdb_c_core_dump
调试结果如下:

GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /apollo/bazel-bin/modules/planning/planning...done.
[New LWP 747]
[New LWP 697]
[New LWP 695]
[New LWP 698]
[New LWP 709]
[New LWP 699]
[New LWP 714]
[New LWP 700]
[New LWP 746]
[New LWP 750]
[New LWP 749]
[New LWP 711]
[New LWP 702]
[New LWP 703]
[New LWP 704]
[New LWP 705]
[New LWP 706]
[New LWP 707]
[New LWP 708]
[New LWP 712]
[New LWP 745]
[New LWP 748]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `/apollo/bazel-bin/modules/planning/planning --flagfile=modules/planning/conf/pl'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000000000042f476 in apollo::common::TrajectoryPoint::relative_time (this=0x0) at bazel-out/local-dbg/genfiles/modules/common/proto/pnc_point.pb.h:1514
1514	  return relative_time_;
(gdb) 

上述结果表明,在bazel-out/local-dbg/genfiles/modules/common/proto/pnc_point.pb.h文件的1514行返回relative_time_时,this指针为空,即引用了一个空指针。显然,这里只是错误暴露处,而非错误产生处。
小提示:可以使用命令bt查看调用堆栈
联想到我修改了RTKReplayPlanner类,于是我立即在modules/planning/planner/rtk/rtk_replay_planner.cc中查找关键字:relative_time,找到相关代码处(注意:下面的代码是我修改后的内容,并非Apollo项目原有代码):

// reset relative time
  double zero_time = current_trajectory[matched_index].relative_time();
  for (auto& trajectory_point : trajectory_points) {
    // davidhopper
    // We shoud add the "planning_init_point.relative_time()" to
    // maintain the correct time sequence.
    trajectory_point.set_relative_time(trajectory_point.relative_time() -
                                       zero_time +
                                       planning_init_point.relative_time());

最终结果水落石出,原来是double zero_time = current_trajectory[matched_index].relative_time();作了越界引用。找到了错误产生原因,代码修改方法也就比较容易了,对matched_index的范围作出限制即可解决问题。

  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值