Skip to content

获取回溯(Backtrace)

本文档介绍为 ROS 2 和 Nav2 获取回溯(backtrace)的几种方法。获取回溯的方式不止一种,但对没有 GDB 经验的 C++ 开发新手而言,本文是一个很好的起点。

以下步骤向 ROS 2 用户展示如何在遇到问题时调整 Nav2 技术栈,从特定服务器获取回溯。本教程同样适用于仿真机器人和实体机器人。

本文涵盖:用 ros2 run 从特定节点获取回溯、用 ros2 launch 从表示单个节点的启动文件获取回溯,以及从更复杂的多节点编排中获取回溯。学完本教程后,当 ROS 2 中的某个服务器崩溃时,你应该就能顺利获取回溯。

GDB 是 Unix 系统上最流行的 C++ 调试器,可用于确定崩溃原因、跟踪线程,也可在代码中设置断点、检查特定位置的内存值。

熟练使用 GDB 是所有 C/C++ 开发工程师的关键技能。许多 IDE 内置了调试器或性能分析器,但适配 ROS 的 IDE 很少。因此,掌握这些底层工具而非依赖 IDE 就显得尤为重要。此外,理解这些工具本身就是 C/C++ 开发的基本功——如果完全交给 IDE,一旦更换工具、或需要通过 ssh 会话对远程设备进行临时调试,就会束手无策。

好在掌握基础之后,GDB 的使用相当简单。第一步是为需要剖析或调试的 ROS 软件包在编译标志中添加 -g。该标志会生成调试符号(debug symbols),GDB 和 valgrind 通过读取这些符号,就能告诉你具体是哪一行代码出了什么问题。如果不设置此标志,仍然可以获得回溯,但其中不会包含出错的行号。调试完成后务必移除该标志,因为它会影响运行时性能。

在项目的 CMakeLists.txt 中添加以下内容即可。如果你的项目已有 add_compile_options(),直接在其中添加 -g。然后用 colcon build --packages-select <package-name> 重新构建工作空间。编译时间可能比平时稍长。

add_compile_options(-g)

现在可以调试代码了。如果是非 ROS 项目,你可能会像下面这样做:启动一个 GDB 会话并让程序立即运行。程序崩溃后会返回一个 GDB 提示符 (gdb),在该提示符下就可以查看所需信息。但对于 ROS 项目来说,节点配置和依赖较多,对初学者或不太熟悉命令行和文件系统的人来说,这种直接使用 GDB 的方式并不友好。

Terminal window
gdb ex run --args /path/to/exe/program

以下各节描述基于 ROS 2 的系统中最常见的 3 种情况,请阅读与你的需求最匹配的那一节。

和非 ROS 示例一样,我们需要在启动 ROS 2 节点之前设置 GDB 会话。虽然可以凭借对 ROS 2 文件系统的了解手动通过命令行设置,但更方便的做法是使用 --prefix 选项。

--prefix 会在 ros2 命令之前执行一段指定的代码,从而在命令执行前插入所需内容。如果直接尝试 gdb ex run --args ros2 run <pkg> <node>,你会发现它找不到 ros2 命令;即便尝试 source 工作空间也会因类似原因失败。

与其费劲查找可执行文件的安装路径并手动输入,不如改用 --prefix,这样就能继续使用你熟悉的 ros2 run 语法,而无需操心 GDB 的细节。

Terminal window
ros2 run --prefix 'gdb -ex run --args' <pkg> <node> --all-other-launch arguments

和之前一样,该前缀会启动一个 GDB 会话,并以所有附加的命令行参数运行请求的节点。此时节点应该已经运行起来,并伴随一些调试输出。

一旦服务器崩溃,你会看到类似下面的提示符,此时就可以获取回溯了。

Terminal window
(gdb)

在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。例如:

