使用GDB调试Apollo项目

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

我之前写了一篇博客使用Visual Studio Code编译、调试Apollo项目,基本能满足Apollo项目的调试需求,但直接在终端中使用GDB调试Apollo项目,灵活性更强。本文简介借助GDB调试Apollo项目的基本方法,希望给大家学习Apollo带来一定的帮助。

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内部编译Apollo项目

使用GDB调试Apollo项目必须带有调试符号信息,因此编译Apollo项目时,不能使用opt选项,可根据实际需求使用如下两个编译命令中任意一个进行构建:

# 构建方法1:使用8个线程(根据你的CPU核数确定)编译Apollo项目,不使用GPU也不优化
bash apollo.sh build -j 8
# 构建方法2:使用8个线程(根据你的CPU核数确定)编译Apollo项目,使用GPU但不优化
bash apollo.sh build_gpu -j 8

3. 在Docker内部启动相关模块调试

planning模块为例说明。完成第2步编译后,会在/apollo/bazel-bin/modules/planning目录中生成可执行文件planning
首先启动Apollo后台核心进程和Dreamview:

bash scripts/bootstrap.sh

在Dreamview中开启与planning模块相关的其他模块进程,这个需要根据实际情况确定,无法给出统一的操作方法。
使用GDB调试Apollo项目_第1张图片
接下来,可以使用如下几种方法启动planning模块的调试:

# 启动方法1:直接使用GDB启动planning模块,注意后面的flagfile需根据需要指定,
# 如果是Navigation模式,则有--flagfile=/apollo/modules/planning/conf/planning_navi.conf
# 注意:--args及后面的--flagfile也可以先不设置,而在进入GDB调试界面后,使用set args
# 命令进行设置,下同。
gdb -q --args bazel-bin/modules/planning/planning --flagfile=/apollo/modules/planning/conf/planning.conf

# 启动方法2:借助Apollo提供的脚本程序,注意后面的flagfile需根据需要指定,
# 如果是Navigation模式,则有--flagfile=/apollo/modules/planning/conf/planning_navi.conf
bash scripts/planning.sh start_gdb --flagfile=/apollo/modules/planning/conf/planning.conf

# 启动方法3:在Dreamview中启动Planning模块,然后使用ps aux | grep planning命令查找
# planning进程ID(PID),假设为35872,则使用attach模式附加到当前planning进程调试
sudo gdb -q bazel-bin/modules/planning/planning -p 35872

注意:如果需要调试各模块内部包含的一些小工具程序,则只能使用如下方法启动调试(以modules/planning/reference_line/smoother_util.cc为例进行说明):

# 注意:--args及后面的一串参数也可以先不设置,而在进入GDB调试界面后,使用set args
# 命令进行设置。
gdb -q --args bazel-bin/modules/planning/reference_line/smoother_util --input_file /apollo/data/bag/record_data.txt --smooth_length 200

Apollo 3.5以上版本(基于Cyber RT框架)的调试启动命令
Apollo 3.5以上版本使用Cyber RT进行任务调度与通信,调试功能模块的命令更新为(进入GDB后的操作方法相同):

gdb -q --args /apollo/bazel-bin/cyber/mainboard -d /apollo/modules/planning/dag/planning.dag
sudo gdb -q /apollo/bazel-bin/cyber/mainboard -p 35872

4. 常见GDB调试命令

进入GDB调试界面后,可以使用如下常见命令调试:
注意:因为Apollo项目文件很多,不要过多使用TAB键进行提示,否则可能会出现响应异常缓慢的现象。

