前面介绍的mtrace
也好,bcc
也罢,其实都是hook
技术的一种实现,但是mtrace
本身使用场景上有局限,而bcc
环境依赖则十分复杂。因此,这些调试手段只适用于开发环境用来调试,对于生产环境,均不是一个非常好的方法。
但其实所谓的hook
技术,本身并不复杂,说白了就是重载malloc/free
函数,让代码在调用malloc
函数时,先调用我们自己定义的malloc
,由自定义的malloc
完成一些前置统计工作后,再去调用系统的malloc
,从而完成一些内存的统计分析。明白了这一点,其实我们可以轻而易举地自己实现一套hook机制,嵌入到代码中,这样带来的好处显而易见:
- 不依赖其他检测工具,可以非常方便地在开发环境甚至在线上进行调试
- 可以在代码里通过开关的方式控制是否检测内存泄漏,如线上默认关闭,如果怀疑出现内存泄漏,则将开关打开,可以非常方便地定位问题
- 系统无关,可以针对不同操作系统有不同的实现,这对于跨平台的应用非常有帮助。
对于C
语言,我们可以用dlsym
去修改动态链接的函数(备注:dlsym
函数是GNU
扩展),但dlsym
的最大劣势仍然是只能在Linux
下使用,那么,有没有一套跨平台的方案,可以在所有平台上运行呢?其实也是有的。
那就是通过宏定义的方式,将malloc/free这些函数替换成我们自己实现的钩子函数。比如我们有如下定义:
//mcheck.h #ifndef _MCHECK_H_ #define _MCHECK_H_ #include<stddef.h> void mcheck_initialize(); void mcheck_terminate(); void *malloc_hook(size_t size, const char *file, int line); void *calloc_hook(size_t nmemb, size_t size, const char *file, int line); void *realloc_hook(void *ptr, size_t size, const char *file, int line); void free_hook(void *p, const char *file, int line); #define malloc(size) malloc_hook(size, __FILE__, __LINE__) #define calloc(nmemb, size) calloc_hook(nmemb, size, __FILE__, __LINE__) #define realloc(ptr, size) realloc_hook(ptr, size, __FILE__, __LINE__) #define free(p) free_hook(p, __FILE__, __LINE__) #endif
由于宏定义是直接替换,因此,有了上面的代码,在执行malloc
的时候,实际上执行的是malloc_hook
函数。而我们只需要在实现malloc_hook/free_hook
的时候加上一些统计信息就行了。
在此之前,我们封装一个链表,用来存储每次申请内存的地址以及申请内存的大小,在每次申请内存的时候,向链表添加一条数据,每次释放的时候,将对应的记录删除掉,那么,当程序结束,如果链表还有数据,那就是没有释放的泄露部分的内存。
当然你也可以用其他的数据接口来存储,这里为了简单演示,就直接使用链表了。
// meminfo.h #ifndef _MEMINFO_H_ #define _MEMINFO_H_ #include<stddef.h> #include<stdio.h> #include<stdlib.h> typedef enum mcheck_errcode_t { MCHECK_SUCCESS, MCHECK_FAILED, }mcheck_errcode_t; typedef struct mcheck_caller_t { const char *file; int line; const char *func; } mcheck_caller_t; typedef struct mcheck_meminfo { void *address; size_t size; mcheck_caller_t caller; }mcheck_meminfo; typedef struct mcheck_mem_list { mcheck_meminfo mem_info; struct mcheck_mem_list *next; }mcheck_mem_list; int mcheck_mem_list_create(mcheck_mem_list **list); int mcheck_mem_list_add(mcheck_mem_list *mlist, mcheck_meminfo mem_info); mcheck_mem_list *mcheck_mem_list_get(mcheck_mem_list *mlist, void *address); int mcheck_mem_list_delete(mcheck_mem_list *mlist, void *address); int mcheck_list_size(mcheck_mem_list *mlist); void mcheck_list_report_leak(mcheck_mem_list *mlist); void mcheck_list_destory(mcheck_mem_list *mlist); void init_report_file(); #endif
定义如下:
//meminfo.c #include "meminfo.h" int mcheck_mem_list_create(mcheck_mem_list **mlist){ if (*mlist != NULL) { mcheck_list_destory(*mlist); } *mlist = (mcheck_mem_list *)calloc(1, sizeof(mcheck_mem_list)); if (*mlist == NULL) { return MCHECK_FAILED; } (*mlist)->next = NULL; return MCHECK_SUCCESS; } int mcheck_mem_list_add(mcheck_mem_list *mlist, mcheck_meminfo mem_info) { mcheck_mem_list *node = NULL; mcheck_mem_list_create(&node); node->mem_info = mem_info; if (mlist == NULL) { mlist = node; return MCHECK_SUCCESS; } while (mlist->next != NULL) { mlist = mlist->next; } mlist->next = node; return MCHECK_SUCCESS; } mcheck_mem_list *mcheck_mem_list_get(mcheck_mem_list *mlist, void *address){ if (mlist == NULL) { return NULL; } mcheck_mem_list *node = mlist; while (mlist != NULL) { if (address == mlist->mem_info.address) { return node; } mlist = mlist->next; } return NULL; } int mcheck_mem_list_delete(mcheck_mem_list *mlist, void *address){ if (mlist == NULL) { return MCHECK_FAILED; } mcheck_mem_list *current = mlist; mcheck_mem_list *prev = mlist; while (current != NULL) { if (current->mem_info.address == address) { if (mlist == current) { if (current->next = NULL) { mlist = NULL; } else { mlist = mlist->next; } } else { prev->next = current->next; } free(current); return MCHECK_SUCCESS; } prev = current; current = current->next; } return MCHECK_FAILED; } int mcheck_list_size(mcheck_mem_list *mlist){ if (mlist == NULL) {return 0;} int size = 0; while(mlist->next != NULL) { size++; mlist = mlist->next; } return size; } void mcheck_list_destory(mcheck_mem_list *mlist){ if (mlist == NULL) { return; } mcheck_mem_list *current = mlist; mcheck_mem_list *prev = mlist; while (prev != NULL) { current = current->next; free(prev); prev = current; } mlist = NULL; } static char *get_report_filename(){ char *fname = getenv("MCHECK_TRACE"); if (fname == NULL) { fname = "mcheck.rpt"; } return fname; } void init_report_file(){ char *fname = get_report_filename(); FILE *fp = NULL; fp = fopen(fname, "w+"); fclose(fp); } static void write_report_file(char *message){ char *fname = get_report_filename(); FILE *fp = NULL; fp = fopen(fname, "a+"); fprintf(fp, "%s\n", message); fflush(stdout); fclose(fp); } void mcheck_list_report_leak(mcheck_mem_list *mlist){ if (mcheck_list_size(mlist) == 0) { write_report_file("All memory was free, congratulations, well done!"); return; } write_report_file("Memory Not Free:"); write_report_file("-----------------------------"); write_report_file("\tAddress\t\tSize\t\tCaller"); mlist = mlist->next; while (mlist != NULL) { mcheck_meminfo mem_info = mlist->mem_info; char message[1024] = {0}; sprintf(message, "\t%p\t%lu\tat\t%s:%d[%s]", mem_info.address, mem_info.size, mem_info.caller.file, mem_info.caller.line, mem_info.caller.func); write_report_file(message); mlist = mlist->next; } }
以上代码逻辑比较简单 ,就不一一解释了。接下来实现一下钩子函数:
#include<stdio.h> #include<malloc.h> #include "meminfo.h" static mcheck_mem_list *mlist = NULL; void mcheck_initialize() { init_report_file(); mcheck_mem_list_create(&mlist); } void mcheck_terminate(){ mcheck_list_report_leak(mlist); mcheck_list_destory(mlist); } void *malloc_hook(size_t size, const char *file, int line) { void *p = malloc(size); char buff[128] = {0}; mcheck_meminfo meminfo = {0}; mcheck_caller_t caller = {0}; caller.file = file; caller.line = line; caller.func = "malloc"; meminfo.address = p; meminfo.size = size; meminfo.caller = caller; mcheck_mem_list_add(mlist, meminfo); return p; } void *calloc_hook(size_t nmemb, size_t size, const char *file, int line) { void *p = calloc(nmemb, size); char buff[128] = {0}; mcheck_meminfo meminfo = {0}; mcheck_caller_t caller = {0}; caller.file = file; caller.line = line; caller.func = "calloc"; meminfo.address = p; meminfo.size = nmemb*size; meminfo.caller = caller; mcheck_mem_list_add(mlist, meminfo); return p; } void *realloc_hook(void *ptr, size_t size, const char *file, int line) { void *p = realloc(ptr, size); char buff[128] = {0}; mcheck_meminfo meminfo = {0}; mcheck_caller_t caller = {0}; caller.file = file; caller.line = line; caller.func = "realloc"; meminfo.address = p; meminfo.size = size; meminfo.caller = caller; mcheck_mem_list_add(mlist, meminfo); mcheck_mem_list_delete(mlist, ptr); return p; } void free_hook(void *p, const char *file, int line){ mcheck_mem_list_delete(mlist, p); free(p); }
如上所示,我们会在每次申请内存之前,收集其调用栈信息,并且封装了mcheck_initialize
和mcheck_terminate
两个接口函数,我们只需要在程序里调用这两个函数,就能检测出内存泄露的问题。
类似与下面这种:
#include <mcheck.h> int main(void){ mcheck_initialize(); //your code mcheck_terminate(); return 0; }
下面我们用一个具体的示例演示一下:
#include "mcheck.h" int main(void){ mcheck_initialize(); void *p1 = malloc(10); void *p2 = malloc(20); free(p1); //free(p2); void *p3 = malloc(30); free(p3); void *p4 = calloc(1, 64); void *p5 = malloc(32); void *p6 = realloc(p5, 128); mcheck_terminate(); return 0;
我们注意在第1行包含了头文件,并在第4行和14行分别调用了mcheck的接口,这样的话,就可以检测出这个过程中出现的内存泄露问题,以上代码运行会产生一个名为mcheck.rpt
的报告,内容如下:
Memory Not Free: ----------------------------- Address Size Caller 0x1fde0b0 20 at mcheck_sample.c:6[malloc] 0x1fde300 64 at mcheck_sample.c:11[calloc] 0x1fde390 128 at mcheck_sample.c:13[realloc]
它告诉我们第6,11,13行分别出现了内存泄露,大小是多少,调用的那么函数申请的内存,还是比较详细的。
但是这种方式也有缺陷。首先就是调用栈只有一层,不能打印出更深层的调用栈,不利于复杂程序的问题排查。其次是如果要使mcheck
生效,必须每个.c
里都要include
该头文件,对于第三方库是没有办法检测到的,因此使用面也是比较有限。
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对C/C++课程感兴趣的读者,可以点击链接,查看详细的服务:C/C++Linux服务器开发/高级架构师