Terminal window
(gdb) backtrace
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007ffff79cc859 in __GI_abort () at abort.c:79
#2 0x00007ffff7c52951 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3 0x00007ffff7c5e47c in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4 0x00007ffff7c5e4e7 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007ffff7c5e799 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6 0x00007ffff7c553eb in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x000055555555936c in std::vector<int, std::allocator<int> >::_M_range_check (
this=0x5555555cfdb0, __n=100) at /usr/include/c++/9/bits/stl_vector.h:1070
#8 0x0000555555558e1d in std::vector<int, std::allocator<int> >::at (this=0x5555555cfdb0,
__n=100) at /usr/include/c++/9/bits/stl_vector.h:1091
#9 0x000055555555828b in GDBTester::VectorCrash (this=0x5555555cfb40)
at /home/steve/Documents/nav2_ws/src/gdb_test_pkg/src/gdb_test_node.cpp:44
#10 0x0000555555559cfc in main (argc=1, argv=0x7fffffffc108)
at /home/steve/Documents/nav2_ws/src/gdb_test_pkg/src/main.cpp:25

在这个例子中,应该从底部开始阅读:

  • main 函数第 25 行调用了 VectorCrash。

  • VectorCrash 第 44 行在 Vector 的 at() 方法中崩溃,传入参数为 100。

  • 最终在 STL vector 的 at()(第 1091 行)中崩溃,原因是范围检查失败抛出了异常。

阅读回溯需要一点时间来适应。通常的做法是:从底部开始,沿着调用栈向上跟踪,直到找到崩溃的那一行,由此推断崩溃原因。用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。

与非 ROS 示例一样,我们需要在启动 ROS 2 启动文件之前设置 GDB 会话。虽然可以通过命令行手动完成,但更方便的做法是沿用 ros2 run 节点示例中的机制,改用启动文件来实现。

在启动文件中,找到你想要调试的节点。本节假设你的启动文件只包含一个节点(当然也可能包含其他内容)。launch_ros 包中的 Node 函数接受一个 prefix 字段,用于传入前缀参数列表。我们将在这里插入 GDB 片段,但与节点示例相比有一处不同:使用 xterm。xterm 会弹出一个新的终端窗口来显示 GDB 并与之交互。这样做是因为启动文件在处理 stdin 时存在问题(例如,当你按下 Ctrl+C 时,目标到底是 GDB 还是 launch?)。更多信息请参见这个 ticket。下面是一个调试 SLAM Toolbox 的示例:

start_sync_slam_toolbox_node = Node(
parameters=[
get_package_share_directory("slam_toolbox") + '/config/mapper_params_online_sync.yaml',
{'use_sim_time': use_sim_time}
],
package='slam_toolbox',
executable='sync_slam_toolbox_node',
name='slam_toolbox',
prefix=['xterm -e gdb -ex run --args'],
output='screen')

和之前一样,该前缀会启动一个 GDB 会话,这次是在 xterm 窗口中,并以所有附加的启动参数运行请求的启动文件。

一旦服务器崩溃,你会看到类似下面的提示符(位于 xterm 会话中),此时就可以获取回溯了。

Terminal window
(gdb)

在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。示例参见上一节。

用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。阅读回溯的方法参见上一节。

处理包含多个节点的启动文件时,方法略有不同,目的是让你在与 GDB 会话交互时不受同一终端中其他日志的干扰。因此,在处理较大的启动文件时,最好把感兴趣的服务器单独拎出来启动。以下说明以 Nav2 为例,但也适用于任何在多个启动文件中包含大量节点的大型项目。

当发现想要调查的崩溃时,将该服务器与其他服务器分开启动会更方便。

如果目标服务器是从嵌套的启动文件(例如一个被 include 的启动文件)中启动的,可以这样做:

  • 从父启动文件中注释掉该启动文件的包含语句

  • 用 -g 标志重新编译目标软件包以生成调试符号

  • 在一个终端中启动父启动文件

  • 按照从启动文件获取一节的说明,在另一个终端中启动该服务器的启动文件

如果目标服务器是直接在这些文件中启动的(例如 Node、LifecycleNode,或在 ComponentContainer 内部),则需要把它与其他节点分开:

  • 从父启动文件中注释掉该节点

  • 用 -g 标志重新编译目标软件包以生成调试符号

  • 在一个终端中启动父启动文件

  • 按照从节点获取一节的说明,在另一个终端中启动该服务器的节点

注意:在这种情况下,如果该节点之前由启动文件提供参数文件,你可能需要为它重映射或提供参数文件。通过 --ros-args 可以指定新参数文件的路径、重映射或节点名称。所需命令行参数请参见这个 ROS 2 教程。

