在 ROS 2 中获取回溯

**目标:**展示在 ROS 2 中获取回溯的各种方法

**教程级别:**中级

**时间:**15 分钟

以下步骤向 ROS 2 用户展示了在遇到问题时如何获取回溯。

概述

什么是回溯?

  • 想象一下,您的程序就像一叠煎饼,每个煎饼代表它当前正在执行的一个函数。

回溯就像一张倒塌的煎饼堆的照片,向您展示它们的顺序,揭示程序最终失败的原因。 - 它列出了调用的函数顺序,一个接一个,直到失败点。

为什么它有用?

  • 查明问题:回溯向您显示导致崩溃的确切行号,而不是猜测代码中发生错误的位置。

  • 揭示上下文:您可以看到最终触发失败的事件链(函数调用其他函数)。

这不仅可以帮助您了解出错的位置,还可以了解出错的原因。

视觉类比:一叠煎饼

1. 每个煎饼都是一个函数:想象一下,一叠煎饼中的每一块都代表着程序当前正在执行的一个函数。 最底部的煎饼是 main() 函数,一切从这里开始。

  1. 添加煎饼:每次函数调用另一个函数时,都会在堆栈顶部放置一块新煎饼。

  2. 崩溃:崩溃就像盘子从堆栈底部滑落一样——当前执行的函数中出现了灾难性的错误。

4. 回溯:回溯就像倒下的煎饼堆的照片。 它显示了煎饼(函数)从上到下的顺序,揭示了您最终是如何到达崩溃现场的。

代码示例:

void functionC() {
  // Something bad happens here, causing a crash
}

void functionB() {
    functionC();
}

void functionA() {
    functionB();
}

int main() {
    functionA();
    return 0;
}

崩溃回溯:

#0  functionC() at file.cpp:3 // Crash occurred here
#1  functionB() at file.cpp:8
#2  functionA() at file.cpp:13
#3  main() at file.cpp:18

回溯如何提供帮助:

  • 崩溃起源:向您显示触发崩溃的“functionC()”中的确切行。

  • 调用序列:显示“main()”调用了“functionA()”,后者调用了“functionB()”,最终导致“functionC()”中的错误。

上面的例子让我们清楚地了解了什么是回溯以及它如何有用。 现在,以下步骤向 ROS 2 用户展示如何在遇到问题时从特定节点获取跟踪。 本教程适用于模拟和物理机器人。

这将介绍如何使用“ros2 run”从特定节点获取回溯,如何使用“ros2 launch”从表示单个节点的启动文件获取回溯,以及如何从更复杂的节点编排中获取回溯。 在本教程结束时,当您注意到 ROS 2 中的节点崩溃时,您应该能够获得回溯。

准备工作

GDB 是 Unix 系统上最流行的 C/C++ 调试器。 它可用于确定崩溃的原因并跟踪线程。 它还可用于在代码中添加断点,以检查软件中特定点的内存值。

使用 GDB 是所有使用 C/C++ 的软件开发人员的一项关键技能。 虽然许多 IDE 都内置了某种调试器或分析器,但了解如何使用这些可用的原始工具而不是依赖 IDE 来提供它们非常重要。 了解这些工具是 C/C++ 开发的一项基本技能,如果您更改角色并且不再有权访问它,或者正在通过 ssh 会话对远程资产进行动态开发,那么将其留给您的 IDE 可能会有问题。

幸运的是,在您掌握了基础知识后,使用 GDB 相当简单。 以下是如何确保您的 ROS2 代码已准备好进行调试:

  • 通过使用 --cmake-args:包含调试符号的最简单方法是将 --cmake-args -DCMAKE_BUILD_TYPE=Debug 添加到您的 colcon build 命令中:

colcon build --packages-up-to <package_name> --cmake-args -DCMAKE_BUILD_TYPE=Debug
  • 通过编辑 CMakeLists.txt :另一种方法是将 -g 添加到您想要分析/调试的 ROS 包的编译器标志中。

此标志构建 GDB 可以读取的调试符号,以告诉您项目中失败的特定代码行及其原因。 如果您不设置此标志,您仍然可以获取回溯,但它不会提供失败的行号。

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

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

以下部分描述了您在使用基于 ROS 2 的系统时可能遇到的三种主要情况。

阅读最能描述您尝试解决的问题的部分。

使用 GDB 调试特定节点

要在启动 ROS 2 节点之前轻松设置 GDB 会话,请利用“–prefix”选项在启动 ROS 2 节点之前轻松设置 GDB 会话。 对于 GDB 调试,请按如下方式使用它:

Note

请记住,ROS 2 可执行文件可能包含多个节点。 --prefix 方法可确保您在进程中调试正确的节点。

