mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-25 14:45:12 +00:00
Created Debugging segfaults and hard-to-decipher pybind11 bugs (markdown)
parent
1fae53bda2
commit
d61001ba6a
115
Debugging-segfaults-and-hard-to-decipher-pybind11-bugs.md
Normal file
115
Debugging-segfaults-and-hard-to-decipher-pybind11-bugs.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
This is meant to aide in debugging hard-to-decipher pybind11 bugs (or surprises in behavior ;).
|
||||||
|
|
||||||
|
## Tracing Python Executions
|
||||||
|
|
||||||
|
Generally, it's easiest to get a sense of where things are failing by using the `trace` module. Here's some code that can be copied+pasted. For examples here, assume these functions are defined in `debug.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
"""
|
||||||
|
Utilities that should be synchronized with:
|
||||||
|
https://drake.mit.edu/python_bindings.html#debugging-with-the-python-bindings
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def reexecute_if_unbuffered():
|
||||||
|
"""Ensures that output is immediately flushed (e.g. for segfaults).
|
||||||
|
ONLY use this at your entrypoint. Otherwise, you may have code be
|
||||||
|
re-executed that will clutter your console."""
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
if os.environ.get("PYTHONUNBUFFERED") in (None, ""):
|
||||||
|
os.environ["PYTHONUNBUFFERED"] = "1"
|
||||||
|
argv = list(sys.argv)
|
||||||
|
if argv[0] != sys.executable:
|
||||||
|
argv.insert(0, sys.executable)
|
||||||
|
cmd = " ".join([shlex.quote(arg) for arg in argv])
|
||||||
|
sys.stdout.flush()
|
||||||
|
os.execv(argv[0], argv)
|
||||||
|
|
||||||
|
|
||||||
|
def traced(func, ignoredirs=None):
|
||||||
|
"""Decorates func such that its execution is traced, but filters out any
|
||||||
|
Python code outside of the system prefix."""
|
||||||
|
import functools
|
||||||
|
import sys
|
||||||
|
import trace
|
||||||
|
if ignoredirs is None:
|
||||||
|
ignoredirs = ["/usr", sys.prefix]
|
||||||
|
tracer = trace.Trace(trace=1, count=0, ignoredirs=ignoredirs)
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
return tracer.runfunc(func, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage of this could look like the following:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
import my_crazy_cool_module # Say this has bindings buried 3 levels deep.
|
||||||
|
|
||||||
|
|
||||||
|
@debug.traced
|
||||||
|
def main():
|
||||||
|
my_crazy_cool_module.do_something_weird()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
debug.reexecute_if_unbuffered()
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can see where a segfault happens along the actual trace of your code.
|
||||||
|
|
||||||
|
## GDB on pybind11 unittests
|
||||||
|
|
||||||
|
The easiest way to do this is to use a debug build w/ a debug Python interpreter directly on pybind11 source code. Ideally, if you have an issue in your code base, you can make a min-reproduction as a failing test on the `pybind11` source code.
|
||||||
|
|
||||||
|
Here's an example workflow of debugging a specific unittest on CPython 3.8 on Ubuntu 18.04; this assumes the following packages are installed:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install cmake build-essential python3.8-dev python3.8-venv python3.8-dbg
|
||||||
|
```
|
||||||
|
|
||||||
|
Then here's an example of running a unittest with GDB:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd pybind11
|
||||||
|
|
||||||
|
python3.8-dbg -m venv ./venv
|
||||||
|
source ./venv/bin/activate
|
||||||
|
pip install -U pip wheel
|
||||||
|
pip install pytest
|
||||||
|
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake .. -GNinja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
|
-DPYTHON_EXECUTABLE=$(which python) \
|
||||||
|
-DPYBIND11_TEST_OVERRIDE=test_multiple_inheritance.cpp
|
||||||
|
|
||||||
|
# Get a sense of what is executed using the `-v` flag to ninja.
|
||||||
|
env PYTHONUNBUFFERED=1 PYTEST_ADDOPTS="-s -x" ninja -v pytest
|
||||||
|
|
||||||
|
# Now reformat it to your usage. For example:
|
||||||
|
src_dir=${PWD}/..
|
||||||
|
build_dir=${PWD}
|
||||||
|
( cd ${build_dir}/tests && gdb --args env PYTHONUNBUFFERED=1 python -m pytest -s -x ${src_dir}/tests/test_multiple_inheritance.py )
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have a segfault, and are using something like `debug.traced`, you should see something like this (for this example, a fake segfault was injected):
|
||||||
|
```
|
||||||
|
../../tests/test_multiple_inheritance.py --- modulename: test_multiple_inheritance, funcname: test_failure_min
|
||||||
|
test_multiple_inheritance.py(14): class MI1(m.Base1, m.Base2):
|
||||||
|
--- modulename: test_multiple_inheritance, funcname: MI1
|
||||||
|
test_multiple_inheritance.py(14): class MI1(m.Base1, m.Base2):
|
||||||
|
test_multiple_inheritance.py(15): def __init__(self, i, j):
|
||||||
|
test_multiple_inheritance.py(19): MI1(1, 2)
|
||||||
|
--- modulename: test_multiple_inheritance, funcname: __init__
|
||||||
|
test_multiple_inheritance.py(16): m.Base1.__init__(self, i)
|
||||||
|
|
||||||
|
Program received signal SIGSEGV, Segmentation fault.
|
||||||
|
0x<crazyaddress> in pybind11::detail::get_type_info (type=0xdeed90) at ../include/pybind11/detail/type_caster_base.h:160
|
||||||
|
160 *value = 0xbadf00d;
|
||||||
|
(gdb)
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user