【C++ debug】在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展

系列文章目录


文章目录

  • 系列文章目录
  • [在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展](#在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展)
    • [1. 问题背景](#1. 问题背景)
    • [2. 思路:用 GDB 启动 Python,让 .so 在 GDB 控制下加载](#2. 思路:用 GDB 启动 Python,让 .so 在 GDB 控制下加载)
    • [3. 前置条件:扩展必须带调试符号编译](#3. 前置条件:扩展必须带调试符号编译)
      • [3.1 在扩展的构建里加「Debug 模式」](#3.1 在扩展的构建里加「Debug 模式」)
      • [3.2 重新安装扩展(带调试符号)](#3.2 重新安装扩展(带调试符号))
      • [3.3 确认 .so 带调试信息(可选)](#3.3 确认 .so 带调试信息(可选))
    • [4. VS Code launch.json 配置🔥](#4. VS Code launch.json 配置🔥)
    • [5. 使用流程与现象](#5. 使用流程与现象)
    • [6. 在 GDB 里查看 PyTorch Tensor 的"实际数据"](#6. 在 GDB 里查看 PyTorch Tensor 的“实际数据”)
    • [7. 避免误入 PyTorch 内部实现](#7. 避免误入 PyTorch 内部实现)
    • [8. 小结](#8. 小结)

在 VS Code 中无 Attach 调试 Python 调用的 C++ 扩展

本文总结一种在 VS Code 下无需手动 attach 即可对「Python 调用的 PyTorch C++ 扩展」进行 C++ 源码级调试的方法,适用于任何通过 torch.utils.cpp_extension 或 pybind11 构建的 .so 扩展。


1. 问题背景

典型调用链:

复制代码
Python 脚本 (.py)
  → import 某包
  → 调用包里的 Python 函数(如 mesh_to_flexible_dual_grid)
  → 该函数内部调用 _C.xxx_cpu(...)  # 即 .so 里的 C++ 函数
  → C++ 实现 (.cpp)
  • debugpy 只能断 .py,进不了 .cpp
  • GDB 只能断 .cpp,不认识 .py
  • 若用 GDB attach 到已运行的 Python 进程,需要改 ptrace 权限、选进程,流程繁琐。

目标:用 GDB 直接启动 Python,一次 F5 即可在 C++ 里断点,无需 attach。


2. 思路:用 GDB 启动 Python,让 .so 在 GDB 控制下加载

让 GDB 作为"主进程",用 program = pythonargs = [你的脚本.py, ...] 启动 Python;脚本里 import 到扩展时,动态库会在同一 GDB 会话中加载,因此:

  • .cpp 里下的断点(或对 C++ 函数名的 pending breakpoint)会在执行到该 C++ 代码时命中。
  • 无需再 attach 到别的进程。

代价:这一次调试会话里,不能同时用 debugpy 断 Python 源码(只能二选一:本次跑要么用 GDB 断 C++,要么用 debugpy 断 Python)。


3. 前置条件:扩展必须带调试符号编译

默认的 pip install -e .python setup.py build 通常用 -O3不带 -g,GDB 无法把断点对应到源码行。

3.1 在扩展的构建里加「Debug 模式」

例如在 setup.py 里用环境变量控制编译选项:

python 复制代码
# 调试 C++ 时使用: DEBUG=1 pip install -e .
BUILD_DEBUG = os.environ.get("DEBUG", "0") == "1"

# 在 CUDAExtension / setuptools 的 extra_compile_args 里:
extra_compile_args={
    "cxx": (["-O0", "-g", "-std=c++17"] if BUILD_DEBUG else ["-O3", "-std=c++17"]),
    "nvcc": (["-O0", "-g", "-G", "-std=c++17"] if BUILD_DEBUG else ["-O3", "-std=c++17"]) + cc_flag,
}
  • Release :不加 DEBUG,用 -O3,无 -g
  • DebugDEBUG=1 时用 -O0 -g(及 nvcc 的 -G),方便断点与单步。

3.2 重新安装扩展(带调试符号)

bash 复制代码
cd /path/to/your/extension   # 你的 C++ 扩展工程根目录
rm -rf build/ dist/ *.egg-info */*.egg-info   # 清理旧产物(重要,避免用错 .so)
DEBUG=1 pip install -e . --no-build-isolation

--no-build-isolation 可避免构建环境与当前环境不一致导致的 ABI 问题。若遇 undefined symbol: ... TensorImpl::incref_pyobject ... 等,多半是之前用别的 PyTorch 版本编的,清理后按上面步骤用当前环境重编即可。

3.3 确认 .so 带调试信息(可选)

bash 复制代码
readelf -S /path/to/your/_C*.so | grep debug

若有 .debug_info.debug_line 等 section,说明带调试符号。


4. VS Code launch.json 配置🔥

在项目或工作区下 .vscode/launch.json 里增加一个 cppdbg 配置,用 GDB 启动 Python:

json 复制代码
{
  "name": "▶ GDB(C++): your_script",
  "type": "cppdbg",
  "request": "launch",
  "program": "/absolute/path/to/your/python",   // 例如 conda 环境里的 python
  "args": [
    "/absolute/path/to/your/script.py",
    "--your-arg", "value"
  ],
  "cwd": "/absolute/path/to/working/dir",
  "MIMode": "gdb",
  "miDebuggerPath": "/usr/bin/gdb",
  "setupCommands": [
    { "text": "set breakpoint pending on", "ignoreFailures": true },
    { "text": "break your_cpp_function_name", "ignoreFailures": true }
  ]
}

要点:

  • program :本机 Python 解释器绝对路径(建议用当前 conda/venv 的 python)。
  • args:你的入口脚本及其参数,和平时命令行一致即可。
  • cwd:脚本的工作目录,影响 import、相对路径等。
  • set breakpoint pending on:允许在符号尚未加载时下断点(.so 是动态加载的,启动时符号还没有)。
  • break your_cpp_function_name:在 C++ 函数名上先下一个 pending 断点,等 .so 加载后会自动绑定;函数名要和 C++ 里一致(可省略,改在源码里点行号断点也行)。

再保留一个 debugpy 配置,用于只断 Python 的情况,例如:

json 复制代码
{
  "name": "▶ Python: your_script",
  "type": "debugpy",
  "request": "launch",
  "program": "/absolute/path/to/your/script.py",
  "args": ["--your-arg", "value"],
  "python": "/absolute/path/to/your/python",
  "cwd": "/absolute/path/to/working/dir",
  "console": "integratedTerminal",
  "justMyCode": false
}

日常用法:要断 C++ 选 GDB 配置,要断 Python 选 debugpy 配置。


5. 使用流程与现象

  1. .cpp 里需要停下的行左侧点断点(例如扩展的 xxx_cpu 函数入口)。
  2. 选择 「▶ GDB(C++): your_script」,F5 启动。
  3. 启动后断点可能显示为灰色(未验证):因为此时 .so 还未加载,GDB 尚未解析到该源文件,属于正常现象。
  4. 继续运行(F5 或 Continue),当 Python 执行到 import 你的扩展并调用到对应 C++ 函数时,断点会命中,此时断点变为红色,光标停在 .cpp 对应行。
  5. 之后即可在 C++ 中单步(F10 Step Over、F11 Step Into)、查看变量、查看调用栈等。

若希望「一启动就停」,可在 setupCommands 里对 Python 入口下断点(例如 break Py_Main),再 Continue 到你的 C++ 断点;一般不必,直接等执行到 C++ 即可。


6. 在 GDB 里查看 PyTorch Tensor 的"实际数据"

Variables 面板里看到的往往是 torch::Tensor 的内部结构(如 impl_target_ 等),而不是元素值。可以在调试控制台用 GDB 命令查看。

假设 C++ 里已有:

cpp 复制代码
const float* v_ptr = vertices.data_ptr<float>();
const int* f_ptr = faces.data_ptr<int>();

且已单步到这两行之后,则:

  • 查看前 9 个 float(3 个顶点 × 3 分量):

    text 复制代码
    -exec p *v_ptr@9
  • 查看前 9 个 int(3 个面 × 3 个顶点索引):

    text 复制代码
    -exec p *f_ptr@9
  • 查看 tensor 的 shape(若 C++ 里有 vertices):

    text 复制代码
    -exec p vertices.sizes()

语法要点:-exec 是 VS Code 里把命令发给 GDB;p 即 print;*ptr@N 表示从指针 ptr 起连续 N 个元素(类型由 ptr 推导)。这样就能在 C++ 调试时直接看到"实际数据"而不是内部结构。


7. 避免误入 PyTorch 内部实现

单步时若用 Step Into (F11) 进入 tensor.size()tensor.data_ptr() 等,会进到 PyTorch 的 TensorImpl.hintrusive_ptr.h 等内部实现,对排查业务逻辑帮助不大。

建议:

  • 在业务 C++ 代码里尽量用 Step Over (F10) 逐行走。
  • 若已误入 PyTorch 内部,用 Step Out (Shift+F11) 返回到你的 .cpp。

8. 小结

项目 说明
目标 不 attach,用 GDB 直接调试 Python 所调用的 C++ 扩展
做法 launch 配置里用 cppdbg,program 填 Python,args 填你的脚本;.so 在运行中被 GDB 加载,断点即可命中
前提 扩展用 -O0 -g 重编(如 DEBUG=1 pip install -e .),并清理旧 build
断点 下在 .cpp 或对 C++ 函数名 break;pending on,.so 加载后自动生效
看数据 -exec p *ptr@N 等 GDB 命令在控制台查看指针指向的数组
单步 多用 F10 避免进 PyTorch 内部;误入后用 Shift+F11 跳出

按上述方式配置一次后,之后只需选对 launch、F5,即可在 C++ 里稳定断点和查看数据,无需再手动 attach 进程。

相关推荐
PingdiGuo_guo1 小时前
C++联合体详解!
开发语言·c++
XW01059992 小时前
4-11判断素数
前端·python·算法·素数
深蓝电商API2 小时前
爬虫增量更新:基于时间戳与哈希去重
爬虫·python
两万五千个小时2 小时前
构建mini Claude Code:06 - Agent 如何「战略性遗忘」(上下文压缩)
人工智能·python
浅念-2 小时前
C++ 继承
开发语言·c++·经验分享·笔记·学习·算法·继承
两万五千个小时2 小时前
构建mini Claude Code:12 - 从「文件冲突」到「分身协作」:Worktree 如何让多 Agent 安全并行
人工智能·python·架构
yuki_uix2 小时前
为什么我的 Auth Token 藏在了 Network 面板的 Doc 里?
前端·python·debug
王老师青少年编程2 小时前
csp信奥赛C++之反素数
数据结构·c++·数学·算法·csp·信奥赛·反素数
新缸中之脑2 小时前
Sonnet 4.6 vs Opus 4.6
java·开发语言