命令 作用 示例 解释
l 查看源代码 l 1,20 将GDB当前所在源程序的第1-20行列出来
b 设置断点 b planning.cc:164 在planning.cc文件第164行设置一个断点。注意:若GDB已调试进入源文件planning.cc中,则可以直接使用b 164命令以简化操作。
b if 设置条件断点 b planning.cc:662 if v > 10.0 若速度大于10.0m/s,则在planning.cc文件第662行设置一个断点
info b 显示当前断点 info b 显示当前所有设置的断点
d 删除断点 d num 首先使用info b显示所有断点,然后删除第num个断点
clear 清除当前行的断点 clear 131 清除当前源文件中第131行的断点,不写行数表示当前行
set args 设置运行参数 set args --flagfile=/apollo/modules/planning/conf/planning.conf 设置一个运行参数flagfile,一般在进入GDB界面后,使用命令r运行进程前设置
show args 显示运行参数 show args 显示运行当前进程时从外面传入的参数
r 运行当前进程 r 在进入GDB调试界面并设置完断点后,使用该命令运行当前进程。注意:如果使用attach模式附加到已有进程PID调试,则不能使用r命令启动进程,而必须使用c命令继续执行当前进程。
start 启动进程并停止在main函数入口处 start 启动进程并停止在main函数入口处
c 继续执行当前进程 c 进入某个断点后,使用该命令继续执行当前进程。 注意:如果使用attach模式附加到已有进程PID调试,必须使用c命令继续执行当前进程。
n 单步执行 n 相当于VS中的F10,即每次执行一条语句,遇函数调用也当成一条普通语句直接返回执行结果
s 单步执行 s 相当于VS中的F11,即每次执行一条语句,遇函数调用则跳转进入调试
finish 停止调试当前函数 finish 停止调试当前函数跳转到该函数的下条语句,一般用于跳出当前函数调用,相当于Visual Studio中的Shift+F11,注意:不能简写为f,因为简写的f表示frame,即打印当前帧
until 跳出当前循环体 until 当你厌倦了在一个循环体内单步跟踪时,这个命令可以跳出当前循环体
until 行号 运行至某行,不只是跳出循环 until 341 运行至341行
info locals 显示当前的局部变量 info locals 显示当前调用堆栈中的所有局部变量
p 打印变量值 p points.size() 打印points.size()的值
p 打印STL库容器中所有元素的值 p *(container._M_impl._M_start)@container.size() 打印STL库容器变量container的所有内容
p 打印STL库容器中前几个元素的值 p *(container._M_impl._M_start)@3 打印STL库容器变量container中前3个元素内容,注意@3不能超过容器的size
p 打印STL库容器中第几个元素的值 p *(container._M_impl._M_start+1) 打印STL库容器变量container中第二个元素内容,注意1不能超过容器的size-1
set var key = value 更改变量的值 set var init_val = 30 将变量init_val的值更改为30
p key=value 更改变量的值 p init_val = 30 将变量init_val的值更改为30, 与 set var init_val = 30作用相同
bt 显示调用堆栈 bt 显示调用堆栈,该语句在调试core dump文件时特别有用
Ctrl+c 停止当前的GDB指令 Ctrl+c 停止当前的GDB指令,退回GDB命令提示符。注意:如果使用attach模式附加到已有进程PID调试,可能无法退出当前执行的指令,可以通过kill PID命令停止被调试的进程,这时GDB会自动退出。
Ctrl+d 退出GDB调试 Ctrl+d 退出GDB调试,与q作用相同。注意:如果使用attach模式附加到已有进程PID调试,可能无法退出GDB,可以通过kill PID命令停止被调试的进程,这时GDB会自动退出。
q 退出GDB调试 q 退出GDB调试,与Ctrl+d作用相同。注意:如果使用attach模式附加到已有进程PID调试,可能无法退出GDB,可以通过kill PID命令停止被调试的进程,这时GDB会自动退出。

关于打印STL库元素方面更多的内容,可以参考这篇文章:《打印STL容器中的内容》
下图给出了一个显示STL容器调试的一个示例:
使用GDB调试Apollo项目_第2张图片

