Android Native 內(nèi)存泄漏檢測工具 LeakTracer

一酸茴、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)用棧信息息拜,效果如下


    image.png

注意: 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**)(&lt_calloc) },
 { "malloc", (void*)__libc_malloc, (void**)(&lt_malloc) },
 { "realloc", (void*)__libc_realloc, (void**)(&lt_realloc) },
 { "free", (void*)__libc_free, (void**)(&lt_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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載羔挡,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末间唉,一起剝皮案震驚了整個濱河市绞灼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呈野,老刑警劉巖低矮,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異被冒,居然都是意外死亡军掂,警方通過查閱死者的電腦和手機轮蜕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝗锥,“玉大人跃洛,你說我怎么就攤上這事≈找椋” “怎么了汇竭?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長穴张。 經(jīng)常有香客問我细燎,道長,這世上最難降的妖魔是什么陆馁? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任找颓,我火速辦了婚禮,結(jié)果婚禮上叮贩,老公的妹妹穿的比我還像新娘击狮。我一直安慰自己,他們只是感情好益老,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布彪蓬。 她就那樣靜靜地躺著,像睡著了一般捺萌。 火紅的嫁衣襯著肌膚如雪档冬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天桃纯,我揣著相機與錄音酷誓,去河邊找鬼。 笑死态坦,一個胖子當著我的面吹牛盐数,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伞梯,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼玫氢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谜诫?” 一聲冷哼從身側(cè)響起漾峡,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喻旷,沒想到半個月后生逸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年槽袄,在試婚紗的時候發(fā)現(xiàn)自己被綠了伟阔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡掰伸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出怀估,到底是詐尸還是另有隱情狮鸭,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布多搀,位于F島的核電站歧蕉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏康铭。R本人自食惡果不足惜惯退,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望从藤。 院中可真熱鬧催跪,春花似錦、人聲如沸夷野。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悯搔。三九已至骑丸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妒貌,已是汗流浹背通危。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留灌曙,地道東北人菊碟。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像平匈,于是被迫代替她去往敵國和親框沟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容