简介:面向需要在Visual Studio Code中调试Apollo自动驾驶项目的C++开发者,尤其适合刚接触Apollo或尚不熟悉GDB用法的初学者,这套调试配置包整合了VSCode与GDB联调所需的核心文件,直接省去手动搭建调试环境的试错过程。包体仅5KB、共含5个文件,以4个JSON配置为主,分别对应调试启动、编译任务、编辑器设置与C/C++路径属性,另附一份HTML格式的调试方法指南,结构精简实用。已有1141人学习/下载。配置包提供了可直接复用的launch.json模板,涵盖program路径指向bazel-bin、启用pretty-printing等关键参数;配套文档细致讲解了如何设置断点、单步执行、观察变量、使用条件断点、查看调用栈以及通过args开启调试日志,帮助读者在Apollo这类大型工程中快速定位异常代码、理解模块执行顺序,从而提升实际开发与排错效率。
1. 为什么在VSCode里给Apollo断点调试这么难:先看懂它的三层隔阂
在VSCode里给Apollo代码打断点,最常见的画面是:断点是一个空心圆,程序跑过去它就是不触发;好不容易触发了,局部变量全部显示optimized out。这不是VSCode坏了,而是Apollo(百度开源的自动驾驶平台)天生带了隔阂——它只跑在 Linux 的 Docker 容器里,默认编译产物不带调试信息,而且 planning、prediction、control 这些模块各自是独立进程。想用断点查清楚“这条轨迹为什么这么走”,先得拆掉三层隔阂:编一个带符号的 dbg 版本、让 VSCode 钻进容器、再用 launch.json 附加到正确的进程。这篇文章按这个顺序给你一条可复现的路径,最后还有六个高频踩坑点。适合已经能在 Apollo 里跑通 demo、但想脱离 printf 日志排障的人。
2. 先搞清楚Apollo的编译模式:不先编dbg版本,断点就是黑匣子
Apollo 默认用 Bazel 做构建,生产环境编译走的是优化模式。很多人在这个环节就翻车了:直接在容器里bash apollo.sh build编了一版,然后高高兴兴打断点,结果发现断点能命中但行号错乱,变量全是optimized out。这不是调试器的问题,是你手上的二进制根本没有可调试性。Bazel 的默认compilation_mode是opt,对应-O2优化,编译器会把局部变量塞进寄存器、把短函数内联掉、把循环展开,源码行号和机器指令的对应关系早就被改得面目全非。你看到的“断点”只是 VSCode 按源码行号强行标了一个位置,gdb 说这里没有可断的指令,它就不触发。
2.1 opt 和 dbg 的差别:为什么断点行为差这么远
先说结论:你要调 Apollo 的 C++ 模块,必须编一个-c dbg的版本。bazel 的dbg模式编译参数等价于-g加低优化级别,保留完整的 DWARF 调试信息,源码路径、行号、变量名、变量生命周期都留在二进制里。下面这张表是我常用的判断依据:
| 对比项 | opt(默认) | dbg(调试用) |
|---|---|---|
| 优化级别 | -O2 左右 | -O0 附近 |
| 调试符号 | 基本没有 | 完整 DWARF |
| 断点命中 | 可能不触发 | 稳定命中 |
| 变量查看 | optimized out | 能看到值 |
| 运行速度 | 快 | 慢不少 |
| 适用场景 | 跑仿真、录数据 | 断点排查逻辑 |
这里要注意一个细节:dbg 模式下 Apollo 跑起来明显变慢,感知、规划这类计算密集模块尤其明显。所以别拿 dbg 版本去做长时程仿真,它是给你调试用的,不是给你量产数据用的。
2.2 只编译你需要调试的模块:build_dbg 的最小命令
Apollo 全量编译一次动辄一两个小时,全量编 dbg 版本更慢。实际调试 planning 模块,只需要编 planning 对应的组件。常见做法是在容器内先 source 环境,再用 bazel 指目标编译:
# 在Apollo开发容器内执行,先source cyber的运行时环境 source /apollo/cyber/setup.bash # 只编planning组件的调试版本 bazel build -c dbg //modules/planning:planning_component这段命令的逻辑是:-c dbg告诉 bazel 用 debug 编译模式;//modules/planning:planning_component是目标路径,对应modules/planning目录下的BUILD文件里定义的组件。如果你要调 prediction,就把最后一个路径换成//modules/prediction:prediction_component。如果你习惯用 Apollo 的顶层脚本,也有bash apollo.sh build_dbg planning这类包装命令,但不同小版本对模块参数的支持不完全一样,bazel 直指目标是最稳的。
2.3 编译完先验证符号表:别等调试时才后悔
编译完成不等于万事大吉。我见过有人编了半小时,断点还是不生效,最后发现编出来的是旧缓存。验证方法其实很简单:
# 查看二进制信息的file输出,注意有没有debug_info字样 file /apollo/bazel-bin/modules/planning/planning_component # 看ELF文件的section表里有没有.debug_*段 readelf -S /apollo/bazel-bin/modules/planning/planning_component | grep debugfile命令输出里如果出现with debug_info,说明这个二进制带了调试符号;readelf -S | grep debug能列出.debug_info、.debug_line这些段。如果这两个命令什么都没查出来,说明你编的还是 opt 版本,回头检查一下 bazel 命令里的-c dbg有没有写对,以及 bazel 有没有真的重新编译(看日志里Building而不是Cached)。
3. 让VSCode钻进Apollo的Docker容器:Remote-Containers与两条关键路径
Apollo 官方只支持 Linux 环境,通常是 Ubuntu 18.04 或 20.04,而且它的依赖全部装在 Docker 镜像里。很多初次接触的人问我“Apollo 能部署到 Windows 上吗”——答案是别折腾,Windows 上你只能通过远程开发的方式去连 Linux 开发机,Apollo 本身跑在容器里。VSCode 调试 Apollo 的第二步,就是让 VSCode 的调试器、终端和文件系统全部进入容器视角,而不是在宿主机上隔着一层去够。
3.1 为什么必须进容器调试,而不是在宿主机直接 attach
Apollo 的二进制依赖大量共享库:cyber 运行时、protobuf、gflags、QT 相关组件,这些全在容器镜像里。宿主机上只有源码,没有依赖。你可以在宿主机上用 VSCode 打开源码目录,但调试器 attach 到容器内进程时,会面临两个问题:一是找不到容器的 PID 映射,二是即使能 attach,gdb 在解析共享库符号时全部失败,断点只能停在主程序入口,进不了你关心的模块。所以省事的路只有一条:让 VSCode 自己进容器。
3.2 用 Attach to Running Container 把 VSCode 接进 Apollo 容器
先在宿主机把 Apollo 的开发容器跑起来。Apollo 仓库的docker/scripts/目录下有一组现成的脚本,常见做法是:
# 在宿主机Apollo源码根目录执行,启动开发容器 bash docker/scripts/dev_start.sh # 另一个终端或脚本进入容器 bash docker/scripts/dev_into.shdev_start.sh会检查镜像、拉起容器并挂载 Apollow 源码目录到容器内的/apollo。容器起来之后,在宿主机打开 VSCode,安装 Remote-Containers 插件(插件市场搜 “Dev Containers” 或 “Remote - Containers”),然后按Ctrl+Shift+P调出命令面板,输入 “Attach to Running Container”,选择刚才启动的 Apollo 容器。VSCode 会重新打开一个窗口,这个窗口的终端、插件、调试器全部运行在容器内。
这里有一个关键操作:重新打开窗口后,用 “File - Open Folder” 打开容器里的/apollo目录,而不是宿主机上的某个路径。这样 VSCode 的工作区根目录就是/apollo,源码路径和编译符号路径天然一致,省掉后面所有路径映射的麻烦。
3.3 容器内的 C++ 和 Python 环境准备:装插件和配置 IntelliSense
容器内窗口打开后,VSCode 会提示安装扩展。你需要装两个:C/C++(ms-vscode.cpptools)和 Python(ms-python.python)。扩展会安装到容器侧,不影响宿主机。C++ 扩展装完后,如果你打开.cpp文件发现红波浪线说找不到头文件,需要手动配一下 IntelliSense。在命令面板运行 “C/C++: Edit Configurations (UI)”,生成的c_cpp_properties.json大致是这样:
{ "configurations": [ { "name": "Apollo", "includePath": [ "${workspaceFolder}/**", "/apollo/cyber", "/apollo/third_party/**" ], "defines": [], "compilerPath": "/usr/bin/g++", "cStandard": "c++17", "cppStandard": "c++17", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 }includePath里的/apollo/cyber是 cyber 框架的头文件根目录,/apollo/third_party/**是第三方依赖头文件。实际报错缺哪个路径,你就往这个数组里加哪个。compilerPath指向容器内 g++ 的绝对路径,可以通过which g++确认。配完之后,代码跳转和补全才会好用,否则你连断点该打在哪一行都要靠猜。
4. 写对一个launch.json:附加调试的三种进程场景
环境通了,接下来是核心环节:配置 launch.json。Apollo 的模块运行方式和普通单进程程序不一样,cyber 框架会把各个组件拉起为独立进程,所以调试策略取决于你想看哪一层。这里给出三种最常见的调试场景,覆盖绝大多数需求。
4.1 场景一:附加到正在跑的 planning_component(最常用)
调试 planning 模块内部逻辑时,需要让 Apollo 先把 planning 跑起来,然后 VSCode 附加到这个进程上。这样 Cyber 的输入输出、消息收发都是真实环境,你只是观察者。
先在容器内终端启动 planning 模块:
# 在容器内启动planning组件 cyber_launch start modules/planning/launch/planning.launch # 确认进程名,拿到PID ps -ef | grep planning_component然后用 VSCode 创建.vscode/launch.json,配置 attach 模式:
{ "version": "0.2.0", "configurations": [ { "name": "Apollo Planning Attach", "type": "cppdbg", "request": "attach", "program": "/apollo/bazel-bin/modules/planning/planning_component", "processId": "${command:pickProcess}", "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb", "cwd": "/apollo", "sourceFileMap": { "/apollo": "${workspaceFolder}" } } ] }参数说明:program指向容器内带调试符号的二进制路径;processId用${command:pickProcess},启动调试时会弹出进程列表让你选,避免手写 PID;miDebuggerPath是容器内 gdb 的路径,验证方式是在终端执行which gdb;sourceFileMap把编译符号里记录的/apollo路径映射到当前工作区,如果你已经在容器内打开/apollo目录,这个映射其实可有可无,但写上无害。
attach 方式适合“模块已经稳定运行、我想看它在某个时刻为什么做出这个决策”的场景。断点命中后,调用堆栈、局部变量、this指针都能看,Cyber 消息里传进来的障碍物数据也都在内存里。
4.2 场景二:直接启动 dreamview_main 并断点
如果你想调的代码正好在 Dreamview 的初始化流程里,比如某个按钮点击后的回调、场景管理器启动逻辑,用 attach 就不合适了,因为进程已经跑完了初始化。这时候用 launch 模式,让 VSCode 直接拉起程序:
{ "name": "Apollo Dreamview Launch", "type": "cppdbg", "request": "launch", "program": "/apollo/bazel-bin/modules/dreamview/dreamview_main", "args": ["--flagfile=/apollo/modules/common/data/global_flagfile.txt"], "cwd": "/apollo", "environment": [ { "name": "CYBER_PATH", "value": "/apollo/cyber" } ], "externalConsole": false, "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb" }这里args里的--flagfile指向 Apollo 的全局 gflags 配置文件,程序启动时会加载里面的参数;environment里的CYBER_PATH是 cyber 框架找运行时配置的环境变量,不设的话程序可能起不来。注意externalConsole设成false,让调试输出显示在 VSCode 的终端里。
launch 和 attach 的选择标准很简单:你想看的代码是否在进程启动路径上。在启动路径上就用 launch,在运行中的某个回调里就用 attach。如果你不确定,优先 attach,附加调试的风险更低,不会把整个 Dreamview 搞挂。
4.3 场景三:调试 cyber Python 模块
Apollo 里有一部分逻辑是 Python 写的,比如某些工具脚本、感知结果可视化、离线数据分析。调这些代码用的是 debugpy。先在容器内给 Python 环境装 debugpy:
pip install debugpy然后在你要调试的 Python 脚本开头插入两行监听代码:
import debugpy # 让脚本等待调试器附加,端口按需修改 debugpy.listen(("0.0.0.0", 5678)) debugpy.wait_for_client() # 停在这里等调试器接入 debugpy.breakpoint()接着在 VSCode 的 launch.json 里加一个 debugpy 配置:
{ "name": "Apollo Python Debug", "type": "debugpy", "request": "attach", "connect": { "host": "127.0.0.1", "port": 5678 } }运行 Python 脚本后,它会在listen处等待,启动 VSCode 调试即可接入。这个场景最常用来调试离线工具链,比如把一段 bag 数据喂进去看输出,断点能帮你看到中间每一帧的变量变化。
三种场景的适用关系可以这样总结:
| 场景 | 调试对象 | request | 难点 |
|---|---|---|---|
| attach 进程 | planning/prediction 等常驻组件 | attach | 找对 PID |
| launch 程序 | dreamview 启动流程 | launch | 环境变量与 flagfile |
| Python 脚本 | 离线工具/cyber python | attach | debugpy 端口 |
5. 断点调不通时,先查这六个坑
Apollo 断点调试的坑,我基本都踩过。下面按现象到原因再到解决的方式整理,遇到问题直接对号入座。
5.1 符号相关的两个坑
坑一:断点是空心圆,或者实心但不命中。现象是 VSCode 里断点显示灰色圆圈,运行多少遍都不触发。原因是二进制没有调试符号,或者符号和源码对不上。解决方法是确认你编的是 dbg 版本,并且 VSCode attach 的program路径指向bazel-bin下的新二进制。改完代码后记得重新bazel build -c dbg,bazel 会增量编译,但前提是目标路径没写错。
坑二:断点命中了,但局部变量全是<optimized out>。现象是程序停在断点处,左侧变量窗口一片红字。原因是你有可能在 attach 时连到了 opt 版本进程,或者 dbg 版本编译时 bazel 的-c dbg没有真正生效。解决方法是先杀掉在跑的模块进程,用 dbg 版本重新拉起,再 attach。还有一个隐蔽情况:Apollo 的 launch 脚本里如果写死了 opt 二进制的路径,你手动启动时要用完整路径直接执行 dbg 版本,绕过 launch 脚本。
5.2 容器与权限相关的两个坑
坑三:附加进程时报ptrace: Operation not permitted。现象是 VSCode 调试器启动后立刻报错,附加失败。原因是容器启动时没有给SYS_PTRACE权限,gdb 无法接管目标进程。解决方法是确认 Apollo 的dev_start.sh是否带了--cap-add=SYS_PTRACE,如果没带,需要手动重新创建容器时加上。检查方式是在宿主机执行docker inspect <容器名>,看CapAdd里有没有SYS_PTRACE。如果你在 WSL2 里跑 Docker,还要额外确认 WSL2 的 systemd 配置没限制权限。
坑四:断点打开后源码文件显示 “Source file not found” 或路径带bazel-out前缀。现象是断点命中了,但 VSCode 打开的不是你编辑的源码文件,跳到一个只读的缓存路径。原因是编译符号里记录的源码路径是 bazel 的中间输出路径,和你工作区路径不一致。解决方法是确认 VSCode 是在容器内打开的/apollo目录;如果还是不行,在launch.json里补sourceFileMap,把/apollo映射到${workspaceFolder}。还要检查你是不是在宿主机装了 C/C++ 插件,容器里的插件也要装,否则源码解析逻辑会走宿主机那一套。
5.3 运行时环境与进程选择相关的两个坑
坑五:launch 模式启动程序秒退,报错找不到libcyber.so。现象是程序启动一瞬间就退出,终端里提示无法加载共享库。原因是 cyber 的动态库路径没有注入,容器里虽然装了库,但LD_LIBRARY_PATH没指过去。解决方法是不要在 launch.json 里硬写环境变量,改成先让 VSCode 的终端执行source /apollo/cyber/setup.bash,再在同一个终端用gdb启动程序。实际操作是:打开 VSCode 集成终端,手动 source 一次,然后直接在终端里运行gdb /apollo/bazel-bin/modules/...,这样最稳;等于你要接受终端和调试器共享环境。
坑六:attach 时选错进程,断点打在另一份代码路径上。现象是调试器成功附加,但断点不命中,或者命中的地方和你预期完全不同。原因是 Apollo 里多个进程可能执行同一个二进制文件(比如多个 component 都链接了 planning 库),你 attach 到了错误的 PID。解决方法是 attach 前先ps -ef | grep 模块名,把命令行参数里真正带对应组件名的 PID 记下来。不要只看进程名,要看完整命令行,Apollo 的cyber_launch经常用同一个可执行文件拉起多个不同的组件实例。
注意:如果断点一直不触发,先不要怀疑 VSCode,按顺序排查三件事——二进制是不是 dbg、attach 的 PID 对不对、源码路径映射对不对。排查完这三件,九成问题已经解决。
6. 进阶:条件断点、日志断点与gflags参数组合验证
调试 Apollo 这种长时间运行的系统,普通断点会让人崩溃:程序每帧都触发断点,你手动跳断点的时间比看代码还多。我常用的做法是条件断点加日志断点组合,配合 gflags 参数来控制行为。
条件断点在 VSCode 里设置很简单:断点上右键,选 “Edit Breakpoint”,输入 C++ 表达式。比如你想在 planning 输出的轨迹点数异常时停下来,条件可以写成trajectory.point_size() > 50。只有条件为 true 时断点才会命中,省掉大量无意义的单步操作。注意表达式要用当前栈帧里可见的变量,别写一个函数调用,调试器在断点处求值函数调用可能卡住。
日志断点是另一个好用的东西:右键断点选 “Logpoint”,输入"hit, speed=" + speed这类表达式,程序执行到这里不会停下,只是在日志窗口输出信息。我一般用它在不需要中断的场景观察变量如何逐帧变化,比如调查“planning 输出的轨迹周期性地跳一下”,用日志断点打十帧数据,比反复按继续要高效得多。
gflags 参数配合断点,是 Apollo 调试里的一个关键技巧。很多 planning 的开关参数都定义在 gflags 里,配置文件在/apollo/modules/planning/conf/planning.conf。比如场景相关的逻辑有单独的 enable 开关,你可以在 launch.json 的args里加--enable_scenario_xxx=false,强制关闭某个分支,观察代码走向。反过来,你也可以在断点命中时用 gdb 的set var临时改参数值,往下多走几步验证假设。这两个手段能让你在一次调试里验证多组输入,不用一遍遍重启模块重放数据。
我的经验是:拿到一个不确定的规划行为,先打开对应模块的 gflags 开关日志,观察是哪个分支进了错路径,再用条件断点精确定位。这个小习惯让我少跑了不知道多少轮仿真。希望帮到你。
本文还有配套的精品资源,点击获取