在ROS 2 / Nav2中获取回溯

概述

本文档介绍了在ROS 2和Nav2中获取回溯的一组方法。有许多实现此目标的方法,但对于没有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会话到远程资源进行开发,依赖IDE可能会带来问题。

幸运的是,一旦掌握了基础知识,使用GDB相当简单。第一步是在要分析/调试的ROS软件包的编译器标志中添加``-g``。此标志会构建调试符号,供GDB和valgrind读取,以告诉您项目中的具体代码行失败的原因。如果不设置此标志,仍然可以获取回溯信息,但不会提供失败的行号。在调试完成后,务必删除此标志,否则会降低运行时性能。

将以下行添加到项目的``CMakeLists.txt``文件中应该就可以解决问题。如果您的项目已经有一个``add_compile_options()``,您只需添加``-g``即可。然后使用此包重新构建工作空间``colcon build --packages-select <package-name>``。编译过程可能比平常稍长一些。

add_compile_options(-g)

现在,您已经准备好调试代码了!如果这是一个非ROS项目,此时您可能会执行以下操作。这里我们启动了一个GDB会话,并告诉我们的程序立即运行。一旦程序崩溃,它将返回一个以 (gdb) 标识的gdb会话提示符。在此提示符下,您可以访问您感兴趣的信息。然而,由于这是一个涉及大量节点配置和其他操作的ROS项目,对于初学者或不喜欢大量命令行操作和理解文件系统的人来说,这不是一个很好的选择。

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

下面是描述使用基于ROS 2的系统可能遇到的三种主要情况的部分。请阅读最能描述你尝试解决的问题的部分。

从一个节点开始

就像在我们的非ROS示例中一样,我们需要在启动ROS 2节点之前设置一个GDB会话。虽然我们可以通过命令行和对ROS 2文件系统的一些了解来设置这个会话,但我们可以使用Open Robotics提供的launch ``--prefix``选项。

--prefix``将在我们的``ros2``命令之前执行一些代码,以便插入一些信息。如果您尝试执行类似于我们在前言中的示例的``gdb ex run --args ros2 run <pkg> <node>,您会发现它找不到``ros2``命令。如果您更聪明一些,您会发现尝试源化您的工作空间也会因类似的原因而失败。

我们不必回到查找可执行文件的安装路径并将其全部输入的状态,而可以使用``--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

在这个例子中,你应该按以下方式阅读,从底部开始:

  • 在主函数中,第25行我们调用了一个名为VectorCrash的函数。

  • 在VectorCrash中,第44行,我们在Vector的``at()``方法中以输入``100``崩溃。

  • 在STL向量的第1091行的``at()``函数中发生了崩溃,并在范围检查失败后抛出了异常。

这些追踪需要一些时间来适应阅读,但一般来说,从底部开始,沿着堆栈向上查看,直到看到崩溃的那一行。然后你可以推断出为什么会崩溃。当你完成GDB操作后,输入``quit``,它将退出会话并关闭任何仍在运行的进程。最后可能会询问是否要终止一些线程,选择是。

从启动文件

与我们非ROS示例一样,在启动ROS 2启动文件之前,我们需要设置一个GDB会话。虽然我们可以通过命令行设置,但我们可以利用与``ros2 run``节点示例中相同的机制来实现,现在使用一个启动文件。

在您的启动文件中,找到您想要调试的节点。在本节中,我们假设您的启动文件只包含一个单独的节点(以及其他可能的信息)。launch_ros``包中使用的``Node``函数将接受一个名为``prefix``的字段,它需要一个前缀参数的列表。我们将在此处插入包含 GDB 代码片段的示例,与我们的节点示例相比有一个变化,即使用``xterm``xterm``将弹出一个新的终端窗口,用于显示和与 GDB 交互。我们之所以这样做,是因为处理启动文件上的``stdin``存在问题(例如,如果您按下Ctrl+C,您是在与GDB 还是启动进行交互?)。有关更多信息,请参阅`此票证<https://github.com/ros2/launch_ros/issues/165>`_。以下是一个关于调试 SLAM 工具箱的示例:

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会话而不受其他日志的干扰。因此,在使用较大的启动文件时,最好将您感兴趣的特定服务器提取出来并单独启动。这些说明针对Nav2,但适用于任何具有多个节点类型的大型项目的一系列启动文件。

因此,在这种情况下,当您遇到需要调查的崩溃时,最好将此服务器与其他服务器分开。

如果您感兴趣的服务器是从嵌套的启动文件(例如,一个包含的启动文件)启动的,您可能需要执行以下操作:

  • 将父启动文件中的启动文件包含部分注释掉。

  • 使用``-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``,它将退出会话并关闭任何仍在运行的进程。最后可能会询问是否要终止一些线程,选择是。

来自Nav2 Bringup

要直接从 nav2 的启动文件中进行调试,您可以执行以下操作:

  • 在适当的启动文件中,将``prefix=['xterm -e gdb -ex run --args']``添加到非组合节点中。

  • 使用 -g 标志重新编译要调试的软件包以获取调试符号。

  • 使用``ros2 launch nav2_bringup tb3_simulation_launch.py use_composition:=False``正常启动。一个单独的 xterm 窗口将打开,其中运行着感兴趣的进程。

注解

关闭组合会严重影响性能。如果这对您很重要,请参考"从大型项目"。

一旦服务器崩溃,您将在 xterm 窗口中看到如下提示。此时,您现在可以获取回溯。

(gdb)

在此会话中,键入``backtrace``,它将为您提供一个回溯。根据您的需求进行复制。请参考上面的示例跟踪。

这些追踪需要一些时间来适应阅读,但一般来说,从底部开始,沿着堆栈向上查看,直到看到崩溃的那一行。然后你可以推断出为什么会崩溃。当你完成GDB操作后,输入``quit``,它将退出会话并关闭任何仍在运行的进程。最后可能会询问是否要终止一些线程,选择是。

崩溃时自动获取回溯信息

``backward-cpp <https://github.com/bombela/backward-cpp>``库提供了美观的堆栈跟踪,而``backward_ros <https://github.com/pal-robotics/backward_ros/tree/foxy-devel>``包装器简化了其集成。

只需将其作为依赖项添加,并在您的CMakeLists中使用`find_package`,然后后向库将被注入到所有可执行文件和库中。