这样做确实比较麻烦,因此建议尽量让每个节点都可以作为独立的启动文件单独启动,以便更容易调试。一组示例参数模板:--ros-args -r __node:=<node_name> --params-file /absolute/path/to/params.yaml。

一旦服务器崩溃,你会在该服务器的专属终端中看到类似下面的提示符,此时就可以获取回溯了。

Terminal window
(gdb)

在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。示例参见上一节。

用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。阅读回溯的方法参见上一节。

要直接从 nav2 bringup 启动文件进行调试,可以这样做:

  • 在相应启动文件的非组合(non-composed)节点上添加 prefix=['xterm -e gdb -ex run --args']。

  • 用 -g 标志重新编译目标软件包以生成调试符号。

  • 用 ros2 launch nav2_bringup tb3_simulation_launch.py use_composition:=False 正常启动。此时会打开一个独立的 xterm 窗口,目标进程将在 GDB 中运行。

注意:关闭组合(composition)会带来严重的性能影响。如果性能对你很重要,请改用「从大型项目获取」一节中的做法。

一旦服务器崩溃,你会在 xterm 窗口中看到类似下面的提示符,此时就可以获取回溯了。

Terminal window
(gdb)

在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。示例参见上一节。

用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。阅读回溯的方法参见上一节。

backward-cpp 库可生成格式清晰的堆栈跟踪,backward_ros 封装简化了它的集成。

Navigation2 软件包已预编译了 backward_ros,因此当进程崩溃时会自动输出回溯。这样日志中就会附带回溯信息,而不再只是一条简单的错误日志,例如:

Terminal window
[planner_server-13] [INFO] [1754377648.634265021] [planner_server]: Computing path to goal....
[ERROR] [planner_server-13]: process has died [pid 165734, exit code -11, cmd '/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/nav2_planner/planner_server --ros-args --log-level info --ros-args -r __node:=planner_server -r __ns:=/ -p use_sim_time:=True --params-file /tmp/launch_params_ou47e26i -r /tf:=tf -r /tf_static:=tf_static'].

而是会得到带有回溯的详细输出,帮助你定位崩溃来源:

Terminal window
[planner_server-13] [INFO] [1754463292.960594359] [planner_server]: Computing path to goal....
[planner_server-13] Stack trace (most recent call last) in thread 130805:
[planner_server-13] #11 Object "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", at 0xffffffffffffffff, in
[planner_server-13] #10 Object "/usr/lib/x86_64-linux-gnu/libc.so.6", at 0x7509de004c3b, in
[planner_server-13] #9 Object "/usr/lib/x86_64-linux-gnu/libc.so.6", at 0x7509ddf77aa3, in
[planner_server-13] #8 Object "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33", at 0x7509de207db3, in
[planner_server-13] #7 Object "/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/libplanner_server_core.so", at 0x7509de887ee7, in std::__future_base::_Async_state_impl<std::thread::_Invoker<std::tuple<nav2::SimpleActionServer<nav2_msgs::action::ComputePathToPose>::handle_accepted(std::shared_ptr<rclcpp_action::ServerGoalHandle<nav2_msgs::action::ComputePathToPose> >)::{lambda()#1}> >, void>::_M_run()
[planner_server-13] #6 Object "/usr/lib/x86_64-linux-gnu/libc.so.6", at 0x7509ddf7ced2, in
[planner_server-13] #5 Object "/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/libplanner_server_core.so", at 0x7509de8825cc, in std::__future_base::_State_baseV2::_M_do_set(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>*, bool*)
[planner_server-13] #4 Object "/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/libplanner_server_core.so", at 0x7509de8bd380, in std::_Function_handler<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> (), std::__future_base::_Task_setter<std::unique_ptr<std::__future_base::_Result<void>, std::__future_base::_Result_base::_Deleter>, std::thread::_Invoker<std::tuple<nav2::SimpleActionServer<nav2_msgs::action::ComputePathToPose>::handle_accepted(std::shared_ptr<rclcpp_action::ServerGoalHandle<nav2_msgs::action::ComputePathToPose> >)::{lambda()#1}> >, void> >::_M_invoke(std::_Any_data const&)
[planner_server-13] #3 Object "/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/libplanner_server_core.so", at 0x7509de8bc2f4, in nav2::SimpleActionServer<nav2_msgs::action::ComputePathToPose>::work()
[planner_server-13] #2 Object "/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/libplanner_server_core.so", at 0x7509de87b798, in nav2_planner::PlannerServer::computePlan()
[planner_server-13] #1 Object "/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/libplanner_server_core.so", at 0x7509de873ce4, in nav2_planner::PlannerServer::publishPlan(nav_msgs::msg::Path_<std::allocator<void> > const&)
[planner_server-13] #0 Object "/opt/ros/rolling/lib/librclcpp_lifecycle.so", at 0x7509de62bf94, in rclcpp_lifecycle::SimpleManagedEntity::is_activated() const
[planner_server-13] Segmentation fault (Address not mapped to object [0x8])
[ERROR] [planner_server-13]: process has died [pid 129889, exit code -11, cmd '/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/nav2_planner/planner_server --ros-args --log-level info --ros-args -r __node:=planner_server -r __ns:=/ -p use_sim_time:=True --params-file /tmp/launch_params_apyru5sq -r /tf:=tf -r /tf_static:=tf_static'].