(gdb) p (*(task_list_._M_impl._M_start))->Name()          
$9 = "LANE_CHANGE_DECIDER"
(gdb) p (*(task_list_._M_impl._M_start+1))->Name()
$10 = "PATH_LANE_BORROW_DECIDER"
(gdb) p (*(task_list_._M_impl._M_start+2))->Name()
$11 = "PATH_BOUNDS_DECIDER"
(gdb) p (*(task_list_._M_impl._M_start+3))->Name()
$12 = "PiecewiseJerkPathOptimizer"
(gdb) p (*(task_list_._M_impl._M_start+4))->Name()
$13 = "PATH_ASSESSMENT_DECIDER"
(gdb) p (*(task_list_._M_impl._M_start+5))->Name()
$14 = "PathDecider"
(gdb) p (*(task_list_._M_impl._M_start+6))->Name()
$15 = "SpeedBoundsDecider"
(gdb) p (*(task_list_._M_impl._M_start+7))->Name()
$16 = "DpStSpeedOptimizer"
(gdb) p (*(task_list_._M_impl._M_start+8))->Name()
$17 = "SpeedDecider"
(gdb) p (*(task_list_._M_impl._M_start+9))->Name()
$18 = "SpeedBoundsDecider"
(gdb) p (*(task_list_._M_impl._M_start+10))->Name()
$19 = "PiecewiseJerkSpeedOptimizer"
(gdb) p (*(task_list_._M_impl._M_start+11))->Name()
$20 = "RssDecider"
(gdb) p (*(task_list_._M_impl._M_start+12))->Name()
$21 = 
(gdb) where
#0  0x00007f66aab24278 in apollo::planning::scenario::lane_follow::LaneFollowStage::PlanOnReferenceLine (this=0x22baa70, planning_start_point=..., frame=0x7f66c00470f0, reference_line_info=0x7f66c004f9e0)
    at modules/planning/scenarios/lane_follow/lane_follow_stage.cc:163
#1  0x00007f66aab23ad4 in apollo::planning::scenario::lane_follow::LaneFollowStage::Process (this=0x22baa70, planning_start_point=..., frame=0x7f66c00470f0) at modules/planning/scenarios/lane_follow/lane_follow_stage.cc:125
#2  0x00007f66a99ba732 in apollo::planning::scenario::Scenario::Process (this=0x22ba5d0, planning_init_point=..., frame=0x7f66c00470f0) at modules/planning/scenarios/scenario.cc:76
#3  0x00007f66ab5f553a in apollo::planning::PublicRoadPlanner::Plan (this=0x2273e30, planning_start_point=..., frame=0x7f66c00470f0, ptr_computed_trajectory=0x7f66247fedf0) at modules/planning/planner/public_road/public_road_planner.cc:51
#4  0x00007f66d0239130 in apollo::planning::NaviPlanning::Plan (this=0x223c1f0, current_time_stamp=1557975960.7090025, stitching_trajectory=std::vector of length 1, capacity 1 = {...}, trajectory_pb=0x7f66247fedf0) at modules/planning/navi_planning.cc:486
#5  0x00007f66d0236cf5 in apollo::planning::NaviPlanning::RunOnce (this=0x223c1f0, local_view=..., trajectory_pb=0x7f66247fedf0) at modules/planning/navi_planning.cc:268
#6  0x00007f66b230c494 in apollo::planning::PlanningComponent::Proc (this=0x1bca110, prediction_obstacles=std::shared_ptr (count 4, weak 0) 0x7f661c076338, chassis=std::shared_ptr (count 7, weak 0) 0x7f661c0663f8, 
    localization_estimate=std::shared_ptr (count 7, weak 0) 0x7f661c05e688) at modules/planning/planning_component.cc:134
#7  0x00007f66b23b36c4 in apollo::cyber::Component::Process (this=0x1bca110, 
    msg0=std::shared_ptr (count 4, weak 0) 0x7f661c076338, msg1=std::shared_ptr (count 7, weak 0) 0x7f661c0663f8, msg2=std::shared_ptr (count 7, weak 0) 0x7f661c05e688) at ./cyber/component/component.h:291
#8  0x00007f66b23a1698 in apollo::cyber::Component::Initialize(apollo::cyber::proto::ComponentConfig const&)::{lambda(std::shared_ptr const&, std::shared_ptr const&, std::shared_ptr const&)#2}::operator()(std::shared_ptr const&, std---Type  to continue, or q  to quit---

5. 调试小技巧

5.1 将VSCode和GDB结合使用

实际使用时,可以将VSCode和GDB结合起来使用。一般使用VSCode上方的文本编辑器显示源代码文件,在VSCode下方的终端窗口进行GDB调试,如下图所示:
使用GDB调试Apollo项目_第3张图片

5.2 启动调试的过程

