Skip to content

第23章 调试技术

pybind11 模块的调试需要跨越 Python 和 C++ 两个世界。掌握正确的调试技术可以快速定位绑定错误、内存泄漏和性能问题。

Python 提供了完善的调试工具,可以调试 pybind11 绑定的函数调用。

import my_module
import pdb
pdb.set_trace()
result = my_module.compute([1, 2, 3, 4, 5])
result = my_module.compute([1, 2, 3])
pdb.pm() # post-mortem 调试
import my_module
result = my_module.compute([1, 2, 3])
import pdb
pdb.set_trace()
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
def debug_compute(data):
print(f"DEBUG: input data = {data}")
result = my_module.compute(data)
print(f"DEBUG: result = {result}")
return result
// C++ 端添加调试打印
#include <pybind11/pybind11.h>
namespace py = pybind11;
int compute_with_debug(const std::vector<int>& data) {
py::print("DEBUG: entering compute_with_debug");
for (size_t i = 0; i < data.size(); ++i) {
py::print("DEBUG: data[", i, "] =", data[i]);
}
int result = 0;
for (const auto& d : data) {
result += d;
}
py::print("DEBUG: result =", result);
return result;
}
PYBIND11_MODULE(debug_module, m) {
m.def("compute_with_debug", &compute_with_debug);
}

关键洞察:Python 调试器可以进入 pybind11 绑定的函数内部。使用 py::print() 在 C++ 代码中添加调试输出,简单有效。

C++ 调试需要使用专业的调试器。Linux 上用 GDB,macOS 上用 LLDB。

Terminal window
g++ -g -O0 -fPIC -shared -std=c++17 \
-I$(python3 -c "import pybind11; print(pybind11.get_include())") \
my_module.cpp -o my_module.so
gdb python3
(gdb) break compute_with_debug
(gdb) run test_script.py
(gdb) bt # 查看调用栈
(gdb) p data # 打印变量
(gdb) n # 单步执行
(gdb) c # 继续
Terminal window
clang++ -g -O0 -fPIC -shared -std=c++17 \
-I$(python3 -c "import pybind11; print(pybind11.get_include())") \
my_module.cpp -o my_module.so
lldb python3
(lldb) breakpoint set --name compute_with_debug
(lldb) process launch --file test_script.py
(lldb) bt # 调用栈
(lldb) frame variable # 当前帧变量
(lldb) step # 单步
(lldb) continue # 继续
Terminal window
gdb -p $(pgrep -f python3)
python3 -c "
import pybind11_debug as m
m.trigger_debug_break() # C++ 端会在此处停下
"

关键洞察:GDB/LLDB 可以调试 Python 进程中运行的 C++ 代码。使用 -g -O0 编译以保留调试信息。py::print() 是更简单的调试方式。

混合调试需要同时观察 Python 调用栈和 C++ 调用栈。

Terminal window
py-spy record -- python3 test_script.py
py-spy record --format=flamegraph -- python3 test_script.py
firefox flamegraph.svg
Terminal window
gdb -ex "python import sys; sys.path.insert(0, '/path/to/gdb-jupyter')"
import my_module
print(dir(my_module)) # 查看导出的函数
help(my_module.compute)
try:
result = my_module.compute([1, 2, 3])
except Exception as e:
import traceback
traceback.print_exc()
// C++ 端详细错误信息
PYBIND11_MODULE(robust_module, m) {
m.def("compute", [](const std::vector<int>& data) {
py::print("DEBUG: compute called with", data.size(), "elements");
if (data.empty()) {
throw py::value_error("data cannot be empty");
}
try {
int result = 0;
for (const auto& d : data) {
result += d;
}
return result;
} catch (const std::exception& e) {
py::print("ERROR:", e.what());
throw;
}
});
}

关键洞察:混合调试的核心是理解 Python 调用 C++ 的边界。使用 py-spy 可以快速定位问题发生在哪一层。

Core dump 记录了程序崩溃时的内存状态,可以事后分析。

Terminal window
ulimit -c unlimited
* soft core unlimited
* hard core unlimited
echo "/tmp/core.%p" | sudo tee /proc/sys/kernel/core_pattern
Terminal window
gdb python3 /tmp/core.12345
(gdb) bt # 查看崩溃时的调用栈
(gdb) py-bt # 查看 Python 调用栈(如果 GDB 支持)
(gdb) info threads # 查看所有线程
(gdb) frame 2 # 切换到第 2 帧
(gdb) p variables # 打印变量
(gdb) python-import sys
(gdb) python-exec "import sys; sys.path.insert(0, 'path/to/gdbpy')"
(gdb) py-bt
// 1. 空指针解引用
int& get_element(std::vector<int>& v, size_t i) {
return v.at(i); // 超界会抛异常
}
// 2. 引用计数错误(需要 GIL)
void bad_function() {
// Python 对象操作需要 GIL
py::gil_scoped_acquire gil; // 添加 GIL
// ... 操作 Python 对象
}
// 3. 内存越界
void write_buffer(int* buffer, size_t size) {
for (size_t i = 0; i <= size; ++i) { // 错误:多了一个 =
buffer[i] = i;
}
}

