代碼基于Android2.3.x版本
Android為Java程序提供了方便的內(nèi)存泄露信息和工具(如MAT),便于查找。但是,對于純粹C/C++ 編寫的natvie進程,卻不那么容易查找內(nèi)存泄露。傳統(tǒng)的C/C++程序可以使用valgrind工具,也可以使用某些代碼檢查工具。幸運的是,Google的bionic庫為我們查找內(nèi)存泄露提供了一個非常棒的API--get_malloc_leak_info。利用它,我們很容易通過得到backtrace的方式找到涉嫌內(nèi)存泄露的地方。
代碼原理分析
我們可以使用adb shell setprop libc.debug.malloc 1來設(shè)置內(nèi)存的調(diào)試等級(debug_level),更詳細的等級解釋見文件bionic/libc/bionic/malloc_debug_common.c中的注釋:
/* Handle to shared library where actual memory allocation is implemented.
* This library is loaded and memory allocation calls are redirected there
* when libc.debug.malloc environment variable contains value other than
* zero:
* 1 – For memory leak detections.
* 5 – For filling allocated / freed memory with patterns defined by
* CHK_SENTINEL_VALUE, and CHK_FILL_FREE macros.
* 10 – For adding pre-, and post- allocation stubs in order to detect
* buffer overruns.
* Note that emulator’s memory allocation instrumentation is not controlled by
* libc.debug.malloc value, but rather by emulator, started with -memcheck
* option. Note also, that if emulator has started with -memcheck option,
* emulator’s instrumented memory allocation will take over value saved in
* libc.debug.malloc. In other words, if emulator has started with -memcheck
* option, libc.debug.malloc value is ignored.
* Actual functionality for debug levels 1-10 is implemented in
* libc_malloc_debug_leak.so, while functionality for emultor’s instrumented
* allocations is implemented in libc_malloc_debug_qemu.so and can be run inside
* the emulator only.
*/
對于不同的調(diào)試等級,內(nèi)存分配管理函數(shù)操作句柄將指向不同的內(nèi)存分配管理函數(shù)。這樣,內(nèi)存的分配和釋放,在不同的的調(diào)試等級下,將使用不同的函數(shù)版本。
詳細過程如下:
如下面代碼注釋所說,在__libc_init例程中會調(diào)用malloc_debug_init進行初始化,進而調(diào)用malloc_init_impl(在一個進程中,使用pthread_once保證其只被執(zhí)行一次)
在malloc_init_impl中,會打開對應(yīng)的C庫,解析出函數(shù)符號:malloc_debug_initialize(見行366),并執(zhí)行之(行373)
當(dāng)debug_level被設(shè)置為1、5、10時,打開庫”/system/lib/libc_malloc_debug_leak.so”。在文件bionic/libc/bionic/malloc_debug_leak.c中,實現(xiàn)了malloc_debug_initialize,但只為返回0的空函數(shù)。若為20,則打開的是:”/system/lib/libc_malloc_debug_qemu.so”
接著,針對不同的debug_level,解析出不同的內(nèi)存操作函數(shù)malloc/free/calloc/realloc/memalign實現(xiàn):
對于debug_level等級1、5、10的情況,malloc/free/calloc/realloc/memalign各種版本的實現(xiàn)位于文件bionic/libc/bionic/malloc_debug_leak.c中。如debug_level為5時的情況,malloc/free/則是在分配內(nèi)存時將分配的內(nèi)存填充為0xeb,釋放時填充為0xef:
當(dāng)debug_level為1調(diào)試memory leak時,其實現(xiàn)是打出backtrace:
void* leak_malloc(size_t bytes)
{
// allocate enough space infront of the allocation to store the pointer for
// the alloc structure. This will making free’ing the structer really fast!
// 1. allocate enough memory and include our header
// 2. set the base pointer to be right after our header
void* base = dlmalloc(bytes + sizeof(AllocationEntry));
if (base != NULL) {
pthread_mutex_lock(&gAllocationsMutex);
intptr_t backtrace[BACKTRACE_SIZE];
size_t numEntries = get_backtrace(backtrace, BACKTRACE_SIZE);
AllocationEntry* header = (AllocationEntry*)base;
header->entry = record_backtrace(backtrace, numEntries, bytes);
header->guard = GUARD;
// now increment base to point to after our header.
// this should just work since our header is 8 bytes.
base = (AllocationEntry*)base + 1;
pthread_mutex_unlock(&gAllocationsMutex);
}
return base;
}
該malloc函數(shù)在實際分配的bytes字節(jié)前額外分配了一塊數(shù)據(jù)用作AllocationEntry。在分配內(nèi)存成功后,分配了一個擁有32個元素的指針數(shù)組,用于存放調(diào)用堆棧指針,調(diào)用函數(shù)get_backtrace將調(diào)用堆棧保存起來,也就是將各函數(shù)指針保存到數(shù)組backtrace中;然后使用record_backtrace記錄下該調(diào)用堆棧,然后讓AllocationEntry的entry成員指向它。函數(shù)record_backtrace會通過hash值在全局調(diào)用堆棧表gHashTable里查找。若沒找到,則創(chuàng)建一項調(diào)用堆棧信息,將其加入到全局表中。最后,將base所指向的地方往后移一下,然后它,就是分配的內(nèi)存地址。
可見,該版本的malloc函數(shù)額外記錄了調(diào)用堆棧的信息。通過在分配的內(nèi)存塊前加一個頭的方式,保存了如何查詢hash表調(diào)用堆棧信息的entry。
再來看一下record_backtrace函數(shù),在分析其代碼之前,看一下結(jié)構(gòu)體(文件malloc_debug_common.h):
struct HashEntry {
size_t slot;// HashTable中的slots數(shù)組索引
HashEntry* prev;//前一項
HashEntry* next;//后一項,新添加時添加到后面
size_t numEntries;//調(diào)用堆棧中的函數(shù)指針數(shù)量
// fields above “size” are NOT sent to the host
size_t size;//表示該次malloc操作所分配的內(nèi)存數(shù)
size_t allocations;//調(diào)用的次數(shù),即此處的malloc被調(diào)用了多少次
intptr_t backtrace[0];//調(diào)用堆棧
};
typedef struct HashTable HashTable;
struct HashTable {
size_t count;
HashEntry* slots[HASHTABLE_SIZE];//HASHTABLE_SIZE=1543
};
和在一個進程中,有一個全局的變量gHashTable,用于記錄誰最終調(diào)用了malloc分配內(nèi)存的調(diào)用堆棧列表。gHashTable的類型是HashTable,其有一個指針,這個指針指向一個slots數(shù)組,該數(shù)組的最大容量是1543;數(shù)組中有多少有效的值由另一個成員count記錄??梢酝ㄟ^backtrace和 numEntries得到hash值,再與HASHTABLE_SIZE整除得到HashEntry在該數(shù)組中的索引,這樣就可以根據(jù)自身信息根據(jù)hash,快速得到在數(shù)組中的索引。
另一個結(jié)構(gòu)體是HashEntry,因其成員存在指向前后的指針,所以它也是個鏈表,hash值相同將添加到鏈表的后面。HashEntry第一個成員slot就是自身在數(shù)組中的索引,亦即由hash運算而來;最后一項即調(diào)用堆棧backtrace[0],里面是函數(shù)指針,這個數(shù)組具體有多少項則由另一個成員numEntries記錄;size表示該次分配的內(nèi)存的大小;allocations是分配次數(shù),即有多少次同一調(diào)用路徑。
這兩個數(shù)據(jù)結(jié)構(gòu)關(guān)系可由下圖表示:
在leak_malloc中調(diào)用record_backtrace記錄堆棧信息時,先由backtrace和numEntries得到hash值,再整除運算后得到在gHashTable中的數(shù)組索引;接著檢查是否已經(jīng)存在該項,即有沒有分配了相同內(nèi)存大小、同一調(diào)用路徑、記錄了相當(dāng)數(shù)量的函數(shù)指針的HashEntry。若有,則直接在原有項上的allocations加1,沒有則創(chuàng)建新項:為HashEntry結(jié)構(gòu)體分配內(nèi)存(見行151,注意最后一個成員backtrace需要根據(jù)numEntries值來確定其有多少項),然后調(diào)用堆棧信息復(fù)制給HashEntry最后的一個成員backtrace。最后,還要為整個表格增加計數(shù)。
這樣record_backtrace函數(shù)完成了向全局表中添加backtrace信息的任務(wù):要么新增加一項HashEntry,要么增加索引。
static HashEntry* record_backtrace(intptr_t* backtrace, size_t numEntries, size_t size)
{
size_t hash = get_hash(backtrace, numEntries);//得到backtrace和numEntries的hash值
size_t slot = hash % HASHTABLE_SIZE;//整除,得到的是HashTable中的HashEntry數(shù)組索引
if (size & SIZE_FLAG_MASK) {
debug_log(“malloc_debug: allocation %zx exceeds bit widthn”, size);
abort();
}
if (gMallocLeakZygoteChild)
size |= SIZE_FLAG_ZYGOTE_CHILD;
HashEntry* entry = find_entry(&gHashTable, slot, backtrace, numEntries, size);
//上面一行: 在全局表中搜索該項是否已經(jīng)存在,即是否該調(diào)用路徑是否已經(jīng)被調(diào)用過
if (entry != NULL) {
entry->allocations++;//若調(diào)用過,則增加計數(shù)
} else {//若沒有調(diào)用,則創(chuàng)建一新項
// create a new entry
entry = (HashEntry*)dlmalloc(sizeof(HashEntry) + numEntries*sizeof(intptr_t));//為該項分配內(nèi)存,
if (!entry)//接上一行:因HashEntry最后一項是intptr_t backtrace[0];故它是一動態(tài)長度,所有numEntries*sizeof(intptr_t)
return NULL;
entry->allocations = 1;
entry->slot = slot;
entry->prev = NULL;
entry->next = gHashTable.slots[slot];
entry->numEntries = numEntries;
entry->size = size;
memcpy(entry->backtrace, backtrace, numEntries * sizeof(intptr_t));//將backtrace拷貝到entry結(jié)構(gòu)體的后面的內(nèi)存中
gHashTable.slots[slot] = entry;//將新分配的并經(jīng)過賦值的一項HashEntry添加到HashTable中的數(shù)組中去
if (entry->next != NULL) {
entry->next->prev = entry;
}
// we just added an entry, increase the size of the hashtable
gHashTable.count++;//增加計數(shù)
}
return entry;
}
在leak_free函數(shù)中會釋放上述全局hash表中的堆棧項(見行550):
void leak_free(void* mem)
{
if (mem != NULL) {
pthread_mutex_lock(&gAllocationsMutex);
// check the guard to make sure it is valid
AllocationEntry* header = (AllocationEntry*)mem – 1;
if (header->guard != GUARD) {
// could be a memaligned block
if (((void**)mem)[-1] == MEMALIGN_GUARD) {
mem = ((void**)mem)[-2];
header = (AllocationEntry*)mem – 1;
}
}
if (header->guard == GUARD || is_valid_entry(header->entry)) {
// decrement the allocations
HashEntry* entry = header->entry;
entry->allocations–;
if (entry->allocations <= 0) {
remove_entry(entry);
dlfree(entry);
}
// now free the memory!
dlfree(header);
} else {
debug_log(“WARNING bad header guard: ’0x%x’! and invalid entry: %pn”,
header->guard, header->entry);
}
pthread_mutex_unlock(&gAllocationsMutex);
}
}
因此,在全局表中剩下的未被釋放的項,就是分配了內(nèi)存但未被釋放的調(diào)用了malloc的調(diào)用堆棧。
get_malloc_leak_info
函數(shù)get_malloc_leak_info用于獲取內(nèi)存泄露信息。在分配內(nèi)存時,記錄下調(diào)用堆棧,在釋放時清除它們。這樣,剩下的就很有可能是產(chǎn)生內(nèi)存泄露的根源。那么如何獲取該內(nèi)存調(diào)用堆棧全局hash表呢?在文件malloc_debug_common.c中提供了函數(shù)get_malloc_leak_info,可以獲取該堆棧信息。
函數(shù)get_malloc_leak_info接收5個參數(shù),用于各種存放各種變量的地址,調(diào)用結(jié)束后,這些變量將得到修改。如其代碼注釋所說:
*info將指向在該函數(shù)中分配的整塊內(nèi)存,這些內(nèi)存空間大小為overallSize;
整個空間若干小項組成,每項的大小為infoSize,這個小項的數(shù)據(jù)結(jié)構(gòu)等同于HashEntry中自size成員開始的結(jié)構(gòu),即第一個成員是malloc分配的內(nèi)存大小,第二個成員是allocations,即多次有著相同調(diào)用堆棧的計數(shù),最后一項是backtrace,共32(BACKTRACE_SIZE)個指針值的空間。因此,*info指向的大內(nèi)存塊包含了共有overallSize/infoSize個小項。注意HashEntry中backtrace數(shù)組是按實際數(shù)量分配的,而此處則統(tǒng)一按32個分配空間,若不到32個,則后面的值置0;
totalMemory是malloc分配的所有內(nèi)存的大?。?br>
最后一個參數(shù)是backtraceSize,即32(BACKTRACE_SIZE)
函數(shù)get_malloc_leak_info首先檢查傳遞進來的變量是否合法,以及全局堆棧中是否有堆棧項:
void get_malloc_leak_info(uint8_t** info, size_t* overallSize,
size_t* infoSize, size_t* totalMemory, size_t* backtraceSize)
{
// don’t do anything if we have invalid arguments
if (info == NULL || overallSize == NULL || infoSize == NULL ||
totalMemory == NULL || backtraceSize == NULL) {
return;
}
*totalMemory = 0;
pthread_mutex_lock(&gAllocationsMutex);
if (gHashTable.count == 0) {
*info = NULL;
*overallSize = 0;
*infoSize = 0;
*backtraceSize = 0;
goto done;
}
接著查看全局堆棧表中有多少項,然后分配一塊內(nèi)存,用于保存指針,這些指針用于指向gHashTable中的所有HashEntry項,并順便計數(shù)出已分配但未釋放的內(nèi)存總數(shù)量totalMemory用于返回給調(diào)用者。最后一個參數(shù)是調(diào)用堆棧中的函數(shù)指針個數(shù),實際值為BACKTRACE_SIZE,即32。.
void** list = (void**)dlmalloc(sizeof(void*) * gHashTable.count);
// get the entries into an array to be sorted
int index = 0;
int i;
for (i = 0 ; i < HASHTABLE_SIZE ; i++) {//遍歷gHashTable全部項
HashEntry* entry = gHashTable.slots[i];
while (entry != NULL) {//有效項放到list中去
list[index] = entry;
*totalMemory = *totalMemory +//計算總分配的內(nèi)存
((entry->size & ~SIZE_FLAG_MASK) * entry->allocations);
index++;
entry = entry->next;//讓entry指向下一個,即相同的slot值
}
}//經(jīng)過此for循環(huán),將全局表中所有的堆棧項指針存放到list指向的表中
// XXX: the protocol doesn’t allow variable size for the stack trace (yet)
*infoSize = (sizeof(size_t) * 2) + (sizeof(intptr_t) * BACKTRACE_SIZE);//32個指針值項,
//注意: info前面是兩個size_t變量,它們是HashEntry中的size和allocations兩個成員,后面是backtrace
*overallSize = *infoSize * gHashTable.count;//計算所有調(diào)用堆棧項所需內(nèi)存
*backtraceSize = BACKTRACE_SIZE;
最后,為所有調(diào)用堆棧項信息分配內(nèi)存,即info指向的地方;并將gHashTable中的調(diào)用堆棧信息(即list表中的HashEntry自其結(jié)構(gòu)體成員size后面的值)拷貝到info所指向的內(nèi)存中。
// now get A byte array big enough for this
*info = (uint8_t*)dlmalloc(*overallSize);//為所有堆棧項分配內(nèi)存,包括各項的2個size_t變量
if (*info == NULL) {//分配不成功,沒內(nèi)存了
*overallSize = 0;
goto out_nomem_info;
}
qsort((void*)list, gHashTable.count, sizeof(void*), hash_entry_compare);//為列表中的項排序
uint8_t* head = *info;
const int count = gHashTable.count;
for (i = 0 ; i < count ; i++) {
HashEntry* entry = list[i];
size_t entrySize = (sizeof(size_t) * 2) + (sizeof(intptr_t) * entry->numEntries);
if (entrySize < *infoSize) {
/* we’re writing less than a full entry, clear out the rest */
memset(head + entrySize, 0, *infoSize – entrySize);//調(diào)用堆棧32項中未填滿的部分
} else {
/* make sure the amount we’re copying doesn’t exceed the limit */
entrySize = *infoSize;
}//下面的一行將32個指針占用空間加上前面兩個size_t變量的值復(fù)制到info項中
memcpy(head, &(entry->size), entrySize);//size_t變量分別為size和allocations
head += *infoSize;//讓head指向下一個info所在內(nèi)存
}
out_nomem_info:
dlfree(list);
done:
pthread_mutex_unlock(&gAllocationsMutex);
}
當(dāng)程序運行結(jié)束時,一般來說,內(nèi)存都應(yīng)該釋放,這時我們可以調(diào)用get_malloc_leak_info獲取未被釋放的調(diào)用堆棧項。原理上,這些就是內(nèi)存泄露的地方。但實際情況可能是,在我們運行g(shù)et_malloc_leak_info時,某些內(nèi)存應(yīng)該保留還不應(yīng)該釋放。
另外,我們有時要檢查的進程是守護進程,不會退出。所以有些內(nèi)存應(yīng)該一直保持下去,不被釋放。這時,我們可以選擇某個狀態(tài)的一個時刻來查看未釋放的內(nèi)存,比如在剛進入時的idle狀態(tài)時的一個時刻,使用get_malloc_leak_info獲取未釋放的內(nèi)存信息,然后在程序執(zhí)行某些操作結(jié)束后返回Idle狀態(tài)時,再次使用get_malloc_leak_info獲取未釋放的內(nèi)存信息。兩種信息對比,新多出來的調(diào)用堆棧項,就存在涉嫌內(nèi)存泄露。
使用get_malloc_leak_info函數(shù)的樣例代碼如下:
typedef struct {
size_t size;//分配的內(nèi)存
size_t dups;//重復(fù)數(shù)
intptr_t * backtrace;//調(diào)用堆棧指針
} AllocEntry;
uint8_t *info = NULL;
size_t overallSize = 0;
size_t infoSize = 0;
size_t totalMemory = 0;
size_t backtraceSize = 0;
get_malloc_leak_info(&info, &overallSize, &infoSize, &totalMemory, &backtraceSize);
LOGI(“returned from get_malloc_leak_info, info=0x%x, overallSize=%d, infoSize=%d, totalMemory=%d, backtraceSize=%d”, (int)info, overallSize, infoSize, totalMemory, backtraceSize);
if (info) {
uint8_t *ptr = info;
size_t count = overallSize / infoSize;
snprintf(buffer, SIZE, ” Allocation count %in”, count);
result.append(buffer);
snprintf(buffer, SIZE, ” Total meory %in”, totalMemory);
result.append(buffer);
AllocEntry * entries = new AllocEntry[count];//數(shù)組
for (size_t i = 0; i < count; i++) {讓獲取的堆棧信息填充到 AllocEntry數(shù)組中
// Each entry should be size_t, size_t, intptr_t[backtraceSize]
AllocEntry *e = &entries[i];
e->size = *reinterpret_cast<size_t *>(ptr);
ptr += sizeof(size_t);
e->dups = *reinterpret_cast<size_t *>(ptr);
ptr += sizeof(size_t);
e->backtrace = reinterpret_cast<intptr_t *>(ptr);
ptr += sizeof(intptr_t) * backtraceSize;
}
具體調(diào)試步驟:
參考http://freepine./2010/02/analyze-memory-leak-of-android-native.html
下載其補丁包和python工具包
將代碼補丁達到android源碼中的frameworks/base下,重新編譯生成image,燒進手機板里,這時會在/system/bin/下有個二進制程序memorydumper。該代碼補丁包向mediaserver進程中添加一個服務(wù),二進制程序通過Binder IPC使用該服務(wù)。該服務(wù)使用get_malloc_leak_info獲取未釋放內(nèi)存信息。
step1.設(shè)置調(diào)試等級并重啟mediaserver進程
adb shell setprop libc.debug.malloc 1
adb shell ps mediaserver
adb shell kill <mediaserver_pid>
它的目的是讓mediaserver進程使用leak_malloc的版本。當(dāng)設(shè)置調(diào)試等級后,殺死m(xù)ediaserver進程,android系統(tǒng)將自動重啟它。這時,它重新加載libc庫,內(nèi)存分配函數(shù)通過handle將使用leak_malloc、leak_free版本。
Step2:在某初始狀態(tài)下,如在使用“照相機”程序之前,執(zhí)行memorydumper,記錄下此時未釋放的內(nèi)存:
$ adb shell /system/bin/memorydumper
$ adb pull /data/memstatus_<mediaserver_pid>.0 .
Step3:執(zhí)行某些操作,如拍照、錄制視頻或播放幾首歌曲,然后退出這些應(yīng)用程序;
Step4:再次執(zhí)行memorydumper,記錄下此時未釋放的內(nèi)存;通過比較工具,比較此次和step2中的差異;這些差異就是有內(nèi)存泄露嫌疑的地方。因為第一得到的未釋放的可能就是那個時刻不該釋放的,比較就是將它們排除掉。
$ adb pull /data/memstatus_<mediaserver_pid>.1 .
$ diff memstatus_<mediaserver_pid>.0 memstatus_<mediaserver_pid>.1 >diff_0_1
Step5:獲取maps文件。根據(jù)該文件,可以得到.so庫文件所在地址范圍空間,用于將調(diào)用堆棧函數(shù)符號地址解析出來。
$ adb pull /proc/<mediaserver_pid>/maps your_path
Step5.執(zhí)行參考鏈接中的python腳本:
./addr2func.py –root-dir=~/u8500-android-2.3_v4.30 –maps-file=maps –product=u8500 diff._0_1>memleak.backtrace
該腳本將通過分析maps文件得到地址段對應(yīng)的庫文件所占用的地址空間,得到每個調(diào)用堆棧的地址對應(yīng)的庫,通過下面的命令,得到對應(yīng)的經(jīng)過編譯器mangled后的函數(shù)名稱、源文件及其行號:
[root-dir]/prebuilt/linux-x86/toolchain/arm-eabi-4.4.0/bin/arm-eabi-addr2line -f -e [root-dir]/ /out/target/product/[product]/symbols/[libname] callstack_address
然后使用[root-dir]/prebuilt/linux-x86/toolchain/arm-eabi-4.4.0/bin/arm-eabi-c++filt進行函數(shù)的demangle,得到與源碼一致的函數(shù)名稱,使我們更易辨認。
一個例子的snapshot:
下面的截圖是第一次使用memorydumper得到的調(diào)用堆棧地址:
下面的截圖是第二次使用memorydumper得到的調(diào)用堆棧地址:
兩者進行diff比較后得到的差異:
使用addr2func后得到的調(diào)用堆棧:
