第一部分:內存泄漏的介紹和解決方案的介紹 (轉載)
第二部分:實現模擬檢查內存泄漏的樣例代碼 (原創)
內存泄漏的介紹和解決方案的介紹
以下文章轉載以:https://www.bcdaren.com/557359488538251265/blog_content.html
衆所周知,C/C++執行效率高,但難以駕馭,開車一時爽,但稍不留神容易翻車。估計每個C/C++程序員都遭受過內存泄漏的困擾。本文提供一種通過wrap malloc查找memory leak的思路,使得你翻車的時候能夠自救,而不至於車毀人亡。=
什麼是內存泄漏?
內存泄漏就是動態申請的內存丟失引用,造成沒有辦法回收它(我知道槓jing要說進程退出前系統會統一回收),相當於在人身上軋個口子,傷口一直流血不止,關鍵這口子還不知道軋哪兒。
內存泄漏對於客戶端應用可能不是什麼大事,而對於長久運行的服務器程序則可能是致命的。
隱式釋放
Java等傻瓜式編程語言會自動管理內存回收,你只管用,系統會通過引用計數技術跟蹤動態分配的每塊內存,在合適的時機自動釋放掉,這種方式叫隱式釋放,相當於車的自動擋。
顯式釋放
而C/C++需要顯式的釋放,也就是開發者需要確保malloc/free配對,確保每塊申請的內存都恰當的釋放掉,相當於車的手動擋。有很多手段可以避免內存泄漏,比如RAII、比如智能指針(大多基於引用計數)、比如內存池。C/C++程序員也是蠻拼的,一直在跟內存泄漏做殊死搏鬥。
理論上,只要我們足夠小心,在每次申請的時候,都牢記釋放,那麼這個世界就清淨了。但現實往往沒有這般美好,比如拋異常了,釋放內存的語句執行不到,比如模塊之間的指針傳遞,又或者某菜鳥程序員不小心埋了顆雷,所以,我們必須直面真實的世界,那就是我們會遭遇內存泄漏。
怎麼查內存泄漏?
我們可以review代碼,double check,結對編程,但從海量代碼裏找到隱藏的問題,這如同大海撈針,談何容易啊?兄弟。
所以,我們需要藉助工具,比如valgrind,但這些找內存泄漏的工具,往往對你使用動態內存的方式有某種期待,或者說約束,比如常駐內存的對象會被誤報出來,然後真正有用的信息會被掩蓋在誤報的汪洋大海里,所以,很多時候,valgrind根本解決不了日常項目中的問題,並沒什麼卵用。
很多著名的開源項目,爲了能用valgrind跑,都大張旗鼓的修改源代碼,從而使得項目符合valgrind的要求,用vargrind跑過沒有任何報警叫valgrind乾淨,這倒也不失爲一個一勞永逸的辦法。
既然這些個玩意兒都中看不中用,所以,求人不如求己,還得自力更生,多大點事兒。
operator new/delete重載和hook malloc/free
可以通過operator new/delete,operator new[]/delete[]重載,但這裏有很細緻的功夫,你需要全面瞭解,而不是貿然行動,建議看看Effective C++,對operator new系列操作符重載有專門的闡述。
你也可以hook malloc、free等c編程接口。
你還可以開啓ptmalloc的調試功能,它有時候也能管點用。
什麼是動態內存分配器?
動態內存分配器是介於kernel跟應用程序之間的一個函數庫,linux glibc提供的動態內存分配器叫ptmalloc,因爲抱了linux的大腿,故而是應用最廣泛的動態內存分配器。
從kernel角度看,動態內存分配器屬於應用程序層;而從應用程序的角度看,動態內存分配器屬於系統層。到底屬於哪一層,這取決於你的身份和角度。
應用程序可以通過mmap系統調用直接向系統申請動態內存,也可以通過動態內存分配器的malloc接口分配內存,而動態內存分配器會通過sbrk、mmap向系統分配內存,所以應用程序通過free釋放的內存,並不一定會真正返還給系統,它也有可能被動態內存分配器緩存起來。
所以當你malloc/free配對得很好,但通過top命令去看進程的內存佔用,還是很高,你不必感到驚訝。
google有自己的動態內存分配器tcmalloc,另外jemalloc也是著名的動態內存分配器,他們有不同的性能表現,也有不同的緩存和分配策略。你可以用它們替換linux系統glibc自帶的ptmalloc。
new/delete跟malloc/free的關係
new是c++的用法,比如Foo *f = new Foo,其實它分爲3步。
- 通過operator new()分配sizeof(Foo)的內存,最終通過malloc分配。
- 在新分配的內存上構建Foo對象。
- 返回新構建的對象地址。
new=分配內存+構造+返回,而delete則是等於析構+free。
所以搞定malloc、free就是從根本上搞定動態內存分配。
chunk
每次通過malloc返回的一塊內存叫一個chunk,動態內存分配器是這樣定義的,後面我們都這樣稱呼。
wrap malloc
gcc支持wrap,即通過傳遞-Wl,--wrap,malloc的方式,可以改變調用malloc的行爲,把對malloc的調用鏈接到自定義的__wrap_malloc(size_t)函數,而我們可以在__wrap_malloc(size_t)函數的實現中通過__real_malloc(size_t)真正分配內存,而後我們可以做搞點小動作。
同樣,我們可以wrap free。
malloc跟free是配對的,當然也有其他相關API,比如calloc、realloc、valloc,這些都是細節,根本上還是malloc和free,比如realloc就是malloc + free的組合。
怎麼去定位內存泄漏呢?
我們會malloc各種不同size的chunk,也就是每種不同size的chunk會有不同數量,如果我們能夠跟蹤每種size的chunk數量,那就可以知道哪種size的chunk在泄漏。很簡單,如果該size的chunk數量一直在增長,那它很可能泄漏。
光知道某種size的chunk泄漏了還不夠,我們得知道是哪個調用路徑上導致該size的chunk被分配,從而去檢查是不是正確釋放了。
怎麼跟蹤到每種size的chunk數量?
我們可以維護一個全局 unsigned int malloc_map[1024 * 1024]數組,該數組的下標就是chunk的size,malloc_map[size]的值就對應到該size的chunk分配量。
這等於維護了一個chunk size到chunk count的映射表,它足夠快,而且可以覆蓋到0 ~ 1M大小的chunk的範圍,它已經足夠大了,試想一次分配一兆的塊已經很恐怖了,可以覆蓋到大部分場景。
那大於1M的塊怎麼辦呢?我們可以通過log的方式記錄下來。
在__wrap_malloc裏,++malloc_map[size]
在__wrap_free裏,--malloc_map[size]
如此一來,我們便通過malloc_map記錄了各size的chunk的分配量。
如何知道釋放的chunk的size?
不對,free(void *p)只有一個參數,我如何知道釋放的chunk的size呢?怎麼辦?
我們通過在__wrap_malloc(size_t)的時候,分配8+size的chunk,也就是額外分配8字節,用起始的8字節存儲該chunk的size,然後返回的是(char*)chunk + 8,也就是偏移8個字節地址,返回給調用malloc的應用程序。
這樣在free的時候,傳入參數void* p,我們把p往前移動8個字節,解引用就能得到該chunk的大小,而該大小值就是之前在__wrap_malloc的時候設置的size。
好了,我們真正做到記錄各size的chunk數量了,它就存在於malloc_map[1M]的數組中,假設64個字節的chunk一直在被分配而沒有被正確回收,最終會表現在malloc_map[size]數值一直在增長,我們覺得該size的chunk很有可能泄漏,那怎麼定位到是哪裏調用過來的呢?
如何記錄調用鏈?
我們可以維護一個toplist數組,該數組假設有10個元素,它保存的是chunk數最大的10種size,這個很容易做到,通過對malloc_map取top 10就行。
然後我們在__wrap_malloc(size_t)裏,測試該size是不是toplist之一,如果是的話,那我們通過glibc的backtrace把調用堆棧dump到log文件裏去。
注意:這裏不能再分配內存,所以你只能使用backtrace,而不能使用backtrace_symbols,這樣你只能得到調用堆棧的符號地址,而不是符號名。
如何把符號地址轉換成符號名,也就是對應到代碼行呢?答案是addr2line。
addr2line
addr2line工具可以做到,你可以追查到調用鏈,進而定位到內存泄漏的問題。
至此,恭喜你,你已經get到了整個核心思想。
當然,實際項目中,我們做的更多,我們不僅僅記錄了toplist size,還記錄了各size chunk的增量toplist,會記錄大塊的malloc/free,會wrap更多的API。
總結
通過wrap malloc/free + backtrace + addr2line,你就可以定位到內存泄漏了。
美好的時間過得太快,又是時候說byebye!
實現模擬檢查內存泄漏的樣例代碼:
#ifndef __MALLOC_MAP_H__
#define __MALLOC_MAP_H__
#ifndef NULL
#define NULL (void *)0
#endif
#define TOPLIST_SIZE (10)
struct __malloc_map {
size_t dump_boundary; /**< 出現dump時的動態內存塊數量邊界 */
struct __malloc_node *ptr_list; /**< 維護動態分配的內存鏈表 */
struct __malloc_node *toplist[TOPLIST_SIZE]; /**< 維護最大內存塊數量top的前10個 */
};
extern void *__wrap_malloc(size_t size);
extern void *__wrap_calloc(size_t size);
extern void *__wrap_realloc(void *mem_address, size_t newsize);
extern void __wrap_free(void *ptr);
extern void malloc_map_dump(void);
extern void malloc_map_show_toplist(void);
extern int malloc_map_set_dump_boundary(size_t dump_boundary);
#define malloc(size) __wrap_malloc(size)
#define calloc(size) __wrap_calloc(size)
#define realloc(mem_address, newsize) __wrap_realloc(mem_address, newsize)
#define free(size) __wrap_free(size)
#endif /* __MALLOC_MAP_H__ */
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
struct __malloc_node {
struct __malloc_node *ptr_next;
size_t chunk_count;
size_t chunk_size;
};
#include "mallocmap.h"
#define BT_BUF_SIZE (100)
#define MALLOC_MAP_INIT \
{ .ptr_list = NULL, .dump_boundary = 0, .toplist = {NULL} }
static int sg_interrupted = 0;
static struct __malloc_map sg_malloc_map = MALLOC_MAP_INIT;
void *__real_malloc(size_t size);
void __real_free(void *ptr);
char *get_name_by_pid(pid_t pid, char *task_name)
{
FILE* fp = NULL;
char proc_pid_path[1024] = {0};
char buf[1024] = {0};
sprintf(proc_pid_path, "/proc/%d/status", pid);
fp = fopen(proc_pid_path, "r");
if (NULL == fp) {
return "";
}
if (NULL != fgets(buf, 1024 - 1, fp)) {
sscanf(buf, "%*s %s", task_name);
}
fclose(fp);
return task_name;
}
/* 若找到,則存放到ptr_cur指針中,返回ptr_cur的之前指針 */
static struct __malloc_node *malloc_node_find_chunk_size(struct __malloc_node *ptr_list,
size_t chunk_size, struct __malloc_node **ptr_cur)
{
struct __malloc_node *ptr_pre = ptr_list;
struct __malloc_node *ptr_tmp = ptr_list;
while (NULL != ptr_tmp) {
if (ptr_tmp->chunk_size == chunk_size) {
(*ptr_cur) = ptr_tmp;
break;
}
ptr_pre = ptr_tmp;
ptr_tmp = ptr_tmp->ptr_next;
}
return ptr_pre;
}
static int malloc_node_insert(struct __malloc_node **pptr_list,
size_t chunk_count, size_t chunk_size)
{
struct __malloc_node *ptr_node = NULL;
ptr_node = (struct __malloc_node *)__real_malloc(sizeof(struct __malloc_node));
if (NULL == ptr_node) {
return -1;
}
ptr_node->chunk_count = chunk_count;
ptr_node->chunk_size = chunk_size;
ptr_node->ptr_next = NULL;
(*pptr_list) = ptr_node;
return 0;
}
static void malloc_node_bubble_sort(struct __malloc_node *ptr_list)
{
struct __malloc_node *p = NULL;
struct __malloc_node *q = NULL;
struct __malloc_node tmp;
for (p = ptr_list; p->ptr_next != NULL; p = p->ptr_next){
for (q = p; q->ptr_next != NULL; q = q->ptr_next){
if (q->chunk_count < q->ptr_next->chunk_count){
tmp.chunk_count = q->chunk_count;
tmp.chunk_size = q->chunk_size;
q->chunk_count = q->ptr_next->chunk_count;
q->chunk_size = q->ptr_next->chunk_size;
q->ptr_next->chunk_count = tmp.chunk_count;
q->ptr_next->chunk_size = tmp.chunk_size;
}
}
}
}
void malloc_map_dump(void)
{
int idx = 0;
int nptrs = 0;
char task_name[1024] = {0};
char execmd[BT_BUF_SIZE] = {0};
void *buffer[BT_BUF_SIZE];
/* buffer裏面存放的是每層調用函數的返回地址 */
nptrs = backtrace(buffer, BT_BUF_SIZE);
printf("******** malloc_map_dump (PID:%u) *********\n", (unsigned int)getpid());
for (idx = 0; idx < nptrs; idx++) {
printf("backtrace(%d) addresses: %p\n", idx, buffer[idx]);
snprintf(execmd, sizeof(execmd), "./addr2line -e %s %p -f", \
get_name_by_pid(getpid(), task_name), buffer[idx]);
system(execmd);
}
printf("******** malloc_map_dump (end) *********\n");
exit(-1);
return;
}
void malloc_map_dump_fd(int fd)
{
int nptrs = 0;
void *buffer[BT_BUF_SIZE];
/* buffer裏面存放的是每層調用函數的返回地址 */
nptrs = backtrace(buffer, BT_BUF_SIZE);
if (nptrs > 0) {
backtrace_symbols_fd(buffer, BT_BUF_SIZE, fd);
}
return;
}
static int malloc_map_del_list(struct __malloc_map *ptr_map, size_t chunk_size)
{
struct __malloc_node *ptr_cur = NULL;
struct __malloc_node *ptr_pre = NULL;
ptr_pre = malloc_node_find_chunk_size(ptr_map->ptr_list, chunk_size, &ptr_cur);
if (NULL == ptr_cur || NULL == ptr_pre) {
fprintf(stderr, "ptr_cur or ptr_pre is null, code=%d (%s)", errno, strerror(errno));
return -1;
}
if (ptr_cur->chunk_count > 0) {
ptr_cur->chunk_count--;
}
if (0 == ptr_cur->chunk_count && NULL != ptr_pre) {
if (ptr_pre == ptr_cur) {
ptr_map->ptr_list = NULL;
} else {
ptr_pre->ptr_next = ptr_cur->ptr_next;
}
__real_free(ptr_cur);
}
return 0;
}
static int malloc_map_add_list(struct __malloc_map *ptr_map, size_t chunk_size)
{
struct __malloc_node *ptr_cur = NULL;
struct __malloc_node *ptr_pre = NULL;
if (NULL == ptr_map->ptr_list) {
malloc_node_insert(&(ptr_map->ptr_list), 1, chunk_size);
} else {
ptr_pre = malloc_node_find_chunk_size(ptr_map->ptr_list, chunk_size, &ptr_cur);
if (NULL != ptr_cur) { // 找到與chunk_size相同的指針
ptr_cur->chunk_count++;
// 分配的chunk數量大於dump_boundary則dump出棧調用流程
if (ptr_cur->chunk_count > ptr_map->dump_boundary) {
malloc_map_dump();
}
} else if (NULL != ptr_pre) { // 沒找到插入到鏈表尾部
malloc_node_insert(&(ptr_pre->ptr_next), 1, chunk_size);
}
}
return 0;
}
static void malloc_map_update_toplist(struct __malloc_map *ptr_map)
{
int idx = 0;
struct __malloc_node *p = NULL;
malloc_node_bubble_sort(ptr_map->ptr_list);
for (idx = 0, p = ptr_map->ptr_list; idx < TOPLIST_SIZE && NULL != p; idx++, p = p->ptr_next) {
ptr_map->toplist[idx] = p;
//printf("__wrap_flush_toplist idx=%u, chunk_size=%u\n", idx, p->chunk_size);
}
}
void malloc_map_show_toplist(void)
{
int idx = 0;
char task_name[1024] = {0};
struct __malloc_map *ptr_map = NULL;
ptr_map = &sg_malloc_map;
if (ptr_map->ptr_list) {
system("clear");
printf("toplist(boundary:%d):\n", ptr_map->dump_boundary);
printf("PID\tADDRESS\tSIZE\tCOUNT\tPNAME\n");
for (idx = 0; idx < TOPLIST_SIZE; idx++) {
if (NULL != ptr_map->toplist[idx]) {
printf("%u\t%p\t%u\t%u\t%s\n", (int)getpid(), \
ptr_map->toplist[idx] + sizeof(size_t), \
ptr_map->toplist[idx]->chunk_size - sizeof(size_t), \
ptr_map->toplist[idx]->chunk_count,
get_name_by_pid(getpid(), task_name));
}
}
printf("\n");
}
}
int malloc_map_set_dump_boundary(size_t dump_boundary)
{
sg_malloc_map.dump_boundary = dump_boundary;
return dump_boundary;
}
void *__wrap_malloc(size_t size)
{
size_t chunk_size = 0;
void *ptr_chunk = NULL;
chunk_size = size + sizeof(size_t);
if (NULL == (ptr_chunk = __real_malloc(chunk_size))) {
fprintf(stderr, "ptr_chunk is null, code=%d (%s)", errno, strerror(errno));
return NULL;
}
memcpy(ptr_chunk, &size, sizeof(size_t));
//printf("__wrap_malloc size=%d, (size_t *)ptr_chunk=%d\n", size, *(size_t *)ptr_chunk);
malloc_map_add_list(&sg_malloc_map, chunk_size);
malloc_map_update_toplist(&sg_malloc_map);
return ptr_chunk + sizeof(size_t);
}
void *__wrap_calloc(size_t size)
{
void *ptr_data = NULL;
if (NULL == (ptr_data = __wrap_malloc(size))) {
return NULL;
}
memset(ptr_data, 0x00, size);
return ptr_data;
}
void *__wrap_realloc(void *mem_address, size_t newsize)
{
size_t real_size = 0;
void *ptr_data = NULL;
real_size = *(size_t *)(mem_address - sizeof(size_t));
if (NULL == (ptr_data = __wrap_malloc(newsize))) {
return NULL;
}
memcpy(ptr_data, mem_address, real_size);
return ptr_data;
}
void __wrap_free(void *ptr)
{
size_t chunk_size = 0;
void *ptr_chunk = NULL;
if (NULL != ptr) {
ptr_chunk = ptr - sizeof(size_t);
chunk_size = (*(size_t *)(ptr_chunk)) + sizeof(size_t);
__real_free(ptr_chunk);
malloc_map_del_list(&sg_malloc_map, chunk_size);
}
}
static void abnormal_andler(int signo)
{
printf("abnormal_andler signo:%d\n", signo);
sg_interrupted = 1;
malloc_map_dump();
exit(0);
}
//////////////////////////////////////////////////////////////////////////////////////
void test_func_xxx(void)
{
int idx = 0;
char *ptr = NULL;
for (idx = 0; idx < 20; idx++) {
ptr = __wrap_malloc(1024 + idx);
if (3 == idx) {
__wrap_free(ptr);
}
if (1 == idx) {
__wrap_free(ptr);
}
}
}
void test_func_yyy(void)
{
int idx = 0;
char *ptr = NULL;
for (idx = 0; idx < 20; idx++) {
ptr = __wrap_malloc(1024 + idx);
if (5 == idx) {
__wrap_free(ptr);
}
if (1 == idx) {
__wrap_free(ptr);
}
}
}
int testmain(void)
{
printf("wrap_malloc main\n");
malloc_map_set_dump_boundary(8);
test_func_xxx();
test_func_yyy();
signal(SIGSEGV, abnormal_andler);
signal(SIGABRT, abnormal_andler);
while (!sg_interrupted) {
malloc_map_show_toplist();
test_func_xxx();
__wrap_malloc(1024);
usleep(1000*3000);
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////////
CFLAGS := -g -Wall -D__GNU__ $(MYCFLAGS) -D_GNU_SOURCE -D__USE_XOPE -mfloat-abi=hard
WRAPFUNC := -rdynamic -funwind-tables -ffunction-sections -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=calloc -Wl,--wrap=realloc
CFLAG_TARGET := $(CFLAGS)
CFLAG_TARGET += -Wl,-rpath-link $(TARGET_LIB_DIR)
CFLAGS += -I./include/
SOURCES := $(wildcard *.c) $(wildcard $(DIR_EXBOARD)/*.c $(DIR_DB)/*.c \
$(DIR_RS485)/*.c $(DIR_XYBASE)/*.c $(DIR_NET)/*.c $(DIR_READER)/*.c )
OBJS := $(patsubst %.c,%.o,$(SOURCES))
DEPS := $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \
$(patsubst %.d,%.cc,$(MISSING_DEPS)))
CPPFLAGS += -MD
TARGET := mallocmap
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAG_TARGET) -o $(TARGET_SERVICE_DIR)/$(TARGET) $(OBJS) $(WRAPFUNC) $(LIB)
cp $(TARGET_SERVICE_DIR)/$(TARGET) $(DEST_PATH)/$(TARGET)_$(LOGNAME) -rf
$(OBJS): %.o: %.c $(DEP)
$(CC) $(CFLAGS) $(WRAPFUNC) -c $< -o $(subst ../,,$@)
clean:
rm -rf $(subst ../,,$(OBJS))
rm -rf *.gcno
cleanall:
rm -rf $(subst ../,,$(OBJS))
rm -rf $(TARGET_SERVICE_DIR)/$(TARGET)
rm -rf *.h~
rm -rf *.c~
rm -rf *.d