在 ROS 2 中获取回溯
以下步骤向 ROS 2 用户展示了在遇到问题时如何获取回溯。
概述
什么是回溯?
想象一下,您的程序就像一叠煎饼,每个煎饼代表它当前正在执行的一个函数。
回溯就像一张倒塌的煎饼堆的照片,向您展示它们的顺序,揭示程序最终失败的原因。 - 它列出了调用的函数顺序,一个接一个,直到失败点。
为什么它有用?
查明问题:回溯向您显示导致崩溃的确切行号,而不是猜测代码中发生错误的位置。
揭示上下文:您可以看到最终触发失败的事件链(函数调用其他函数)。
这不仅可以帮助您了解出错的位置,还可以了解出错的原因。
视觉类比:一叠煎饼
1. 每个煎饼都是一个函数:想象一下,一叠煎饼中的每一块都代表着程序当前正在执行的一个函数。 最底部的煎饼是 main() 函数,一切从这里开始。
添加煎饼:每次函数调用另一个函数时,都会在堆栈顶部放置一块新煎饼。
崩溃:崩溃就像盘子从堆栈底部滑落一样——当前执行的函数中出现了灾难性的错误。
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')
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
,反向库就会注入到所有可执行文件和库中。