获取回溯(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 的方式并不友好。
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 的细节。
ros2 run --prefix 'gdb -ex run --args' <pkg> <node> --all-other-launch arguments和之前一样,该前缀会启动一个 GDB 会话,并以所有附加的命令行参数运行请求的节点。此时节点应该已经运行起来,并伴随一些调试输出。
一旦服务器崩溃,你会看到类似下面的提示符,此时就可以获取回溯了。
(gdb)在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。例如:
(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 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。
从启动文件获取
Section titled “从启动文件获取”与非 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 会话中),此时就可以获取回溯了。
(gdb)在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。示例参见上一节。
用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。阅读回溯的方法参见上一节。
从大型项目获取
Section titled “从大型项目获取”处理包含多个节点的启动文件时,方法略有不同,目的是让你在与 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。
一旦服务器崩溃,你会在该服务器的专属终端中看到类似下面的提示符,此时就可以获取回溯了。
(gdb)在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。示例参见上一节。
用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。阅读回溯的方法参见上一节。
从 Nav2 Bringup 获取
Section titled “从 Nav2 Bringup 获取”要直接从 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 窗口中看到类似下面的提示符,此时就可以获取回溯了。
(gdb)在提示符下输入 backtrace 即可获取回溯,将其复制保存以备后用。示例参见上一节。
用完 GDB 后输入 quit 退出会话,GDB 会终止所有仍在运行的进程。退出时若询问是否杀死某些线程,选择是即可。阅读回溯的方法参见上一节。
崩溃时自动获取回溯
Section titled “崩溃时自动获取回溯”backward-cpp 库可生成格式清晰的堆栈跟踪,backward_ros 封装简化了它的集成。
Navigation2 软件包已预编译了 backward_ros,因此当进程崩溃时会自动输出回溯。这样日志中就会附带回溯信息,而不再只是一条简单的错误日志,例如:
[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'].而是会得到带有回溯的详细输出,帮助你定位崩溃来源:
[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,可以在回溯中获得更详细的信息,包括行号、文件名和相关代码片段:
[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启动参数)才能获得自动回溯。