关键洞察:Core dump 是诊断崩溃的终极工具。确保 core dump 路径有写权限,且文件不会被自动清理。

内存泄漏是 pybind11 模块的常见问题。valgrind 和 AddressSanitizer 是主要工具。

Terminal window
valgrind --leak-check=full --show-leak-kinds=all \
--track-origins=yes --verbose \
python3 test_script.py 2>&1 | tee valgrind.log
valgrind --leak-check=summary python3 test.py
valgrind --tool=massif python3 test.py # 内存分配分析
valgrind --tool=helgrind python3 test.py # 线程检查
Terminal window
clang++ -g -fsanitize=address -fno-omit-frame-pointer \
-shared -std=c++17 \
-I$(python3 -c "import pybind11; print(pybind11.get_include())") \
my_module.cpp -o my_module.so
LD_LIBRARY_PATH=. python3 test_script.py
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fsanitize=address")
endif()
#include <pybind11/pybind11.h>
#include <memory>
namespace py = pybind11;
// 场景 1:返回裸指针(泄漏)
int* create_array() {
return new int[100]; // 泄漏!
}
// 修复:返回智能指针或 vector
std::vector<int> create_array_fixed() {
return std::vector<int>(100);
}
// 场景 2:持有 Python 对象但未正确管理
class Holder {
public:
void set_pyobject(py::object obj) {
// 需要保持引用
obj_ = obj; // py::object 自动管理引用计数
}
private:
py::object obj_; // 正确:py::object 管理生命周期
};
// 场景 3:循环引用
class Node {
public:
void set_next(std::shared_ptr<Node> next) {
next_ = next;
}
void set_prev(std::shared_ptr<Node> prev) {
prev_ = prev;
}
std::shared_ptr<Node> next_;
std::shared_ptr<Node> prev_; // 可能导致循环引用
};
// 修复:使用弱引用
class NodeFixed {
public:
void set_next(std::shared_ptr<NodeFixed> next) {
next_ = next;
}
std::weak_ptr<NodeFixed> next_; // 弱引用打破循环
};
PYBIND11_MODULE(memory_leak_module, m) {
m.def("create_array", &create_array_fixed);
py::class_<Holder>(m, "Holder")
.def(py::init<>())
.def("set_pyobject", &Holder::set_pyobject);
}

关键洞察:ASAN 适合快速检测,valgrind 适合详细分析。pybind11 的 py::object 自动管理引用计数,但要避免裸指针返回。

性能问题需要用 profiling 工具定位热点。

Terminal window
perf record -g --call-graph=dwarf -- python3 test_script.py
perf report
perf script > out.perf
Terminal window
pip install py-spy
py-spy record -o profile.svg -- python3 test_script.py
py-spy record -o profile.svg --format=flamegraph -- python3 test_script.py
open profile.svg # macOS
firefox profile.svg # Linux
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
import my_module
result = my_module.compute([1, 2, 3, 4, 5] * 100000)
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative') # 按累计时间排序
stats.print_stats(30) # 前 30 行
stats.print_callee('compute')
@profile
def hot_function():
result = my_module.compute(data)
return result
kernprof -l -v test_script.py

关键洞察:火焰图直观展示了调用栈和时间消耗。perf 给出硬件级别的数据。cProfile 可以快速定位 Python 层的热点。

调试技术总结:

工具用途平台
pdbPython 调试所有
py-spyCPU 火焰图所有
GDBC++ 调试Linux
LLDBC++ 调试macOS
valgrind内存泄漏Linux
ASAN内存错误所有
perf性能分析Linux
Core dump崩溃分析所有

实战建议:先快速定位层(Python 还是 C++),再选择工具。ASAN 是内存问题的首选,火焰图是性能问题的首选。复杂问题需要组合使用多种工具。

常见绑定错误速查:

错误类型表现解决方案
GIL 未释放Python 被阻塞使用 py::gil_scoped_release
引用计数错误泄漏或崩溃使用 py::object 管理
类型不匹配转换失败检查 py::cast 语法
未捕获异常传播到 Python添加 try-catch
ABI 不兼容导入失败确保编译器版本一致