Python 自定义内存分配器的 hook 实现

1次阅读

python无法直接hook malloc/free,必须在c层替换cpython的内存函数指针;可行方案包括编译时修改源码或ld_preload预加载自定义malloc/free/realloc;需注意线程安全、解释器未初始化限制及三套allocator接口(pymem/pyobject/pyarena)的完整覆盖。

Python 自定义内存分配器的 hook 实现

Python 里没法直接 hook mallocfree

Python 解释器(CPython)本身不提供 API 让你在 Python 层拦截或替换底层 C 的内存分配函数。你写的 sys.settrace__import__ 钩子,对 PyMem_MallocPyObject_Malloc 这类 C 层调用完全无效。想靠纯 Python 实现“自定义内存分配器”,本质是行不通的。

唯一可行路径:编译时替换 CPython 的内存函数指针

CPython 在启动时会通过全局函数指针(如 _PyMem_RawMallocFunc_PyMem_RawFreeFunc)调用内存函数。这些指针在 Objects/obmalloc.cPython/pyarena.c 中被使用,且允许在构建前通过宏或符号重定向覆盖。

实操建议:

  • 修改 CPython 源码,在 Python/pylifecycle.cPyInterpreterState_Init 之前,用你自己的函数地址赋值给 _PyMem_RawMallocFunc 等指针
  • 或者更稳妥的方式:用 LD_PRELOADlinux)或 DYLD_INSERT_LIBRARIESmacos)预加载一个共享库,其中强符号定义 mallocfreerealloc —— 注意必须同时覆盖所有三个,否则 CPython 内部混用会导致崩溃
  • windows 下需用 DLL 注入 + IAT Hook,但 CPython 官方不保证 ABI 稳定性,极易因版本升级失效
  • 不要试图只 hook PyObject_Malloc:CPython 对小对象走 obmalloc 池,大对象才走 malloc;而 PyMem_RawMalloc 可能绕过所有 Python 层 allocator 直接调 C 库,漏掉就等于没 hook

LD_PRELOAD 方案下最常踩的坑

现象:Segmentation fault 在解释器启动早期就发生,甚至卡在 Py_Initialize 前。

立即学习Python免费学习笔记(深入)”;

原因和对策:

  • 你的 malloc 实现里调用了任何 Python C API(比如 PyErr_SetString)—— 不行,此时解释器还没初始化,PyThreadState_Get() 返回 NULL
  • 没处理线程安全:CPython 启动阶段就有多个线程并发malloc(如 GIL 初始化、信号 handler 注册),你的分配器必须带锁,或用 per-thread slab
  • 忘记导出 realloc:很多系统库(包括 libc 自身)在 malloc 后会调 realloc,缺了就会 fallback 到默认实现,导致内存管理错乱
  • 日志写到 stdoutstderr:在 LD_PRELOAD 早期,FILE* 可能未就绪,用 write(2) 更可靠

为什么连 tracemalloc 都不算“自定义分配器”

tracemalloc 是事后采样(通过 PyMem_SetAllocator 替换 Python 层 allocator,并记录调用),它不控制实际内存布局,也不改变分配行为,只是“看”而不是“管”。如果你的目标是内存池复用、NUMA 绑核、或 GPU 内存映射,tracemalloc 无能为力。

真正要接管分配逻辑,就必须在 C 层面对齐 CPython 的三套 allocator 接口:PyMem(C 兼容)、PyObject(对象专用)、PyArena(AST 构建用),每套的 malloc/realloc/free 都得单独 hook,且它们之间有隐式依赖——比如 PyArenaalloc 最终可能调 PyObject_Malloc

不是做不到,但每个 CPython 小版本都可能调整这些函数的调用链或初始化顺序。上线前务必用 valgrind --tool=memcheck 跑满所有测试用例,尤其注意 fork() 后子进程的 allocator 状态是否被继承或重置。

text=ZqhQzanResources