一酸茴、LeakTracer介紹
簡單來說分预,該庫主要是通過重寫libc中的malloc、free薪捍、new笼痹、delete這些函數(shù)和操作符,記錄內(nèi)存申請和釋放操作來判斷程序是否可能出現(xiàn)了內(nèi)存泄漏酪穿。
二凳干、使用介紹
- 該庫主要是提供了以下幾個函數(shù)對外使用,用于內(nèi)存泄漏檢測昆稿,其中writeLeaksToFile函數(shù)用于將內(nèi)存泄漏信息輸出到文件
/** starts monitoring memory allocations in all threads */
inline void startMonitoringAllThreads(void);
/** starts monitoring memory allocations in current thread */
inline void startMonitoringThisThread(void);
/** stops monitoring memory allocations (in all threads or in
* this thread only, depends on the function used to start
* monitoring */
inline void stopMonitoringAllocations(void);
/** stops all monitoring - both of allocations and releases */
inline void stopAllMonitoring(void);
/** writes report with all memory leaks */
void writeLeaksToFile(const char *reportFileName);
-
然后用該庫提供的leak-tracer-helpers文件夾中的工具對輸出的日志進行分析,打印出內(nèi)存泄漏的代碼位置及調(diào)用棧信息息拜,效果如下
注意: leak-tracer-helpers目錄下的leak-analyze-addr2line工具其實是一堆shell代碼溉潭,用到了addr2line工具净响,需要將NDK中的addr2line加到環(huán)境變量Path中。
三喳瓣、源碼分析
這里以如下流程為例分析內(nèi)存泄漏檢測流程:
調(diào)用startMonitoringAllThreads初始化
手動造成內(nèi)存泄漏
調(diào)用stopAllMonitoring停止檢測
調(diào)用writeLeaksToFile輸出內(nèi)存泄漏信息
初始化
先來看看startMonitoringAllThreads函數(shù)馋贤,這里主要是調(diào)用了leaktracer::MemoryTrace::Setup()進行初始化操作
inline void MemoryTrace::startMonitoringAllThreads(void) {
leaktracer::MemoryTrace::Setup();
TRACE((stderr, "LeakTracer: startMonitoringAllThreads\n"));
if (!__monitoringReleases) {
MutexLock lock(__allocations_mutex);
// double-check inside Mutex
if (!__monitoringReleases) {
__allocations.clearAllInfo();
__monitoringReleases = true;
}
}
// 將這個標記位置為true,后續(xù)用于判斷
__monitoringAllThreads = true;
stopMonitoringPerThreadAllocations();
}
然后在leaktracer::MemoryTrace::Setup()中看到畏陕,主要是執(zhí)行了MemoryTrace::init_no_alloc_allowed()
int MemoryTrace::Setup(void)
{
pthread_once(&MemoryTrace::_init_no_alloc_allowed_once, MemoryTrace::init_no_alloc_allowed);
if (!leaktracer::MemoryTrace::GetInstance().AllMonitoringIsDisabled()) {
pthread_once(&MemoryTrace::_init_full_once, MemoryTrace::init_full_from_once);
}
#if 0
else if (!leaktracer::MemoryTrace::GetInstance().__setupDone) {
}
#endif
return 0;
}
在MemoryTrace::init_no_alloc_allowed()函數(shù)中做了最關(guān)鍵的兩件事:
調(diào)用dlsym函數(shù)找到libc中的calloc,malloc等函數(shù)配乓,記錄在libc_alloc_func_t結(jié)構(gòu)體的localredirect變量中
調(diào)用dladdr函數(shù),用于記錄動態(tài)庫加載時的基礎(chǔ)偏移量惠毁,Dl_info結(jié)構(gòu)體的內(nèi)容如下犹芹,dli_fbase用于記錄so加載時基礎(chǔ)偏移量,這個后續(xù)在計算指令相對于動態(tài)庫的偏移量時會用到
typedef struct {
/* Pathname of shared object that contains address. */
const char* dli_fname;
/* Address at which shared object is loaded. */
void* dli_fbase;
/* Name of nearest symbol with address lower than addr. */
const char* dli_sname;
/* Exact address of symbol named in dli_sname. */
void* dli_saddr;
} Dl_info;
typedef struct {
const char *symbname;
void *libcsymbol;
void **localredirect;
} libc_alloc_func_t;
static Dl_info s_P2pSODlInfo;
static libc_alloc_func_t libc_alloc_funcs[] = {
{ "calloc", (void*)__libc_calloc, (void**)(<_calloc) },
{ "malloc", (void*)__libc_malloc, (void**)(<_malloc) },
{ "realloc", (void*)__libc_realloc, (void**)(<_realloc) },
{ "free", (void*)__libc_free, (void**)(<_free) }
};
void MemoryTrace::init_no_alloc_allowed()
{
libc_alloc_func_t *curfunc;
unsigned i;
// 記錄libc中需要重寫的函數(shù)的地址
for (i=0; i<(sizeof(libc_alloc_funcs)/sizeof(libc_alloc_funcs[0])); ++i) {
curfunc = &libc_alloc_funcs[i];
if (!*curfunc->localredirect) {
if (curfunc->libcsymbol) {
*curfunc->localredirect = curfunc->libcsymbol;
} else {
*curfunc->localredirect = dlsym(RTLD_NEXT, curfunc->symbname);
}
}
}
// 調(diào)用一次dladdr鞠绰,用于記錄動態(tài)庫加載時的基礎(chǔ)偏移量
dladdr((const void*)init_no_alloc_allowed, &s_P2pSODlInfo);
__instance = reinterpret_cast<MemoryTrace*>(&s_memoryTrace_instance);
// we're using a c++ placement to initialized the MemoryTrace object living in the data section
new (__instance) MemoryTrace();
// it seems some implementation of pthread_key_create use malloc() internally (old linuxthreads)
// these are not supported yet
pthread_key_create(&__instance->__thread_internal_disabler_key, NULL);
}
以上便是初始化相關(guān)的主要內(nèi)容腰埂,接下來看下申請內(nèi)存和釋放內(nèi)存時的操作
- 內(nèi)存申請
這里定義的lt_malloc、lt_free等函數(shù)就是當時初始化時的函數(shù)蜈膨,指向了libc中的malloc屿笼、free等函數(shù)
/*
* underlying allocation, de-allocation used within
* this tool
*/
#define LT_MALLOC (*lt_malloc)
#define LT_FREE (*lt_free)
#define LT_REALLOC (*lt_realloc)
#define LT_CALLOC (*lt_calloc)
在代碼中實際調(diào)用的malloc將被重寫為如下內(nèi)容,主要是在調(diào)用libc的malloc后對內(nèi)存申請進行記錄
void *malloc(size_t size)
{
void *p;
leaktracer::MemoryTrace::Setup();
leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadUp();
p = LT_MALLOC(size);
leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadDown();
leaktracer::MemoryTrace::GetInstance().registerAllocation(p, size, false);
return p;
}
將內(nèi)存申請記錄存到__allocations這個Map中翁巍,包含內(nèi)存地址驴一、大小、時間灶壶、調(diào)用棧信息肝断,其中需要關(guān)注下記錄調(diào)用棧信息的實現(xiàn)
inline void MemoryTrace::registerAllocation(void *p, size_t size, bool is_array) {
allocation_info_t *info = NULL;
if (!AllMonitoringIsDisabled() &&
(__monitoringAllThreads || getThreadOptions().monitoringAllocations) && p != NULL) {
MutexLock lock(__allocations_mutex);
info = __allocations.insert(p);
if (info != NULL) {
info->size = size;
info->isArray = is_array;
storeTimestamp(info->timestamp);
}
}
// we store the stack without locking __allocations_mutex
// it should be safe enough
// prevent a deadlock between backtrave function who are now using advanced dl_iterate_phdr function
// and dl_* function which uses malloc functions
if (info != NULL) {
storeAllocationStack(info->allocStack);
}
if (p == NULL) {
InternalMonitoringDisablerThreadUp();
// WARNING
InternalMonitoringDisablerThreadDown();
}
}
記錄調(diào)用棧信息的實現(xiàn)如下
void MemoryTrace::storeAllocationStack(void* arr[ALLOCATION_STACK_DEPTH])
{
unsigned int iIndex = 0;
TraceHandle traceHandle;
traceHandle.backtrace = arr;
traceHandle.pos = 0;
_Unwind_Backtrace(Unwind_Trace_Fn, &traceHandle);
// fill remaining spaces
for (iIndex = traceHandle.pos; iIndex < ALLOCATION_STACK_DEPTH; iIndex++)
arr[iIndex] = NULL;
}
真正記錄調(diào)用棧的實現(xiàn)
調(diào)用_Unwind_GetIP獲取當前棧幀里的指令指針(Instruction Pointer)
和初始化時的dli_fbase相減,即可得到指令相對于動態(tài)庫的偏移量
_Unwind_Reason_Code Unwind_Trace_Fn(_Unwind_Context *context, void *hnd) {
struct TraceHandle *traceHandle = (struct TraceHandle *) hnd;
_Unwind_Word ip = _Unwind_GetIP(context);
if (traceHandle->pos != ALLOCATION_STACK_DEPTH) {
traceHandle->backtrace[traceHandle->pos] = (void *) (ip - (_Unwind_Word) s_P2pSODlInfo.dli_fbase);
++traceHandle->pos;
return _URC_NO_REASON;
}
return _URC_END_OF_STACK;
}
- 內(nèi)存釋放
和申請內(nèi)存類似例朱,釋放內(nèi)存時候孝情,通過重寫free函數(shù),將內(nèi)存申請記錄移除
void free(void* ptr)
{
leaktracer::MemoryTrace::Setup();
leaktracer::MemoryTrace::GetInstance().registerRelease(ptr, false);
LT_FREE(ptr);
}
inline void MemoryTrace::registerRelease(void *p, bool is_array) {
if (!AllMonitoringIsDisabled() && __monitoringReleases && p != NULL) {
MutexLock lock(__allocations_mutex);
allocation_info_t *info = __allocations.find(p);
if (info != NULL) {
if (info->isArray != is_array) {
InternalMonitoringDisablerThreadUp();
// WARNING
InternalMonitoringDisablerThreadDown();
}
__allocations.release(p);
}
}
}
- 記錄內(nèi)存泄漏信息
打開輸出流
void MemoryTrace::writeLeaksToFile(const char* reportFilename)
{
MutexLock lock(__allocations_mutex);
InternalMonitoringDisablerThreadUp();
std::ofstream oleaks;
oleaks.open(reportFilename, std::ios_base::out);
if (oleaks.is_open())
{
writeLeaksPrivate(oleaks);
oleaks.close();
}
else
{
std::cerr << "Failed to write to \"" << reportFilename << "\"\n";
}
InternalMonitoringDisablerThreadDown();
}
__allocations是存放內(nèi)存申請記錄的map洒嗤,其中的內(nèi)容就是申請了但是未釋放的內(nèi)存箫荡,輸出相關(guān)信息
void MemoryTrace::writeLeaksPrivate(std::ostream &out)
{
struct timespec mono, utc, diff;
allocation_info_t *info;
void *p;
double d;
const int precision = 6;
int maxsecwidth;
clock_gettime(CLOCK_REALTIME, &utc);
clock_gettime(CLOCK_MONOTONIC, &mono);
if (utc.tv_nsec > mono.tv_nsec) {
diff.tv_nsec = utc.tv_nsec - mono.tv_nsec;
diff.tv_sec = utc.tv_sec - mono.tv_sec;
} else {
diff.tv_nsec = 1000000000 - (mono.tv_nsec - utc.tv_nsec);
diff.tv_sec = utc.tv_sec - mono.tv_sec -1;
}
maxsecwidth = 0;
while(mono.tv_sec > 0) {
mono.tv_sec = mono.tv_sec/10;
maxsecwidth++;
}
if (maxsecwidth == 0) maxsecwidth=1;
out << "# LeakTracer report";
d = diff.tv_sec + (((double)diff.tv_nsec)/1000000000);
out << " diff_utc_mono=" << std::fixed << std::left << std::setprecision(precision) << d ;
out << "\n";
__allocations.beginIteration();
while (__allocations.getNextPair(&info, &p)) {
d = info->timestamp.tv_sec + (((double)info->timestamp.tv_nsec)/1000000000);
out << "leak, ";
out << "time=" << std::fixed << std::right << std::setprecision(precision) << std::setfill('0') << std::setw(maxsecwidth+1+precision) << d << ", "; // setw(16) ?
out << "stack=";
for (unsigned int i = 0; i < ALLOCATION_STACK_DEPTH; i++) {
if (info->allocStack[i] == NULL) break;
if (i > 0) out << ' ';
out << info->allocStack[i];
}
out << ", ";
out << "size=" << info->size << ", ";
out << "data=";
const char *data = reinterpret_cast<const char *>(p);
for (unsigned int i = 0; i < PRINTED_DATA_BUFFER_SIZE && i < info->size; i++)
out << (isprint(data[i]) ? data[i] : '.');
out << '\n';
}
}
- 取出日志后的分析腳本
主要是調(diào)用了addr2line將偏移地址轉(zhuǎn)換為代碼位置
#!/usr/bin/perl
use IO::Handle;
my $exe_name = shift (@ARGV);
my $log_name = shift (@ARGV);
if (!$exe_name || !$log_name) {
print "Usage: $0 <PROGRAM> <LEAKFILE>\n";
exit (1);
}
print "Processing \"$log_name\" log for \"$exe_name\"\n";
print "Matching addresses to \"$exe_name\"\n";
my %stacks;
my %addresses;
my $lines = 0;
open (LEAKFILE, $log_name) || die("failed to read from \"$log_name\"");
while (<LEAKFILE>) {
chomp;
my $line = $_;
if ($line =~ /^leak, time=([\d.]*), stack=([\w ]*), size=(\d*), data=.*/) {
$lines ++;
my $id = $2;
$stacks{$id}{COUNTER} ++;
$stacks{$id}{TIME} = $1;
$stacks{$id}{SIZE} += $3;
my @ptrs = split(/ /, $id);
foreach $ptr (@ptrs) {
$addresses{$ptr} = "unknown";
}
}
}
close (LEAKFILE);
printf "found $lines leak(s)\n";
if ($lines == 0) { exit 0; }
# resolving addresses
my @unique_addresses = keys (%addresses);
my $addr_list = "";
foreach $addr (@unique_addresses) { $addr_list .= " $addr"; }
if (!open(ADDRLIST, "addr2lineArm64 -e $exe_name $addr_list |")) { die "Failed to resolve addresses"; }
my $addr_idx = 0;
while (<ADDRLIST>) {
chomp;
$addresses{$unique_addresses[$addr_idx]} = $_;
$addr_idx++;
}
close (ADDRLIST);
# printing allocations
while (($stack, $info) = each(%stacks)) {
print $info->{SIZE}." bytes lost in ".$info->{COUNTER}." blocks (one of them allocated at ".$info->{TIME}."), from following call stack:\n";
@stack = split(/ /, $stack);
foreach $addr (@stack) { print "\t".$addresses{$addr}."\n"; }
}
PS:為了方便地對各種ABI的動態(tài)庫進行分析,我將NDK中各種ABI對應(yīng)的addr2line工具根據(jù)ABI分別命名渔隶,如arm64-v8a對應(yīng)的addr2line命名為addr2lineArm64
Demo下載地址
https://github.com/wangshengyang1996/AndroidLeakTracer
https://gitee.com/luisliuyi/android-native-leak-tracer.git