python tracemalloc 检查踩内存原理分析-记录一次真实的踩内存问题分析解决实战

tracemalloc 是python官方提供的跟踪内存分配的工具,也包含了踩内存的相关检测。它会在内存Malloc申请时在申请的内存前后加一些特定的数据填充字节,在内存Realloc、Free时,检查这些填充的字节是否符合预期,如果不符合预期,那么认为这段内存是被其他函数修改过的,然后主动abort,并提示哪些字节与预期不符、以及提示出该段内存最近是在哪行python代码中申请的,如果我们的机器配置了可以生成core,那么就可以从后面的core文件中去分析内存数据。

踩内存检测原理

内存申请

obmalloc.c:_PyMem_DebugRawAlloc()

在n个字节的内存申请时,会在原有的nbtyes基础上多申请 3 * 8(SIZEOF_SIZE_T) = 24个字节的内存,总的内存申请大小就变成了 nbytes + 24 个字节。然后在存放存放数据data的nbytes前后,分别填充特定的数据,在内存释放 的时候,检测前后的填充数据是否符合预期,来判断内存是否被踩。

用到的宏及相关定义

arduino 复制代码
#define PYMEM_CLEANBYTE      0xCD
#define PYMEM_DEADBYTE       0xDD
#define PYMEM_FORBIDDENBYTE  0xFD

typedef struct {
    /* We tag each block with an API ID in order to tag API violations */
    char api_id;
    PyMemAllocatorEx alloc;
} debug_alloc_api_t;
static struct {
    debug_alloc_api_t raw;
    debug_alloc_api_t mem;
    debug_alloc_api_t obj;
} _PyMem_Debug = {
    {'r', PYRAW_ALLOC},
    {'m', PYMEM_ALLOC},
    {'o', PYOBJ_ALLOC}
    };
// api_id有三种,'r'、'm'、'o'

以申请32字节(nbytes=8)内存为例,在调用完_PyMem_DebugRawAlloc方法且使用的是malloc而不是calloc之后,它的内存数据如下:

内存释放

obmalloc.c:_PyMem_DebugRawFree()

在内存释放时,会检查这个内存的填充值是否符合预期,如果不符合预期会主动abort,生成一个core。

检查逻辑:

obmalloc.c:_PyMem_DebugCheckAddress()

检查api id是否符合预期

ini 复制代码
/* Check the API id */
id = (char)q[-SST];
if (id != api) {
    msg = msgbuf;
    snprintf(msgbuf, sizeof(msgbuf), "bad ID: Allocated using API '%c', verified using API '%c'", id, api);
    msgbuf[sizeof(msgbuf)-1] = 0;
    goto error;
}

检查填充的字节是否都是PYMEM_FORBIDDENBYTE

ini 复制代码
/* Check the stuff at the start of p first:  if there's underwrite
 * corruption, the number-of-bytes field may be nuts, and checking
 * the tail could lead to a segfault then.
 */
for (i = SST-1; i >= 1; --i) {
    if (*(q-i) != PYMEM_FORBIDDENBYTE) {
        msg = "bad leading pad byte";
        goto error;
    }
}

nbytes = read_size_t(q - 2*SST);
tail = q + nbytes;
for (i = 0; i < SST; ++i) {
    if (tail[i] != PYMEM_FORBIDDENBYTE) {
        msg = "bad trailing pad byte";
        goto error;
    }
}

写一个代码验证下上述的原理

scss 复制代码
from ctypes import c_char

data = 999999

addr = id(data)  # cpython解释器id拿到的就是对象的地址
before_addr = addr - 16
after_addr = addr + 28
print(hex(addr))
print(hex(before_addr))
print(hex(after_addr))
print((c_char * 56).from_address(before_addr)[:56])
print((c_char * 16).from_address(before_addr)[:16])
print((c_char * 8).from_address(after_addr)[:8])
  • PyLongObject的大小是32,但是这个申请的内存是28字节,对应0x1c - 后续可以再研究下
  • api_id是'o'对应:{'o', PYOBJ_ALLOC}

踩内存分析实战

先写一个简单的.c函数

python_tracemalloctest.c 复制代码
void test_tracemalloc(long long *p){
    *p = 0x1;
    *(p + 1) = 0x2;
}

然后编译成.so

vbnet 复制代码
gcc -Wall -g -fPIC -shared -o python_tracemalloctest.so.0 python_tracemalloctest.c

写一个测试的python,调用ctypes中的memset强制改写内存数据

scss 复制代码
from ctypes import c_char, memset

data = 999999

addr = id(data)  # cpython解释器id拿到的就是对象的地址
before_addr = addr - 16
after_addr = addr + 32
print(hex(addr))
print(hex(before_addr))
print(hex(after_addr))
print((c_char * 16).from_address(before_addr)[:16])
print((c_char * 8).from_address(after_addr)[:8])

# memset(before_addr, 0xAB, 16)  # 踩掉头
memset(after_addr, 0xAB, 8)  # 踩掉尾

踩掉头的报错

踩掉尾的报错

根据提示信息Debug memory block at address p=0x7f2055d346dc,可以知道0x7f2055d346dc这个地址指向的内存检查有问题,28 bytes originally requesetd告诉了本次访问的内存字节数是28.

后面提示的就是哪些字节与预期不符,可以先把怀疑范围缩小到某个函数或者某个接口调用,然后使用gdb,打印前后的内存值,看哪些内存被踩。然后进行修改

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
默_笙1 天前
🍙 给每个请求过安检:FastAPI 是怎么把校验写进类型注解的
python
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
qq_426003961 天前
启动playwright录制codegen生成自动化测试脚本
python·自动化
虎头金猫1 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
胡写代码1 天前
别再前后端各写一套表单校验了
java·后端
长沙三为智能科技1 天前
家政小程序开发从0到上线:五阶段交付流程与验收清单
python
伞伞悦读1 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
大勇前进1 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端