到目前為止,先後通過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];
4.怎樣獲取棧中的符號?
gcc給我們提相應的函數,按照要求調用就行。
char* stack[20] = {0};
mc[i].depth = backtrace(reinterpret_cast(stack), sizeof(stack)/sizeof(stack[0]));
if (mc[i].depth){
mc[i].symbols = backtrace_symbols(reinterpret_cast(stack), mc[i].depth);
}
backtrace函數用於獲取棧的深度(depth
),以及每一層棧地址(stack
)。
backtrace_symbols
函數根據棧地址返回符號名(symbols
)。
需要注意的是,backtrace_symbols返回的是符號的數組,這個數組的空間是由backtrace_symbols
分配的,但需要調用者釋放。
為什麼這裡backtrace_symbols
分配了內存卻沒有引起stack overflow呢?以下是我的猜測:
backtrace_symbols
函數和wrap機制都是GNU提供的,屬性親戚關系。既然是親戚,那麼大家通融一下,讓backtrace_symbols
繞過wrap機制直接使用內存也是有可能的。
源代碼:
#include
using namespace std;
#include "string.h"
#include
#include
#include
#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 "<(stack), sizeof(stack)/sizeof(stack[0]));
if (mc[i].depth){
mc[i].symbols = backtrace_symbols(reinterpret_cast(stack), mc[i].depth);
}
break;
}
}
return p;
}
void __real_free(void *ptr);
void __wrap_free(void *ptr)
{
cout<<"free "<
編譯命令:
g++ -o test test.cpp -g -Wl,--wrap,malloc -Wl,--wrap,free
運行:
./test | grep "===" | cut -d"[" -f3 | tr -d "]" | addr2line -e test
方法分析:
優點:
(1)在程序運行結束時,打印程序內存洩漏情況以及導致洩漏發生的代碼所在的文件及行號
(2)C/C++都適用
(3)需要修改產品源代碼即可實現功能
(4)對一起鏈接的所有.o和靜態庫都有效
缺點:
(1)對動態庫不適用
(2)求堆棧信息和求文件名行號是兩個操作,不能一次性解決問題