为什么直接使用 GDB 会很棘手

--prefix 将在 ROS 2 命令之前执行一些代码,以便我们插入一些信息。 如果您尝试执行 gdb ex run --args ros2 run <pkg> <node> ,就像我们在准备阶段的示例一样,您会发现它找不到 ros2 命令。 此外,尝试在 GDB 中获取工作区也会因类似原因而失败。 这是因为 GDB 以这种方式启动时缺少通常使 ros2 命令可用的环境设置。

使用 –prefix 简化流程

我们不必重新查找可执行文件的安装路径并将其全部输入,而是可以使用 --prefix。 这使我们能够使用您习惯的相同 ros2 run 语法,而不必担心某些 GDB 细节。

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

GDB 体验

与之前一样,此前缀将启动 GDB 会话并运行您请求的节点以及所有附加命令行参数。

现在您的节点应该正在运行,并且应该会进行一些调试打印。

读取堆栈跟踪

使用 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”函数将接受一个字段前缀,该前缀采用前缀参数列表。 我们将在此处插入 GDB 代码片段。

根据您的设置,请考虑以下方法:

  • 使用 GUI 进行本地调试: 如果您在本地调试并且有可用的 GUI 系统,请使用:

prefix=['xterm -e gdb -ex run --args']

这将提供更具交互性的调试体验。 基于“start_sync_slam_toolbox_node”进行调试的示例用例 -

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'],  # For interactive GDB in a separate window/GUI
  output='screen')
  • **远程调试(无 GUI):**如果没有 GUI 进行调试,则省略“xterm -e”:

prefix=['gdb -ex run --args']

GDB 的输出和交互将在您启动 ROS 2 应用程序的终端会话中发生。 以下是“start_sync_slam_toolbox_node”的类似示例 -

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=['gdb -ex run --args'],  # For GDB within the launch terminal
  output='screen')

与之前一样,此前缀将启动 GDB 会话(现在在“xterm”中)并运行您请求的启动文件,其中定义了所有附加启动参数。

一旦您的节点崩溃,您将看到如下所示的提示,现在在“xterm”会话中。 此时,您现在可以获取回溯,并使用“读取堆栈跟踪”中的说明读取它。

来自大型项目

使用具有多个节点的启动文件略有不同,因此您可以与 GDB 会话进行交互,而不会被同一终端中的其他日志所困扰。 因此,在使用较大的启动文件时,最好拉出您感兴趣的特定节点并单独启动它。

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

  • 从父启动文件中注释掉启动文件包含

  • 使用“-g”标志重新编译感兴趣的包以获取调试符号

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

  • 按照“从启动文件”中的说明在另一个终端中启动节点的启动文件。

或者,如果您感兴趣的节点直接在这些文件中启动(例如,您看到“Node”、“LifecycleNode”或在“ComponentContainer”内),则需要将其与其他节点分开:

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

  • 使用“-g”标志重新编译感兴趣的包以获取调试符号

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

  • 按照“使用 GDB 调试特定节点”中的说明在另一个终端中启动节点。

Note

在这种情况下,如果启动文件先前提供了参数文件,则可能需要重新映射或向此节点提供参数文件。 使用 --ros-args,您可以为其提供新参数文件、重新映射或名称的路径。 有关所需的命令行参数,请参阅 本教程

我们理解这可能很麻烦,因此它可能鼓励您将每个节点都作为单独包含的启动文件,以便于调试。 示例参数集可能是 ``–ros-args -r __node:=<node_name> –params-file /absolute/path/to/params.yaml``(作为模板)。

一旦您的节点崩溃,您将在特定节点的终端中看到如下所示的提示。 此时,您现在可以获取回溯,并使用`读取堆栈跟踪`_中的说明读取它。

如果 C++ 测试失败,可以直接在构建目录中的测试可执行文件上使用 GDB。 确保在调试模式下构建代码。 由于 CMake 可能会缓存先前的构建类型,请清除缓存并重建。

colcon build --cmake-clean-cache --mixin debug

为了让 GDB 加载任何调用的共享库的调试符号,请确保获取您的环境的源代码。 这将配置“LD_LIBRARY_PATH”的值。

source install/setup.bash

最后,直接通过 GDB 运行测试。 例如:

gdb -ex run ./build/rcl/test/test_logging

如果代码抛出未处理的异常,您可以在 gtest 处理它之前在 GDB 中捕获它。

gdb ./build/rcl/test/test_logging
catch throw
run

崩溃时自动回溯

backward-cpp 库提供了漂亮的堆栈跟踪,而 backward_ros 包装器简化了其集成。

只需将其添加为依赖项并在 CMakeLists 中对其进行 find_package,反向库就会注入到所有可执行文件和库中。