[奇技淫巧] C语言 做malloc的会计师
1. 核心思路
动态内存是计算机程序必不可少的组件. 在程序运行当中, 对于编译之初未知的数据流大小, 有时需要保存下来,这时就需要动态地请求内存空间. 一般地, 动态内存空间取自于堆(Heap). 所以堆是堆, 栈(Stack)是栈 两者不要放在一起, 容易混淆.
1) malloc动态分配
malloc是一个标准库函数, 我猜它的名字全称应该是(Memory allocate function). 类似功能的函数还有 calloc
realloc
valloc
等.
简单地可以作一下理解:
calloc
= malloc
+ memset(0)
realloc
= free
+ malloc
+memset(0)
+ memcpy
假如把内存当作钱, 那么malloc
就是借钱, free
就是还钱. 有借有还, 再借不难. 那么借钱不还呢? 或者说忘记还了呢? 这就是内存泄漏. 当项目有了一定规模之后, 想要找出内存泄漏的点, 是非常困难的. 那是否可以改造一下malloc
在借钱的时候记一下帐呢?
2. 用于DEBUG的malloc函数
1) 接口设计
接口设计无非就是要考虑需求, 需要哪些输入项, 再者就是可移植性, 是否能够完美嵌入到之前的程序当中去.
void *
malloc(size_t size);
DESCRIPTION
The malloc(), calloc(), valloc(), realloc(), and reallocf() functions allocate memory. The allocated memory is aligned such that it
can be used for any data type, including AltiVec- and SSE-related types. The aligned_alloc() function allocates memory with the
requested alignment. The free() function frees allocations that were created via the preceding allocation functions.
首先, 这个用于DEBUG 记录分配情况的新的malloc
函数肯定在原有的传参模型上进行改进, 因此必须要传参分配的字节数, 再者那就是收集的数据, 例如调用的源文件名,行号. 在最后添加一个malloc
形式的函数指针, 旨在不同项目可能会有自己的分配内存函数
void* __RH_Debug_malloc( size_t size, char* FILE, int LINE, void* (*__malloc_func)(size_t size) );
2) 分析需要收集的数据
每次分配需要记录一下信息:
- 需要分配的内存大小, 字节为单位
size
- 调用时所在的源代码文件名
FILE
- 调用时所在代码的行号
LINE
- 实际malloc的函数
__malloc_func
其实每次分配出来的指针 都应该包含以上信息, 所以, 何尝不打包作成一个struct呢?
struct __RH_DebugMemoryInfo_t{
size_t byte;
const char* FILE;
uint32_t LINE;
void* ptr;
};
因此, 这其实是一种映射关系, 分配的指针ptr 作为键, 其包含的结构体信息(如上)作为键值, 建立映射.
在这里我使用的是哈希映射.
一下只给出 API 作为本次构造的辅助工具 源码见Github
struct __HashList_t{
const size_t key;
const void* const object;
const struct __HashList_t* const pNext ;
};
typedef struct __HashList_t __HashList_t;
struct __HashMap_t{
const __HashList_t* const pList;
};
typedef struct __HashMap_t __HashMap_t;
__HashMap_t* __Hash_createMap ( void );
void* __Hash_find ( const __HashMap_t *pHead, size_t key );
void __Hash_pair ( const __HashMap_t *pHead, size_t key , void* object );
void* __Hash_get ( const __HashMap_t *pHead, size_t key );
void* __Hash_remove ( const __HashMap_t *pHead, size_t key );
void __Hash_removeAll ( __HashMap_t *pHead );
利用映射, 通过用给定指针查找到对应的内存空间,当时调用的文件及行号.因此,这个改造的debug_malloc需要记录这些信息.
3) 代码
size_t RH_Debug_alloced_byte = 0; /*分配出去的内存总字节数*/
size_t RH_Debug_free_byte = 0; /*剩余空闲内存的总字节数*/ /*我项目不需要用到, 可自行添加逻辑*/
static __HashMap_t* pHEAD_HASHMAP_size_2_ptr = NULL;
void* __RH_Debug_malloc( size_t size, char* FILE, int LINE, void* (*__malloc_func)(size_t size) ){
if( !pHEAD_HASHMAP_size_2_ptr )
pHEAD_HASHMAP_size_2_ptr = __Hash_createMap();
struct __RH_DebugMemoryInfo_t* pInfo = malloc(sizeof(struct __RH_DebugMemoryInfo_t));
void* ptr = (*__malloc_func)(size);
pInfo->ptr = ptr;
pInfo->FILE = FILE; /* 调用时的文件名 */
pInfo->LINE = LINE; /* 调用时的行号 */
pInfo->byte = size; /* 请求字节数 */
RH_Debug_alloced_byte += pInfo->byte;
__Hash_pair(pHEAD_HASHMAP_size_2_ptr, (size_t)ptr, pInfo);
return ptr;
}
void __RH_Debug_free(void* ptr, void (*__free_func)(void*)){
struct __RH_DebugMemoryInfo_t* pInfo = (struct __RH_DebugMemoryInfo_t*)__Hash_get(pHEAD_HASHMAP_size_2_ptr, (size_t)ptr);
RH_Debug_alloced_byte -= pInfo->byte;
(*__free_func)(ptr);
}
RH_Debug_alloced_byte
分配出去的内存总字节数
pHEAD_HASHMAP_size_2_ptr
映射表头
pInfo
指针信息包
另外, 编写一个print_info每次调用时可以输出这些信息:
void* __RH_Debug_print_memory_info(void* ptr, int (*__print_func)(const char * restrict format, ...)){
/* 通过映射查找信息 */
struct __RH_DebugMemoryInfo_t* pInfo = (struct __RH_DebugMemoryInfo_t*)__Hash_get(pHEAD_HASHMAP_size_2_ptr, (size_t)ptr);
/* 这一行是为了确保信息能够全部录入, 有时文件路径可能很长, 如果嫌麻烦 可以改成 size_t len = (常数) */
size_t len = strlen("$DEBUG_MEM_INFO: [] [Ln ] [: byte]\n")+strlen(pInfo->FILE)+((sizeof(pInfo->LINE)+sizeof(pInfo->byte))<<3);
/* alloca函数 懂的自然懂, 就是栈空间的动态分配, 退出函数自动free */
char* str = alloca( len + sizeof('\0') );
/* 总之就是构造一句句子, C语言造字符串也是出了名地复杂 */
snprintf(str, len, "$DEBUG_MEM_INFO: [%s] [Ln %d] [%zu:%zu Byte]\n",pInfo->FILE,pInfo->LINE,pInfo->byte,RH_Debug_alloced_byte);
/* 使用你自己的打印函数, 这里没有做断言, 假定用户不调皮, 不传个NULL进来 = =! */
(*__print_func)("%s",str);
return ptr;
}
4) 锦上添花
上述代码已经能够完整的地记录每次动态分配时的信息, 但是可移植性不强, 一般开发者更习惯于malloc(x)
方式去调用, 而不是__Debug_malloc(x,__FILE__,__LINE__,malloc)
所以, 如何做到完美嵌套呢?
很简单, 用宏.
#define RH_MALLOC(x) __RH_Debug_print_memory_info( __RH_Debug_malloc(x, __FILE__, __LINE__, malloc), printf )
在这个宏里面printf
malloc
是函数指针, 可以替换自己想要的等价功能接口的函数.
这样每次调用RH_MALLOC
自动打印动态内存信息, 方便寻找泄漏之处.
5) 结果
附上在编写自研的Glucoo项目中, 使用此方法成功找到了内存泄漏之处.
3. 参考资料
[1] MALLOC(3) BSD Library Functions Manual MALLOC(3)