使用 RelWithDebInfo 或 Debug 构建配置来编译 Nav2,可以在回溯中获得更详细的信息,包括行号、文件名和相关代码片段:

Terminal window
[planner_server-13] [INFO] [1754377736.955537475] [planner_server]: Computing path to goal....
[planner_server-13] Stack trace (most recent call last) in thread 173639:
[planner_server-13] #11 Object "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", at 0xffffffffffffffff, in
[planner_server-13] #10 Object "/usr/lib/x86_64-linux-gnu/libc.so.6", at 0x795ea7ac8c3b, in
[planner_server-13] #9 Object "/usr/lib/x86_64-linux-gnu/libc.so.6", at 0x795ea7a3baa3, in
⋮
[planner_server-13] #2 Source "/opt/overlay_ws/src/navigation2/nav2_planner/src/planner_server.cpp", line 546, in computePlan [0x795ea8344363]
[planner_server-13] 543: }
[planner_server-13] 544:
[planner_server-13] 545: // Publish the plan for visualization purposes
[planner_server-13] > 546: publishPlan(result->path);
[planner_server-13] 547:
[planner_server-13] 548: auto cycle_duration = this->now() - start_time;
[planner_server-13] 549: result->planning_time = cycle_duration;
[planner_server-13] #1 Source "/opt/overlay_ws/src/navigation2/nav2_planner/src/planner_server.cpp", line 640, in publishPlan [0x795ea833df4a]
[planner_server-13] 637: auto msg = std::make_unique<nav_msgs::msg::Path>(path);
[planner_server-13] 638: RCLCPP_WARN(get_logger(), "Publishing plan with %zu poses", path.poses.size());
[planner_server-13] 639: plan_publisher_.reset();
[planner_server-13] > 640: if (plan_publisher_->is_activated() && plan_publisher_->get_subscription_count() > 0) {
[planner_server-13] 641: plan_publisher_->publish(std::move(msg));
[planner_server-13] 642: }
[planner_server-13] 643: }
[planner_server-13] #0 Object "/opt/ros/rolling/lib/librclcpp_lifecycle.so", at 0x795ea80f1f94, in rclcpp_lifecycle::SimpleManagedEntity::is_activated() const
[planner_server-13] Segmentation fault (Address not mapped to object [0x8])
[ERROR] [planner_server-13]: process has died [pid 172784, exit code -11, cmd '/opt/overlay_ws/src/navigation2/install/nav2_planner/lib/nav2_planner/planner_server --ros-args --log-level info --ros-args -r __node:=planner_server -r __ns:=/ -p use_sim_time:=True --params-file /tmp/launch_params_px13xl6j -r /tf:=tf -r /tf_static:=tf_static'].

如果想在自己的软件包中启用自动回溯,只需将 backward_ros 添加为依赖并在 CMakeLists 中 find_package,backward 库就会自动注入到所有可执行文件和库中。

注意:目前由于 rclcpp 缺少相关支持,backward_ros 无法与 ComposedNodes 配合使用。在该问题解决之前,需要以禁用组合的方式启动 Nav2 技术栈(例如使用 use_composition:=False 启动参数)才能获得自动回溯。