严正声明:本文系作者davidhopper原创,未经许可,不得转载。
我之前写了一篇博客使用Visual Studio Code编译、调试Apollo项目,基本能满足Apollo项目的调试需求,但直接在终端中使用GDB调试Apollo项目,灵活性更强。本文简介借助GDB调试Apollo项目的基本方法,希望给大家学习Apollo带来一定的帮助。
# 进入Apollo项目根目录(我的路径为:`~/code/apollo`,你需要修改为自己的路径)
cd ~/code/apollo
# 启动Apollo项目的Docker(注意:2.0以上版本在后面加上一个“-C”选项,
# 表示从中国服务器拉取镜像文件,以加快下载速度)
bash docker/scripts/dev_start.sh
# 进入Docker
bash docker/scripts/dev_into.sh
使用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
以planning
模块为例说明。完成第2步编译后,会在/apollo/bazel-bin/modules/planning
目录中生成可执行文件planning
。
首先启动Apollo后台核心进程和Dreamview:
bash scripts/bootstrap.sh
在Dreamview中开启与planning
模块相关的其他模块进程,这个需要根据实际情况确定,无法给出统一的操作方法。
接下来,可以使用如下几种方法启动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
进入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) 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---
实际使用时,可以将VSCode和GDB结合起来使用。一般使用VSCode上方的文本编辑器显示源代码文件,在VSCode下方的终端窗口进行GDB调试,如下图所示:
在进入GDB界面后,使用b
命令设置相关断点,使用r
命令启动待调试进程,待运行至断点处后,再根据具体需要合理使用n
、s
、c
、p
、bt
等命令进行单步调试。
注意:如果使用attach模式附加到已有进程PID调试,则不能使用r
命令启动进程,而必须使用c
命令继续执行当前进程。否则,GDB永远不会跳转至你所设置的断点处。
调试过程中,如果想执行某条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调试界面后,使用如下命令进行日志输出设置:
# 打开调试日志,这时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
考虑到某些特殊情形,可能需要暂时退出当前调试,待下次重启调试时又希望自动加载当前所有断点,GDB完全支持该需求,操作方法如下:
# 在GDB调试界面,将当前设置的所有断点全部保存到文件/apollo/data/log/a.break
save b /apollo/data/log/a.break
# 在GDB调试界面,从文件/apollo/data/log/a.break中恢复所有断点
source /apollo/data/log/a.break
在调试过程中,不可避免地需要重新编译代码,这时不必退出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()
函数:
顺利编译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会在断点planning.cc:206
处暂停,使用指令n
执行单步调试,可观察到语句AINFO << "Planning started";
未被注释:
GDB同样在断点planning.cc:206
处暂停,使用指令l
显示断点附件处的代码,可观察到语句AINFO << "Planning started";
已被注释,代码修改生效:
注意:如果使用attach模式附加到已有进程PID调试Planning
模块,该方法不会生效。