获取 Backtrace
目标: 介绍在 ROS 2 中获取 backtrace(回溯)的多种方法
教程级别: 中级
预计时间: 15 分钟
本节将介绍 ROS 2 用户在遇到问题时如何获取 backtrace。
什么是 Backtrace?
- 可以把程序想象成一摞煎饼,每一张代表一个正在执行的函数。Backtrace 就像是这摞煎饼倒塌后拍下的照片,记录了它们原本的排列顺序,揭示程序是如何一步步走向崩溃的。
- 它列出了函数的调用序列,层层嵌套,直到崩溃发生的那个点。
为什么它有用?
- 精确定位问题: 无需猜测代码哪里出错,backtrace 能直接指出导致崩溃的确切行号。
- 揭示上下文: 你可以看到导致崩溃的完整调用链(一个函数调用另一个函数),不仅帮你定位出错位置,还能帮你理解出错原因。
直观类比:一摞煎饼
-
每张煎饼是一个函数:煎饼堆中的每一张煎饼代表程序正在执行的一个函数。最底部的煎饼是你的
main()函数,也就是一切的起点。 -
叠加煎饼:每当一个函数调用另一个函数,就会有一张新煎饼叠到顶部。
-
崩溃:崩溃就像盘子从底部滑出——当前正在执行的函数发生了严重错误。
-
Backtrace:Backtrace 就像这摞煎饼倒塌后拍下的照片,从上到下展示了煎饼(函数)的顺序,揭示你是如何一步步到达崩溃位置的。
代码示例:
void functionC() { // Something bad happens here, causing a crash}
void functionB() { functionC();}
void functionA() { functionB();}
int main() { functionA(); return 0;}崩溃时的 Backtrace:
#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:18Backtrace 如何帮助调试:
- 崩溃来源: 显示
functionC()中触发崩溃的确切行号。 - 调用序列: 揭示
main()调用了functionA(),后者调用了functionB(),最终引发functionC()中的错误。
通过上面的例子,我们对 backtrace 及其作用有了直观的认识。接下来介绍遇到问题时如何从特定节点获取回溯信息。本教程适用于仿真机器人和实体机器人。
本教程涵盖三种场景:使用 ros2 run 从特定节点获取 backtrace、从单个节点的 launch 文件获取 backtrace,以及从更复杂的多节点 launch 配置中获取 backtrace。学完本教程后,你应该能够在 ROS 2 节点崩溃时获取 backtrace。
GDB 是 Unix 系统上最流行的 C/C++ 调试器,可以用来定位崩溃原因、跟踪线程执行,还可以在代码中设置断点,检查程序在特定位置时内存中的值。
掌握 GDB 是所有 C/C++ 软件开发者的重要技能。许多 IDE 内置了调试器或性能分析器,但了解底层工具的用法、不完全依赖 IDE 非常重要。如果换了工作环境没有 IDE 可用,或者需要通过 SSH 连接到远程设备调试,完全依赖 IDE 就会遇到困难。
幸运的是,掌握了基础知识后,使用 GDB 并不难。以下是确保你的 ROS 2 代码准备好调试的方法:
- 使用
--cmake-args:添加调试符号最简单的方法是在colcon build命令中指定--cmake-args -DCMAKE_BUILD_TYPE=Debug:
$ colcon build --packages-up-to <package_name> --cmake-args -DCMAKE_BUILD_TYPE=Debug- 编辑
CMakeLists.txt:另一种方法是在你要分析或调试的 ROS 包的编译器标志中添加-g。该标志会生成 GDB 可读取的调试符号,让你知道项目中具体哪一行代码出了问题以及原因。如果不设置此标志,仍然可以获得 backtrace,但不会包含失败位置的行号。
现在你已经准备好调试代码了!如果这是一个非 ROS 项目,你可能会这样做:启动一个 GDB 会话并让程序立即运行。程序崩溃后会返回 GDB 提示符 (gdb),你可以在此查看所需信息。然而,ROS 项目通常涉及大量节点配置和其他设置,这种方式对初学者或不太熟悉命令行操作的人来说并不方便。
$ gdb ex run --args /path/to/exe/program以下各节描述了基于 ROS 2 的系统中你可能遇到的三种主要场景。请选择最符合你情况的那一节。
使用 GDB 调试特定节点
Section titled “使用 GDB 调试特定节点”要在启动 ROS 2 节点之前设置 GDB 会话,可以使用 --prefix 选项,用法如下:
注意:一个 ROS 2 可执行文件可能包含多个节点。
--prefix方法确保你调试的是进程中的正确节点。
为什么直接使用 GDB 可能会有问题
--prefix 会在 ROS 2 命令之前执行一段代码,允许我们插入调试信息。如果你像前文示例那样直接执行 gdb ex run --args ros2 run <pkg> <node>,会发现 GDB 找不到 ros2 命令。同样,尝试在 GDB 中 source 你的工作空间也会因类似原因而失败。这是因为以这种方式启动的 GDB 缺少让 ros2 命令可用的环境设置。
使用 —prefix 简化流程
与其手动查找可执行文件的安装路径再逐字输入,不如使用 --prefix。这样就能继续使用你习惯的 ros2 run 语法,无需操心 GDB 的细节。
$ ros2 run --prefix 'gdb -ex run --args' <pkg> <node> --all-other-launch argumentsGDB 体验
与之前一样,这个 prefix 会启动一个 GDB 会话,并使用所有额外的命令行参数运行你请求的节点。现在你应该有了运行中的节点,并且可以看到调试输出。
读取堆栈跟踪
Section titled “读取堆栈跟踪”使用 GDB 获取 backtrace 后,以下是解读方法:
-
从底部开始看:Backtrace 按逆时间顺序列出函数调用,底部的函数是崩溃的源头。
-
沿着堆栈向上追踪:上面每一行代表调用了下面函数的那个函数。向上追踪直到到达你自己项目中的代码行,这通常能揭示问题的起始点。
-
调试线索:函数名及其参数可以提供关于问题原因的宝贵线索。
节点崩溃后如何调试
节点崩溃后,你会看到类似下面的提示符。此时你可以获取 backtrace。
(gdb)在此会话中输入 backtrace,即可获得 backtrace 信息,根据需要复制即可。
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 第 1091 行的
at()方法中,原因是范围检查失败后抛出了异常。
阅读堆栈跟踪需要一些练习,但总体方法就是从底部开始,沿着堆栈向上追踪直到找到崩溃的那一行,然后推断崩溃原因。使用完 GDB 后,输入 quit 退出会话并终止所有仍在运行的进程。它可能会在最后问你是否要终止某些线程,回答 yes 即可。
从 Launch 文件中调试
Section titled “从 Launch 文件中调试”与非 ROS 示例类似,我们需要在启动 ROS 2 launch 文件之前设置 GDB 会话。虽然可以通过命令行设置,但也可以利用与 ros2 run 节点示例中相同的机制,直接在 launch 文件中实现。
在你的 launch 文件中,找到你想要调试的节点。本节假设你的 launch 文件只包含单个节点(可能还有其他配置信息)。launch_ros 包中的 Node 函数接受一个 prefix 字段,接收一个 prefix 参数列表,我们就在这里插入 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')与之前一样,这个 prefix 会启动一个 GDB 会话(现在在 xterm 中),并运行你请求的 launch 文件以及所有定义的额外 launch 参数。
节点崩溃后,你会看到类似下面的提示符(现在出现在 xterm 窗口中),此时你可以获取 backtrace,然后按照”读取堆栈跟踪”一节中的说明来解读它。
从大型项目中调试
Section titled “从大型项目中调试”使用包含多个节点的 launch 文件调试略有不同,目的是让你能与 GDB 会话交互而不被同一终端中的其他日志干扰。因此,在处理较大的 launch 文件时,建议把你感兴趣的特定节点单独拿出来启动。
如果你感兴趣的节点是从嵌套的 launch 文件(例如被包含的子 launch 文件)中启动的,你可能需要执行以下操作:
-
从父 launch 文件中注释掉对子 launch 文件的包含
-
使用
-g标志重新编译感兴趣的包以获得调试符号 -
在一个终端中启动父 launch 文件
-
按照从 Launch 文件中调试一节中的说明,在另一个终端中启动该节点的 launch 文件。
或者,如果你感兴趣的节点是在这些文件中直接启动的(例如你看到了一个 Node、LifecycleNode,或者在 ComponentContainer 内部),你需要将该节点与其他节点分离:
-
从父 launch 文件中注释掉对该节点的包含
-
使用
-g标志重新编译感兴趣的包以获得调试符号 -
在一个终端中启动父 launch 文件
-
按照使用 GDB 调试特定节点一节中的说明,在另一个终端中单独启动该节点。
注意:在这种情况下,如果之前 launch 文件为该节点提供了参数,你可能需要重新映射话题或提供参数文件。通过
--ros-args可以指定新参数文件的路径、重映射或名称。关于所需的命令行参数,请参阅相关教程。我们理解这可能比较麻烦,因此建议尽可能将每个节点封装为单独的 launch 文件,以便简化调试。一组示例参数可能是
--ros-args -r __node:=<node_name> --params-file /absolute/path/to/params.yaml(作为模板参考)。
节点崩溃后,你会在该特定节点的终端中看到类似下面的提示符。此时你可以获取 backtrace,然后按照”读取堆栈跟踪”一节中的说明来解读它。
使用 GDB 调试测试
Section titled “使用 GDB 调试测试”如果 C++ 测试失败,可以直接在构建目录中对测试可执行文件使用 GDB。确保以调试模式构建代码。由于之前的构建类型可能被 CMake 缓存,需要清除缓存后重新构建。
$ colcon build --cmake-clean-cache --mixin debug为了让 GDB 加载被调用的共享库的调试符号,请确保先 source 你的环境,以正确配置 LD_LIBRARY_PATH。
$ source install/setup.bash最后,直接通过 GDB 运行测试。例如:
$ gdb -ex run ./build/rcl/test/test_logging如果代码抛出了未处理的异常,你可以在 gtest 处理它之前,先在 GDB 中捕获它。
$ gdb ./build/rcl/test/test_loggingcatch throwrun崩溃时自动获取 Backtrace
Section titled “崩溃时自动获取 Backtrace”backward-cpp 库提供了美观的堆栈跟踪,而 backward_ros 包装器则简化了它的集成。
只需将其添加为依赖项,并在 CMakeLists 中对它调用 find_package,backward 库就会自动注入到你所有的可执行文件和库中。