Python C扩展类型__reduce__方法声明触发段错误排查
问题:Python C扩展类型__reduce__方法触发段错误
我给自己实现的Python C扩展类型编写了__reduce__方法以支持pickle序列化,该操作在其他类型上已成功实现,但这个类型却触发了段错误。以下是最小复现代码、GDB调试输出与回溯信息,以及环境信息:
最小复现代码
main.c
#define PY_SSIZE_T_CLEAN #include <Python.h> typedef struct { PyObject_HEAD unsigned char attr1; unsigned char attr2; unsigned char attr3; } SomeObject; static int SomeObject__init(SomeObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"attr1", "attr2", "attr3", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "bbb", kwlist, &self->attr1, &self->attr2, &self->attr3)) { return -1; } return 0; } static PyObject *SomeObject__reduce(SomeObject *self, PyObject *Py_UNUSED(ignored)) { return Py_BuildValue("O(BBB)N", Py_TYPE(self), self->attr1, self->attr2, self->attr3, Py_None); } static PyMethodDef SomeObject__methods[] = { { .ml_name = "__reduce__", .ml_meth = (PyCFunction)SomeObject__reduce, .ml_flags = METH_NOARGS, }, }; static PyTypeObject SomeType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "somemodule.SomeObject", .tp_basicsize = sizeof(SomeObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_methods = SomeObject__methods, .tp_init = (initproc)SomeObject__init, .tp_new = PyType_GenericNew, }; static PyModuleDef somemodule = { PyModuleDef_HEAD_INIT, .m_name = "somemodule", .m_size = -1, }; PyMODINIT_FUNC PyInit_somemodule(void) { if (PyType_Ready(&SomeType) < 0) return NULL; PyObject *m = PyModule_Create(&somemodule); if (m == NULL) return NULL; Py_INCREF(&SomeType); if (PyModule_AddObject(m, "Some", (PyObject *)&SomeType) < 0) { Py_DECREF(&SomeType); Py_DECREF(m); return NULL; } return m; }
setup.py
from setuptools import setup, Extension setup( name="somemodule", version="0.1.0", ext_modules=[Extension("somemodule", ["main.c"])] )
demo.py
import somemodule somemodule.Some(12, 12, 12).__reduce__()
GDB调试输出
$ gdb /home/xlurio/Playground/minimum-SIGSEGV/venv/bin/python -ex "run \"/home/xlurio/Playground/minimum-SIGSEGV/demo.py\"" Program received signal SIGSEGV, Segmentation fault. 0x000000000058da02 in PyUnicode_FromFormatV () (gdb) bt #0 0x000000000058da02 in PyUnicode_FromFormatV () #1 0x000000000055cbaf in PyErr_Format () #2 0x00000000004d2924 in ?? () #3 0x00007ffff7fbe252 in PyInit_somemodule () at main.c:57 #4 0x0000000000685b4e in _PyImport_LoadDynamicModuleWithSpec () #5 0x0000000000686661 in ?? () #6 0x00000000005c52f0 in ?? () #7 0x00000000005f61c8 in PyVectorcall_Call () --Type <RET> for more, q to quit, c to continue without paging-- #8 0x0000000000571917 in _PyEval_EvalFrameDefault () #9 0x0000000000569cea in _PyEval_EvalCodeWithName () #10 0x00000000005f6a13 in _PyFunction_Vectorcall () #11 0x0000000000570ac2 in _PyEval_EvalFrameDefault () #12 0x00000000005f6836 in _PyFunction_Vectorcall () #13 0x000000000056bbdf in _PyEval_EvalFrameDefault () #14 0x00000000005f6836 in _PyFunction_Vectorcall () #15 0x000000000056b9fd in _PyEval_EvalFrameDefault () --Type <RET> for more, q to quit, c to continue without paging-- #16 0x00000000005f6836 in _PyFunction_Vectorcall () #17 0x000000000056b9fd in _PyEval_EvalFrameDefault () #18 0x00000000005f6836 in _PyFunction_Vectorcall () #19 0x000000000056b9fd in _PyEval_EvalFrameDefault () #20 0x00000000005f6836 in _PyFunction_Vectorcall () #21 0x00000000005f3c41 in ?? () #22 0x00000000005f40a8 in _PyObject_CallMethodIdObjArgs () #23 0x0000000000552f1c in PyImport_ImportModuleLevelObject () --Type <RET> for more, q to quit, c to continue without paging-- #24 0x000000000056ddd5 in _PyEval_EvalFrameDefault () #25 0x0000000000569cea in _PyEval_EvalCodeWithName () #26 0x000000000068e7b7 in PyEval_EvalCode () #27 0x0000000000680001 in ?? () #28 0x000000000068007f in ?? () #29 0x0000000000680121 in ?? () #30 0x0000000000680db7 in PyRun_SimpleFileExFlags () #31 0x00000000006b8122 in Py_RunMain () --Type <RET> for more, q to quit, c to continue without paging-- #32 0x00000000006b84ad in Py_BytesMain () #33 0x00007ffff7de4083 in __libc_start_main (main=0x4ef1e0 <main>, argc=2, argv=0x7fffffffd998, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffd988) at ../csu/libc-start.c:308 #34 0x00000000005fb39e in _start ()
环境信息
- CPython版本:3.8
- 操作系统与架构:Ubuntu 20.04 LTS,x86-64
问题分析与解决方案
核心问题
段错误的根源在于**__reduce__方法的签名与PyMethodDef中指定的调用标志不匹配**:
- 你在
SomeObject__methods中给__reduce__方法设置了METH_NOARGS标志,该标志表示方法不接受任何参数,对应的函数签名应为PyObject* func(PyObject* self)。 - 但你的
SomeObject__reduce函数定义了两个参数(SomeObject *self和PyObject *ignored),当CPython调用该方法时只会传递self一个参数,导致函数读取栈上的非法内存,最终触发段错误。
修复步骤
有两种可行的修复方式:
方式一:修改函数签名,移除多余参数
将SomeObject__reduce的签名改为单参数形式:
static PyObject *SomeObject__reduce(SomeObject *self) { return Py_BuildValue("O(BBB)N", Py_TYPE(self), self->attr1, self->attr2, self->attr3, Py_None); }
同时保持PyMethodDef中的METH_NOARGS标志不变。
方式二:调整调用标志为METH_O
如果需要保留第二个参数(即使未使用),将PyMethodDef中的ml_flags改为METH_O:
static PyMethodDef SomeObject__methods[] = { { .ml_name = "__reduce__", .ml_meth = (PyCFunction)SomeObject__reduce, .ml_flags = METH_O, // 修改此处 }, };
METH_O标志表示方法接受一个可选参数,与你当前的函数签名匹配。
验证
修复后,重新编译扩展并运行demo.py,__reduce__方法将正常返回预期的元组,不会再触发段错误。
内容的提问来源于stack exchange,提问作者Kalegas
相关产品推荐
相关产品推荐