在进入GDB界面后,使用b命令设置相关断点,使用r命令启动待调试进程,待运行至断点处后,再根据具体需要合理使用nscpbt等命令进行单步调试。
注意:如果使用attach模式附加到已有进程PID调试,则不能使用r命令启动进程,而必须使用c命令继续执行当前进程。否则,GDB永远不会跳转至你所设置的断点处。

5.3 在GDB中执行Shell命令的方法

调试过程中,如果想执行某条shell命令(例如查找某个文件是否存在),可不必退出GDB界面而直接操作,具体方法如下:

# 方法1
shell command-string
# 例如在所有目录中查找apollo.sh是否存在
shell sudo find / -name "apollo.sh"
# 方法2
!command-string
# 例如在所有目录中查找apollo.sh是否存在
!sudo find / -name "apollo.sh"

使用GDB调试Apollo项目_第4张图片

5.4 将调试信息输出到日志文件

有时可能需要将调试信息输出到日志文件,操作方法如下:
进行GDB调试界面后,使用如下命令进行日志输出设置:

# 打开调试日志,这时GDB会在当前目录中生成一个“gdb.txt”文件,并将调试信息输出到该文件
set logging on
# 关闭调试日志
set logging off
# 将调试日志输出到指定文件/apollo/data/log/davidhopper_gdb.txt
set logging file /apollo/data/log/davidhopper_gdb.txt
set logging on

4
5

5.5 保存和加载断点

考虑到某些特殊情形,可能需要暂时退出当前调试,待下次重启调试时又希望自动加载当前所有断点,GDB完全支持该需求,操作方法如下:

5.5.1 保存断点到指定文件

# 在GDB调试界面,将当前设置的所有断点全部保存到文件/apollo/data/log/a.break
save b /apollo/data/log/a.break

6

5.5.2 从指定文件加载断点

# 在GDB调试界面,从文件/apollo/data/log/a.break中恢复所有断点
source /apollo/data/log/a.break

7

5.6 重新编译运行代码

在调试过程中,不可避免地需要重新编译代码,这时不必退出GDB,只需在外部重新编译代码后(也可参考5.3节方法直接在GDB内部重新编译),在GDB内部使用指令r重新运程程序,GDB会自动更新程序状态。下面以一个小实例进行具体说明:
示例很简单,就是将modules/planning/planning.cc中的Status Planning::Start()函数注释一行代码AINFO << "Planning started";,如下所示:

Status Planning::Start() {
  timer_ = AdapterManager::CreateTimer(
      ros::Duration(1.0 / FLAGS_planning_loop_rate), &Planning::OnTimer, this);
  // The "reference_line_provider_" may not be created yet in navigation mode.
  // It is necessary to check its existence.
  if (reference_line_provider_) {
    reference_line_provider_->Start();
  }
  start_time_ = Clock::NowInSeconds();
  // Note that the following line will be commented.
  AINFO << "Planning started";
  return Status::OK();
}
  • 以下是修改之前的modules/planning/planning.cc文件中的Status Planning::Start()函数:
    使用GDB调试Apollo项目_第5张图片

  • 顺利编译Apollo项目后,在Docker内部使用指令gdb -q --args bazel-bin/modules/planning/planning --flagfile=/apollo/modules/planning/conf/planning.conf启动Planning模块调试,使用指令b modules/planning/planning.cc:206设置断点,使用指令r运行Planning模块:
    使用GDB调试Apollo项目_第6张图片

  • GDB会在断点planning.cc:206处暂停,使用指令n执行单步调试,可观察到语句AINFO << "Planning started";未被注释:
    使用GDB调试Apollo项目_第7张图片

  • 接下来,在VSCode中将语句AINFO << "Planning started";注释:
    使用GDB调试Apollo项目_第8张图片

  • 在GDB调试界面中,使用指令!bash apollo.sh build -j 8重新编译Apollo项目: 12

  • 编译成功后,使用指令r并回答y重新运行Planning模块:
    使用GDB调试Apollo项目_第9张图片

  • GDB同样在断点planning.cc:206处暂停,使用指令l显示断点附件处的代码,可观察到语句AINFO << "Planning started";已被注释,代码修改生效:
    使用GDB调试Apollo项目_第10张图片
    注意:如果使用attach模式附加到已有进程PID调试Planning模块,该方法不会生效。

你可能感兴趣的:(GDB)