linux中記憶體洩漏的檢測(五)記錄記憶體洩漏的程式碼
到目前為止,先後通過wrap malloc、new函式過載和計算指標記憶體大小的方法,基本上滿足了對記憶體洩漏檢測的需要。
如果發現了記憶體洩漏,那麼就要找到記憶體洩漏的地方並且修正它了。
茫茫程式碼,如何去找?如果能根據未釋放的記憶體找到申請它的地方就好了。
我們今天就是要做這個事情。
想要根據記憶體地址查出申請者的資訊,那麼在一開始申請的時候就要建立地址與申請者之間的對映。
1.記憶體地址
記憶體地址,是一個unsigned long型的數值,用void *
來儲存也可以。為了避免型別轉換,我使用了void
*
。
2.申請者資訊
申請者的資訊比較複雜,不是一個型別可以搞定的。它包括哪些內容呢?
在C情況下,主要是需要知道誰呼叫了__wrap_malloc
。但在C++情況下,呼叫__wrap_malloc
的一定是new,這沒有什麼意義,還需要知道是誰呼叫了new。再進一步說,new有可能是在建構函式中被呼叫的,那麼很有可能我們真正需要知道的是誰呼叫了建構函式。
由此可見,僅僅知道是誰呼叫了__wrap_malloc
不夠的,我們需要的是整個棧資訊。
整個棧包含了很多內容,在這裡,我們只記錄棧的深度(int)和每一層的符號名(char **)。符號名在整個程式中是唯一的(不管C還是C++)且相對位置是確定的(動態庫除外),當程式結束時再根據符號名反推出呼叫者的檔名和行號。
為什麼不直接獲取檔名和行號?
因為求符號名的實現比較簡單。
3.對映方式
說到對映,首先想到的是map、hash這樣的東西。
但需要說明的是,這裡是__wrap_malloc
函式,是每次程式動態分配空間時必然會走到的地方。
這有什麼關係呢?想象一下,在由於某個動態申請記憶體的操作來到了這個函式,而在這個函式裡又不小心申請了一次記憶體,會怎樣呢?在-Wl,--wrap,malloc
的作用下又來到了這裡,於是開啟了“雞生蛋、蛋生雞”的死迴圈中,直到——stack overflow。
所以,在這個函式裡能使用的,只能使用棧空間或者全域性空間,如果一定要使用堆空間,也必須顯示地使用__real_malloc
代替new或者malloc。由於在map、hash中會不可避免地使用動態記憶體空間的情況,還是放棄吧。
怎麼辦呢?為了避免節外生枝,我這裡使用了最簡單但是有點笨的方法——陣列。
struct memory_record
{
void * addr;
size_t count;
int depth;
char **symbols;
}mc[1000];
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
4.怎樣獲取棧中的符號?
gcc給我們提相應的函式,按照要求呼叫就行。
char* stack[20] = {0};
mc[i].depth = backtrace(reinterpret_cast<void ** >(stack), sizeof(stack)/sizeof(stack[0]));
if (mc[i].depth){
mc[i].symbols = backtrace_symbols(reinterpret_cast<void**>(stack), mc[i].depth);
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
backtrace函式用於獲取棧的深度(depth
),以及每一層棧地址(stack
)。 backtrace_symbols
函式根據棧地址返回符號名(symbols
)。
需要注意的是,backtrace_symbols返回的是符號的陣列,這個陣列的空間是由backtrace_symbols
分配的,但需要呼叫者釋放。
為什麼這裡backtrace_symbols
分配了記憶體卻沒有引起stack overflow呢?以下是我的猜測: backtrace_symbols
函式和wrap機制都是GNU提供的,屬性親戚關係。既然是親戚,那麼大家通融一下,讓backtrace_symbols
繞過wrap機制直接使用記憶體也是有可能的。
原始碼:
#include <iostream>
using namespace std;
#include "string.h"
#include <stdio.h>
#include <malloc.h>
#include <execinfo.h>
#if(defined(_X86_) && !defined(__x86_64))
#define _ALLOCA_S_MARKER_SIZE 4
#elif defined(__ia64__) || defined(__x86_64)
#define _ALLOCA_S_MARKER_SIZE 8
#endif
size_t count = 0;
int backtrace(void **buffer, int size);
struct memory_record
{
void * addr;
size_t count;
int depth;
char **symbols;
}mc[1000];
extern "C"
{
void* __real_malloc(int c);
void * __wrap_malloc(size_t size)
{
void *p = __real_malloc(size);
size_t w = *((size_t*)((char*)p - _ALLOCA_S_MARKER_SIZE));
cout<<"malloc "<<p<<endl;
for(int i = 0; i < 1000; i++)
{
if(mc[i].count == 0)
{
count += w;
mc[i].addr = p;
mc[i].count = w;
char* stack[20] = {0};
mc[i].depth = backtrace(reinterpret_cast<void**>(stack), sizeof(stack)/sizeof(stack[0]));
if (mc[i].depth){
mc[i].symbols = backtrace_symbols(reinterpret_cast<void**>(stack), mc[i].depth);
}
break;
}
}
return p;
}
void __real_free(void *ptr);
void __wrap_free(void *ptr)
{
cout<<"free "<<ptr<<endl;
size_t w = *((size_t*)((char*)ptr - _ALLOCA_S_MARKER_SIZE));
for(int i = 0; i < 1000; i++)
{
if(mc[i].addr == ptr)
{
mc[i].count -= w;
count -= w;
if(mc[i].symbols)
__real_free(mc[i].symbols);
break;
}
}
__real_free(ptr);
}
}
void *operator new(size_t size)
{
return malloc(size);
}
void operator delete(void *ptr)
{
free(ptr);
}
void print_leaked_memory()
{
if(count != 0)
cout<<"memory leak!"<<endl;
for(int i = 0; i < 1000; i++)
{
if(mc[i].count != 0)
{
cout<<mc[i].addr<<' '<<mc[i].count<<endl;
if (mc[i].symbols){
for(size_t j = 0; j < mc[i].depth; j++){
printf("===[%d]:%s\n", (j+1), mc[i].symbols[j]);
}
}
__real_free(mc[i].symbols);
}
}
}
class A
{
int *p1;
public:
A(){p1 = new int;}
~A(){delete p1;}
};
int main(void)
{
memset(mc, 0, sizeof(mc));
count = 0;
int *p1 = new int(4);
int *p2 = new int(5);
delete p1;
print_leaked_memory();
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
編譯命令:
g++ -o test test.cpp -g -Wl,--wrap,malloc -Wl,--wrap,free
- 1
- 1
執行:
./test | grep "===" | cut -d"[" -f3 | tr -d "]" | addr2line -e test
- 1
- 1
方法分析:
優點:
(1)在程式執行結束時,列印程式記憶體洩漏情況以及導致洩漏發生的程式碼所在的檔案及行號
(2)C/C++都適用
(3)需要修改產品原始碼即可實現功能
(4)對一起連結的所有.o和靜態庫都有效
缺點:
(1)對動態庫不適用
(2)求堆疊資訊和求檔名行號是兩個操作,不能一次性解決問題