简介:面向需要在VSCOD中调试Apollo自动驾驶项目的开发者,尤其是刚接触Apollo的开发者,这套精简配置包将GDB断点调试所需的核心文件集中打包,解决从零配置调试启动、编译任务与C/C++环境等常见痛点。资源共5个文件,以4个JSON配置和1个HTML说明文档组成,压缩包仅5KB;其中JSON文件分别负责调试会话入口、编译任务绑定、编辑器与C/C++环境参数设置,HTML文档则详细梳理了在Apollo工程中配置和使用GDB调试的完整思路与常见注意事项。包体小巧但结构清晰,适合已有Apollo构建环境、希望快速接入VSCOD调试的读者,也可作为排查调试配置错误时的对照清单。目前已有1139人学习下载,复用时可结合自身bazel-bin下的可执行文件路径调整调试目标,并在源码中设置断点、观察变量、单步追踪,是提升Apollo代码调试效率的实用模板。 说实话,在Apollo这种级别的代码里折腾调试,多少有点“在高速上换轮胎”的意味。模块多、依赖重、还跑在Docker里,新手上来就想用VSCode打个断点看变量,往往卡在第一步:压根连不上进程。但这件事本身并不复杂,只要把几个关键点理清楚,你也能像调普通C++项目一样,舒舒服服地在VSCode里断点调试Apollo各模块代码。这篇东西是我自己踩坑踩出来的经验汇总,照着走基本能通。
1. 为什么Apollo断点调试这么麻烦
Apollo不是普通的CMake工程,它的构建、运行和进程管理方式决定了你不能像调试本地程序那样直接“F5”完事。
1.1 三个绕不开的客观现实
第一,Apollo一律跑在Docker容器里。官方推荐用Docker开发镜像,代码在容器里编译、容器里运行。你宿主机上装的VSCode默认连不到容器里的进程,需要走远程开发通道。
第二,Apollo用的是Bazel构建系统。编译产物不是整齐划一的build/bin目录,而是散落在Bazel的output base里,路径非常深。你给VSCode配置launch.json时,program字段指定到那里会很痛苦,而且路径随时可能因为编译配置变化而漂移。
第三,Apollo模块进程是独立启动的。像cyber、perception、planning这些模块,分别跑在不同的进程里。你要调试某个模块,必须先把它启动起来,再让调试器用“附加(attach)”模式连接上去。想从启动那一刻就接管进程,配置会繁琐得多,实际中也没必要。
1.2 适合断点调试的场景
不是所有代码都值得上断点。Apollo里最常见的调试场景就三类:
- 看规划/决策算法的中间变量:比如某个巡航状态下,
planning模块为什么选了这条轨迹,断点看代价函数的输出。 - 梳理异步回调时序:Cyber框架里消息触发严重依赖协程和回调,光靠日志很难梳理顺序,断点暂停现场非常有效。
- 排查偶发崩溃:那种上线跑几分钟才复现的段错误,用
catchsegv或者直接gdb起服务,崩了看调用栈,比逐行加日志高效太多。
如果是单纯的接口联调、参数核对,还是老老实实用日志,断点反而耽误时间。
2. 准备工作:环境与工具链
在动手配置之前,先把底子打好。这一步偷懒的话,后面各种玄学问题会找上门。
2.1 VSCode侧需要装的扩展
打开VSCode扩展市场,装这三样,缺一不可:
| 扩展名 | 作用 | 为什么必要 |
|---|---|---|
| Dev Containers | 连接并进入Docker容器开发 | 这是进入Apollo容器的核心通道 |
| C/C++ | 微软官方C++调试、智能提示 | 提供cppdbg调试引擎,断点、变量监视全靠它 |
| Remote - SSH(可选) | 远程连服务器开发 | 如果你的Docker跑在远程机器上,需要它作为中间层 |
装完后按Ctrl+Shift+P调出命令面板,输入Remote-Containers: Reopen in Container,VSCode会重新加载窗口并进入容器环境。这一步成功的话,左下角会显示"Dev Container"字样。
2.2 宿主机和容器端口检查
调试本身不走网络端口,但为了保险起见,确认容器里能访问到代码目录。必须保证你挂载到容器的宿主机目录,和容器内/apollo路径是一一对应的。
怎么查?在容器内执行/apollo/scripts/docker_start.sh看你当时启动Docker时的挂载参数,或者直接在你VSCode打开的容器终端里执行ls /apollo,能看到代码就说明挂载OK。
注意:如果宿主机代码路径和容器内路径不一致,后面launch.json里
sourceFileMap配不好,断点就会变成“未绑定”状态,怎么点都断不下来。
3. 调试配置的逐步拆解
进入容器后,真正的配置才开始。核心就两个文件:.vscode/launch.json和.vscode/tasks.json。
3.1 一个能直接用的launch.json
在.vscode目录下新建launch.json,直接贴下面这份配置(这是针对Apollo 7.0/8.0的通用模板):
{ "version": "0.2.0", "configurations": [ { "name": "Apollo Debug: Planning", "type": "cppdbg", "request": "attach", "program": "/apollo/bazel-bin/modules/planning/planning", "processId": "${command:pickProcess}", "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "cwd": "/apollo", "sourceFileMap": { "/apollo": "/apollo" }, "externalConsole": false, "pipeTransport": { "pipeCwd": "/apollo", "pipeProgram": "bash", "pipeArgs": ["-c"], "debuggerPath": "/usr/bin/gdb" } } ] }几个字段单独说下:
program:这个必须指向实际编译出来的二进制文件路径。Apollo用Bazel构建,一般产物在/apollo/bazel-bin/modules/planning/planning这样的路径。你调试哪个模块就改成哪个。processId:用了${command:pickProcess},这样按F5后VSCode会弹出进程列表让你选。因为Apollo模块进程名和二进制名一样,选起来很好认。pipeTransport:这个是从容器外调试的关键,它会让gdb通过bash管道进入容器执行调试。虽然我们在容器内开发,但保留这个配置可以增加兼容性。sourceFileMap:源代码路径映射。只要保持/apollo到/apollo即可,因为容器内路径就是真实路径。
3.2 编译调试版代码的tasks.json
有了launch.json,还得确保代码是带调试符号编译的。Apollo默认的编译模式有debug选项。创建一个tasks.json来配合:
{ "version": "2.0.0", "tasks": [ { "label": "Build Apollo Planning Debug", "type": "shell", "command": "bash", "args": [ "-c", "cd /apollo && source cyber/setup.bash && bazel build -c dbg //modules/planning:planning" ], "problemMatcher": [], "group": { "kind": "build", "isDefault": true } } ] }这里最关键的是-c dbg参数,它告诉Bazel以debug模式编译。不传这个参数,Bazel默认是opt优化模式,很多变量值被优化掉,断点也会出现跳行、看不到变量值的情况。
踩坑提醒:如果你之前用
-c opt编译过,再切到dbg模式,Bazel会全部重新编译一遍,耗时非常长。建议一开始就明确用dbg模式开发调试。
4. 实操过程:从启动模块到断点命中
配置毕竟是静态的,真正跑起来才能暴露问题。下面按整个调试流程的先后顺序来一遍。
4.1 启动DreamView和待调试模块
先用bash scripts/bootstrap.sh把整个Apollo后台拉起来,打开网页版的DreamView,在模块管理里把你要调试的模块启动。
这里有个关键点:要在容器终端里模块启动命令,而不是用DreamView的按钮启动。比如调试planning,就这么干:
cd /apollo source cyber/setup.bash cyber_launch start modules/planning/launch/planning.launch这样planning进程会以一个独立的、明显的进程跑起来。不要用cyber_launch那种伪分布式模式,会把多个模块进程混在一起,选进程的时候容易选错。
等看到类似[WARN] [timestamp] Planning: Started的日志,说明模块起来了。这时候可以用ps -aux | grep planning确认进程PID。
4.2 在VSCode里附加进程
回到VSCode,把中断点打在你关心的代码行上。按F5,选择Apollo Debug: Planning配置,VSCode会弹出进程列表。
在进程列表里找到/apollo/bazel-bin/modules/planning/planning,选中确定。过一两秒,调试器就附加上了。
附加成功后,底部状态栏会变成橙色,并且多出调试控制按钮。此时代码运行到断点处会自动暂停。
4.3 断点命中后的实用操作
断点暂停后,左侧调试面板会显示变量、监视、调用堆栈。这里面有几个高频操作:
- 添加监视表达式:右键变量选择“Add to Watch”,可以直接监视复杂表达式,比如
trajectory_point.path_point.x。 - 调用堆栈切换:Apollo很多逻辑在Cyber框架的回调里,查看调用堆栈可以跳转到上一层调用者,梳理消息流。
- 条件断点:循环里断点每次都停,谁也顶不住。右键断点选择“Edit Breakpoint”,输入条件表达式,比如
frame_->current_frame_ == nullptr,只有条件满足时才暂停。
实用技巧:在调试Apollo时,由于Cyber框架有协程调度,暂停一个断点可能会把其他协程的定时任务也阻塞掉。如果你发现暂停后整个系统像“冻住”一样,这不是你的问题,是框架特性。快速查看变量后,尽快按F5继续。
4.4 多模块同时调试怎么办
如果需要在planning和control两个模块间打断点,看数据交互,处理方式稍微不同。因为processId是动态选择的,你可以先附加planning,调试一会儿后,再开一个新的VSCode调试会话附加到control。
简单来说:第一个F5附加planning,第二个F5再选control的进程。VSCode会启动两个调试会话并排显示。注意两个模块最好都在同一个容器里,避免跨容器通信混乱。
5. 常见问题与排查技巧实录
下面这些坑,基本是每个调试Apollo的人都会遇到的。我按优先级列出来。
5.1 断点显示“未绑定”或空心圆
最常见也最坑。表现是:断点打上了,但是圆点是空心,鼠标悬停提示"You might have not bound this breakpoint"。
排查顺序:
- 确认编译模式:执行
file /apollo/bazel-bin/modules/planning/planning,看输出里有没有with debug_info字样,没有就说明编译模式不对,重新用-c dbg编译。 - 确认代码路径:断点所在的文件路径,必须和编译时的源码路径一致。如果代码是通过软链或者拷贝进容器的,路径就容易错。
- 确认程序字段:
program字段指向的二进制路径是否存在。Bazel的产物路径有时候会变,重新bazel build一下试试。
5.2 附加时报“无法找到可执行文件”
报这个错,基本可以确定是program字段指定的路径错了。Apollo不同模块的Bazel目标名和产物名偶尔不同。你可以在容器里执行:
bazel info bazel-bin它输出的是Bazel真正的输出根目录。如果你在/apollo下ls bazel-bin发现是个软链,那没问题。但如果你换了Bazel的--output_user_root参数,路径就不会是/apollo/bazel-bin了,需要去查一下实际路径。
5.3 断点跳过或者变量值不正确
这个基本就是优化模式导致的。确认Bazel的编译模式,再次强调:-c dbg才是调试模式,-c fastbuild和-c opt都不行。
如果确实是dbg模式还有问题,可能是gdb版本和代码优化级别不匹配。Apollo官方镜像里的gdb版本一般没问题。实在遇到变量值“看起来不对”,试试右键变量选“Use Hexadecimal Display”看底层十六进制,有时候是显示格式问题。
5.4 附加后F5直接秒退
启动调试后一两秒就自动退出,通常和gdb权限有关。在容器里执行:
gdb -version能正常显示版本号就没什么问题。如果提示permission denied,检查容器是否加了--privileged参数。部分Apollo容器默认非特权模式,gdb的ptrace系统调用会被限制,这时需要在启动容器时加--privileged,或者在宿主机上执行:
echo 0 > /proc/sys/kernel/yama/ptrace_scope对我个人来说,绝大部分“附加失败”都是这一类权限或路径问题,和VSCode本身关系不大。
5.5 调试时系统一直报“Timed out”
常见于计算机负载太高,或者Cyber框架本身有超时机制。Apollo里像Planning模块,如果输入数据没到齐,会一直等。而你的断点如果停在了等待数据之后的处理逻辑上,可能一直没到断点。
排查思路:确认DreamView里对应的自动驾驶场景在正常运行,比如有虚拟评测器在发数据,或者cyber_recorder在回放包。没有数据流,模块自然跑不到你的断点行。
6. 在容器启动时就接管进程的调试法
上面讲的都是“先启动、后附加”,这是最稳妥的。但有一种情况必须用“启动模式”:调试模块的初始化逻辑,因为初始化代码在进程启动早期就执行完了,附加模式根本赶不上。
对这种场景,得用request: "launch"模式。但Apollo的模块启动往往伴随大量环境变量和启动参数,直接在VSCode里写全很麻烦。我的做法是养成一个习惯:用一个shell脚本包一层。比如建一个/apollo/scripts/vscode_launch_planning.sh:
#!/bin/bash source /apollo/cyber/setup.bash /usr/bin/gdb --args /apollo/bazel-bin/modules/planning/planning \ --flagfile=/apollo/modules/planning/conf/planning.conf \ --log_dir=/apollo/data/log然后在launch.json里配:
{ "name": "Apollo Launch: Planning", "type": "cppdbg", "request": "launch", "program": "/usr/bin/gdb", "args": ["--args", "/apollo/bazel-bin/modules/planning/planning", "--flagfile=/apollo/modules/planning/conf/planning.conf"], "cwd": "/apollo", "sourceFileMap": { "/apollo": "/apollo" } }这样VSCode启动gdb后再拉起planning进程,断点能命中初始化代码。
7. 几个值得养成的调试习惯
调试Apollo这种大型项目,比工具有限的更重要是使用工具的节奏感。分享三个个人经验:
习惯一:小范围验证断点。新配置好调试环境,别上来就去断特别深的算法行。先在模块入口函数打断点,确认附加成功、路径对、环境通,再往深了断。
习惯二:高频使用条件断点。Apollo的高频循环像控制链路,刷新率可能到100Hz。直接打断点基本没法看数据,用frame_->frame_num > 100这种条件,能一下子过滤掉前100帧,直接看后续状态。
习惯三:善用日志作为断点的辅助。在断点处右键选Add Log Message,可以设置不中断的日志输出,直接打在调试控制台。这比改代码加AINFO高效得多,不用重编译,对排查“这段代码到底走没走、走了几次”这种问题特别好用。
调试环境的搭建是一次投入、长期受益的事。我第一次配置VSCode调试Apollo的时候,光在路径映射和Bazel编译模式上就耗了快两天。但跑通之后,再debug任何模块的算法问题,效率比同事用gdb命令行操作快了几倍不止。后面遇到新模块,无非就是复制launch.json、改个program路径的功夫。
最后再分享一个细节:如果调试过程中发现gdb命令行的输出乱码或者编码异常,可以去容器里执行export LANG=C.UTF-8再重试。这种边缘问题看起来不起眼,但在关键时刻很可能卡你半小时以上。祝你调试愉快,少踩坑。
本文还有配套的精品资源,点击获取