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;
}
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,打印前后的内存值,看哪些内存被踩。然后进行修改