
简介面向需要在Visual Studio Code中调试Apollo自动驾驶项目的开发者这份资源提供了一套开箱即用的GDB断点调试配置方案帮助使用者省去手动编写调试环境配置的繁琐过程快速上手并深入理解Apollo框架的运行逻辑。资源包内含5个文件以4个json配置文件为核心分别对应launch.json、tasks.json、settings.json及c_cpp_properties.json可直接放入项目的.vscode目录使用配套的html说明文档则系统讲解了从环境准备、扩展安装到launch.json参数含义、断点设置与条件断点、单步执行、监视变量等完整调试流程并对常见路径替换与日志参数调整给出提示。全包仅约5KB轻量实用便于快速参考与移植。已有1141人学习下载适合刚接触Apollo源码阅读、希望通过图形化调试工具提升代码分析效率的开发者是一份可落地的工程化辅助资料。1. 为什么 VSCode 断点调试 Apollo 这么难先搞懂这套代码该怎么跑刚接触 Apollo 源码时最常见的翻车方式不是编译报错而是想在 planning 模块里加一行日志看看某个变量结果要重新编译整个模块再把整个系统重启等它跑起来规划轨迹都错过好几轮了。后来我换成 VSCode gdb 断点调试 Apollo直接在源码行间打断点命中后看变量、看调用栈整个过程能控制在秒级。这篇笔记就把我踩过的坑和最终跑通的配置完整写出来怎么让 gdb 找到 Apollo 的 Bazel 产物怎么让 VSCode 附加到已经运行的模块进程以及断点无效、附加失败时的排查思路。适合正在读 Apollo 源码、想改规划/感知/控制模块的开发者尤其是每次改代码都要等几分钟编译的老手。2. 调试环境搭建Bazel 产物、Docker 容器与 VSCode 插件2.1 先弄懂 Apollo 的二进制产物都放在哪Apollo 用的是 Bazel 构建所有模块最终都会生成到bazel-bin/目录下。比如 planning 模块的可执行文件路径一般是bazel-bin/modules/planning/planning但这个路径是 Bazel 的软链结构真正落地的符号文件还在synced/或者bazel-out/里。gdb 断点调试 Apollo 时要确保launch.json里指向的program是这个真实存在的可执行文件而不是 Bazel 的中间产物。我在配置前会先执行一个检查命令确认这个文件是不是动态链接的 ELF以及它依赖了哪些.socd /apollo ls -l bazel-bin/modules/planning/planning file bazel-bin/modules/planning/planning ldd bazel-bin/modules/planning/planning | head -20ls -l用来确认符号链接有没有断掉file输出里如果看到ELF 64-bit LSB executable说明是可执行文件如果用ldd能看到一堆libcyber、libplanning之类的库说明它依赖 Cyber RT 运行时的动态库这些库的路径也会被 gdb 加载。Apollo 的符号表默认是带调试信息的但如果 Bazel 用了-c opt编译很多行号信息会被优化掉断点命中了也看不到变量值这是后面避坑章要解决的问题。2.2 用 Remote-SSH 进宿主机再挂进 Apollo 的 Docker 容器Apollo 官方推荐的开发方式是 Docker 容器宿主机上只跑一个 IDE。我的做法是先在 VSCode 里安装 Remote-SSH 插件连上宿主机再通过 Remote-Containers 插件“Attach to Running Container”附加到已经启动的 Apollo 开发容器。这样做的原因很简单gdb 要在容器内运行才能访问bazel-bin/和源码如果在宿主机上直接调试容器内的进程符号路径对不上断点根本找不到文件。插件只需要两个ms-vscode-remote.remote-ssh和ms-vscode-remote.remote-containers再装一个ms-vscode.cpptools。注意 C/C 插件必须安装在容器内否则代码跳转和断点状态显示都不生效。我一般是在容器窗口里重新安装一遍 C/C 扩展然后在命令面板执行“C/C: Select IntelliSense Configuration”选择容器内的 g这样整个符号解析链路才完整。2.3 确定调试目标从模块启动命令反推进程Apollo 很多模块不是直接手动启动的而是通过cyber_launch拉起所以你在终端里看到的进程命令往往很长。这时候不要凭感觉写program用ps找到目标进程的真实路径ps -ef | grep planning | grep -v grep ll /proc/pid/exe/proc/pid/exe是指向实际可执行文件的软链ll一下就能看到完整路径。这个方法尤其重要因为 Apollo 的二进制文件名和模块名不一定一致比如定位模块的可执行文件可能叫localization而cyber_launch启动的却是另一个包。把ps和ll的结果记下来后面写launch.json时直接复制路径能避开一多半断点无效的问题。3. 核心配置拆解launch.json 与 tasks.json 的完整写法3.1 launch.json让 gdb 找到 Apollo 可执行文件VSCode 的 C/C 调试配置集中在.vscode/launch.json核心是cppdbg类型。下面是我调试 planning 模块时用的一份配置去掉噪音只留关键字段{ version: 0.2.0, configurations: [ { name: Apollo Planning Debug, type: cppdbg, request: launch, program: /apollo/bazel-bin/modules/planning/planning, args: [], stopAtEntry: false, cwd: /apollo, environment: [ { name: CYBER_PATH, value: /apollo/cyber }, { name: GLOG_logtostderr, value: 1 } ], externalConsole: false, MIMode: gdb, miDebuggerPath: /usr/bin/gdb, setupCommands: [ { description: Enable pretty printing, text: -enable-pretty-printing, ignoreFailures: true }, { text: set print thread-events 0, ignoreFailures: true } ] } ] }这里最容易被忽略的是cwd。Apollo 模块运行时会读取相对路径下的配置文件和 data 目录cwd必须设为工作空间根目录/apollo否则启动后立刻加载不到参数表现为进程存在但没有输出。CYBER_PATH和GLOG_logtostderr是常见环境变量前者告诉 Cyber RT 去哪找配置后者让日志直接打到 stderr方便在 VSCode 调试控制台里看到输出。MIMode: gdb是硬性要求miDebuggerPath要指向容器里的 gdb不是宿主机上的。3.2 tasks.json把 Bazel 构建挂到 F5 之前断点调试 Apollo 的前提是代码已经编译成带调试信息的版本。我习惯在launch.json里设置preLaunchTask让 VSCode 在每次按 F5 前先执行一次 Bazel 构建这样改完代码直接按 F5不用切换到终端敲命令。.vscode/tasks.json配置如下{ version: 2.0.0, tasks: [ { label: build-planning, type: shell, command: bazel build, args: [ //modules/planning:planning, --compilation_modedbg ], options: { cwd: /apollo, env: { CYBER_PATH: /apollo/cyber } }, group: build, problemMatcher: [] } ] }--compilation_modedbg是调试 Apollo 的关键参数它会让 Bazel 采用-O0 -g编译符号信息和行号映射都完整保留。如果不加这个参数Bazel 默认会用优化模式断点命中了但局部变量会显示optimized out这条血泪教训让我白白排查了半天。problemMatcher留空数组因为 Bazel 的报错格式和 VSCode 的默认解析器对不上留空反而不会误报。3.3 attach 模式调试已经跑起来的模块进程如果模块已经由cyber_launch启动你不能再用request: launch去启动第二个进程否则会出现端口占用或资源冲突。这时候改用request: attach把 VSCode 的 gdb 附加到现有进程上。配置片段如下{ name: Attach to Apollo Planning, type: cppdbg, request: attach, program: /apollo/bazel-bin/modules/planning/planning, processId: ${command:pickProcess}, cwd: /apollo, MIMode: gdb, miDebuggerPath: /usr/bin/gdb, setupCommands: [ { text: set pagination off, ignoreFailures: true } ] }processId可以使用 VSCode 内置的pickProcess命令按 F5 后会弹出一个进程列表直接选 planning 对应 PID。注意program字段在 attach 模式下也必须填对gdb 需要它来匹配符号表和二进制文件。附加成功后gdb 默认会暂停所有线程这时如果断点还没设置好可以按继续运行再打开源文件打断点整个过程不用重启 Apollo。4. 实战断点调试从启动到命中断点的完整链路4.1 在源码中下断点先找一个稳定命中的点Apollo 的 planning 模块每次规划周期都会调用OnLanePlanning::RunOnce这是一个很合适的断点位置。打开modules/planning/planning_component.cc找到RunOnce函数入口按 F9 打上断点。这里要注意如果断点打在循环内每次规划周期都会触发建议右键断点选择“Edit Condition”填入frame-current_frame_ ! nullptr之类的条件避免因为空指针误触发。我还会在断点命中前确认符号匹配断点应该是实心的红色圆点如果显示成空心圆说明 gdb 还没把这个文件认成当前执行代码需要回到第 3 章的program检查。第一次调试不建议直接在模板函数里打断点Apollo 代码里用了大量模板和宏展开后的行号和源码不一致很容易误导。4.2 启动模块并让 VSCode 附加到进程实际操作中我推荐先手动启动模块再附加调试。原因很简单Apollo 的依赖服务很多比如planning要等prediction、routing、localization都输出数据如果你用request: launch直接启动 planning它可能因为等不到输入而卡死。所以先在终端执行cd /apollo source cyber/setup.bash source modules/planning/planning.launchplanning.launch是 Apollo 提供的启动脚本内部会拉起 Cyber 的调度器和 planning 组件。等日志里出现The planning module has started后在 VSCode 里按 F5选择Attach to Apollo Planning再在进程列表里选中planning的 PID。附加完成后gdb 会载入符号表这时候打开源码断点应该变成实心红点。4.3 观察调用栈、变量和线程确认 Cyber RT 上下文命中断点后VSCode 左侧的“调用堆栈”面板会列出当前线程栈。Apollo 的 Cyber RT 有自己的协程调度所以你会看到栈底是cyber::routine之类的帧而不是普通的main函数。第一次调试的人容易慌其实这是正常的planning 模块跑在 Cyber 的调度线程里RunOnce是被协程调度的不是被main直接调用。变量面板里可以展开frame和reference_line这些核心对象查看路径规划结果。建议在 Watch 窗口添加一个表达式比如frame-current_frame_-planned_path()这样每次 F10 单步时能直观看到路径点位变化。如果某个变量显示incomplete type说明 gdb 没加载对应的调试符号检查一下是否漏了--compilation_modedbg。5. Apollo 断点调试避坑指南符号丢失、权限不足、断点无效5.1 现象gdb 附加时提示 Operation not permitted附加进程时VSCode 控制台直接报错warning: Could not attach to process后续给出Operation not permitted。原因是 Docker 容器默认限制了ptrace系统调用gdb 无法附加到其他进程。解决方案是在启动 Apollo 容器时加权限docker run --cap-addSYS_PTRACE或者直接用--privileged。如果是用 Apollo 自带的脚本进入容器那就在docker_start配置里加上这个 cap改完必须重启容器才能生效。5.2 现象断点是空心圆运行到该行却跳过源码里断点显示为空心圆圈按 F5 后直接运行到底不暂停。这种情况多半是launch.json里的program路径与实际进程不一致。我用过一个反例ps里看到的是planning --flagfile...但program写成planning的软链路径结果 gdb 加载了另一个二进制。解决方法是先用info sharedlibrary查看 gdb 实际加载的库路径确认和/proc/pid/exe指向一致如果发现多个同名的planning可执行文件优先选没有符号链接的那个绝对路径。5.3 现象gdb 报错 Dwarf Error: DW_FORM_strx调试时 VSCode 底部弹出Dwarf Error: DW_FORM_strx断点完全不可用。这是 gdb 版本和 GCC 生成调试信息格式不匹配导致的。Apollo 容器里如果自带 gdb 版本较老遇到 GCC 9 以上默认生成的 DWARF5 就会崩。解决办法是容器里升级 gdb 到支持 DWARF5 的版本或者在 Bazel 编译参数里加-gdwarf-4让符号表降到旧格式。我一般两者都做升级 gdb 是长期方案编译加参数是临时方案。5.4 现象VSCode 远程调试时频繁断连附加后立刻退出去Remote-SSH 连进容器调试时VSCode 的服务进程有时会退出导致 gdb 跟着终止。最常见的原因是容器内/tmp/vscode-server目录权限不对多用户共用容器时尤其严重。解决方式是删除容器内的~/.vscode-server和/tmp/vscode-server重新加载窗口如果还不行检查remote.SSH.useLocalServer配置改为false让 VSCode 在远程侧启动独立服务。这个问题玄学成分很高但我每次遇到都是按这个顺序排查基本都能救回来。6. 二次开发改造把调试配置固化为团队共用的多模块模板6.1 用${input}动态切换调试模块Apollo 有 planning、prediction、control 等多个模块挨个改launch.json太累。我改造的思路是把program路径改成由 VSCode 输入变量动态生成。在launch.json里定义inputs然后在配置里引用它{ inputs: [ { id: modulePath, type: promptString, description: 输入模块可执行文件路径, default: /apollo/bazel-bin/modules/planning/planning } ], configurations: [ { name: Apollo Attach Module, type: cppdbg, request: attach, program: ${input:modulePath}, processId: ${command:pickProcess}, cwd: /apollo } ] }按 F5 时 VSCode 会弹出输入框你只需要粘贴ps里看到的进程路径不用维护多份一样结构的配置。这个方式对团队协作尤其友好新人不用理解 Bazel 产物目录结构也能快速开始调试。6.2 把 Bazel 构建任务串联成一条流水线多模块联调时一次改动了 planning 和 prediction 两个模块我不想手动跑两次bazel build。我会在tasks.json里把它们串起来用dependsOn声明依赖关系{ label: build-both-modules, dependsOn: [build-planning, build-prediction], dependsOrder: parallel, problemMatcher: [] }构建任务并行执行后再让对应的调试配置的preLaunchTask指向build-both-modules。这里有一个值得注意的细节如果两个模块之间共享了同一个 proto 生成的库并行构建可能产生文件锁冲突遇到这种情况就把dependsOrder改成sequence。6.3 把.vscode目录提交到版本库最后一步是把.vscode/launch.json、.vscode/tasks.json和配置好的构建脚本一起放进 Apollo 工作区的版本库。项目初始化时我会在 README 里写一段最短验证路径克隆代码、进入容器、按 F5 附加到 planning 进程、命中断点。从那以后我每次改调试配置都强制走一遍“纯命令行 gdb 起一次 → 确认断点实心了 → 再打开 VSCode 图形界面”因为图形工具容易把底层报错藏起来命令行能直接看到 gdb 的真实反馈。希望这套配置思路也能帮你少踩几个坑。本文还有配套的精品资源点击获取