Android中native進程記憶體泄露的調試技巧

來源:互聯網
上載者:User
Android中native進程記憶體泄露的調試技巧
紅狼部落格

代碼基於Android2.3.x版本

Android為Java程式提供了方便的記憶體泄露資訊和工具(如MAT),便於尋找。但是,對於純粹C/C++ 編寫的natvie進程,卻不那麼容易尋找記憶體泄露。傳統的C/C++程式可以使用valgrind工具,也可以使用某些代碼檢查工具。幸運的是,Google的bionic庫為我們尋找記憶體泄露提供了一個非常棒的API--get_malloc_leak_info。利用它,我們很容易通過得到backtrace的方式找到涉嫌記憶體泄露的地方。

代碼原理分析

我們可以使用adb shell setprop libc.debug.malloc 1來設定記憶體的調試等級(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.
*/

對於不同的調試等級,記憶體配置管理函數操作控制代碼將指向不同的記憶體配置管理函數。這樣,記憶體的分配和釋放,在不同的的調試等級下,將使用不同的函數版本。
詳細過程如下:

如下面代碼注釋所說,在__libc_init常式中會調用malloc_debug_init進行初始化,進而調用malloc_init_impl(在一個進程中,使用pthread_once保證其只被執行一次)

在malloc_init_impl中,會開啟對應的C庫,解析出函數符號:malloc_debug_initialize(見行366),並執行之(行373)

當debug_level被設定為1、5、10時,開啟庫”/system/lib/libc_malloc_debug_leak.so”。在檔案bionic/libc/bionic/malloc_debug_leak.c中,實現了malloc_debug_initialize,但只為返回0的空函數。若為20,則開啟的是:”/system/lib/libc_malloc_debug_qemu.so”

接著,針對不同的debug_level,解析出不同的記憶體操作函數malloc/free/calloc/realloc/memalign實現:

對於debug_level等級1、5、10的情況,malloc/free/calloc/realloc/memalign各種版本的實現位於檔案bionic/libc/bionic/malloc_debug_leak.c中。如debug_level為5時的情況,malloc/free/則是在分配記憶體時將分配的記憶體填充為0xeb,釋放時填充為0xef:

當debug_level為1調試memory leak時,其實現是打出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函數在實際分配的bytes位元組前額外分配了一塊資料用作AllocationEntry。在分配記憶體成功後,分配了一個擁有32個元素的指標數組,用於存放呼叫堆疊指標,調用函數get_backtrace將呼叫堆疊儲存起來,也就是將各函數指標儲存到數組backtrace中;然後使用record_backtrace記錄下該呼叫堆疊,然後讓AllocationEntry的entry成員指向它。函數record_backtrace會通過hash值在全域呼叫堆疊表gHashTable裡尋找。若沒找到,則建立一項呼叫堆疊資訊,將其加入到全域表中。最後,將base所指向的地方往後移一下,然後它,就是分配的記憶體位址。
可見,該版本的malloc函數額外記錄了呼叫堆疊的資訊。通過在分配的記憶體塊前加一個頭的方式,儲存了如何查詢hash表呼叫堆疊資訊的entry。

再來看一下record_backtrace函數,在分析其代碼之前,看一下結構體(檔案malloc_debug_common.h):
struct HashEntry {
size_t slot;// HashTable中的slots數組索引
HashEntry* prev;//前一項
HashEntry* next;//後一項,新添加時添加到後面
size_t numEntries;//呼叫堆疊中的函數指標數量
// fields above “size” are NOT sent to the host
size_t size;//表示該次malloc操作所分配的記憶體數
size_t allocations;//調用的次數,即此處的malloc被調用了多少次
intptr_t backtrace[0];//呼叫堆疊
};

typedef struct HashTable HashTable;
struct HashTable {
size_t count;
HashEntry* slots[HASHTABLE_SIZE];//HASHTABLE_SIZE=1543
};
和在一個進程中,有一個全域的變數gHashTable,用於記錄誰最終調用了malloc分配記憶體的呼叫堆疊列表。gHashTable的類型是HashTable,其有一個指標,這個指標指向一個slots數組,該數組的最大容量是1543;數組中有多少有效值由另一個成員count記錄。可以通過backtrace和 numEntries得到hash值,再與HASHTABLE_SIZE整除得到HashEntry在該數組中的索引,這樣就可以根據自身資訊根據hash,快速得到在數組中的索引。
另一個結構體是HashEntry,因其成員存在指向前後的指標,所以它也是個鏈表,hash值相同將添加到鏈表的後面。HashEntry第一個成員slot就是自身在數組中的索引,亦即由hash運算而來;最後一項即呼叫堆疊backtrace[0],裡面是函數指標,這個數組具體有多少項則由另一個成員numEntries記錄;size表示該次分配的記憶體的大小;allocations是分配次數,即有多少次同一調用路徑。
這兩個資料結構關係可由表示:

在leak_malloc中調用record_backtrace記錄堆棧資訊時,先由backtrace和numEntries得到hash值,再整除運算後得到在gHashTable中的數組索引;接著檢查是否已經存在該項,即有沒有分配了相同記憶體大小、同一調用路徑、記錄了相當數量的函數指標的HashEntry。若有,則直接在原有項上的allocations加1,沒有則建立新項:為HashEntry結構體分配記憶體(見行151,注意最後一個成員backtrace需要根據numEntries值來確定其有多少項),然後呼叫堆疊資訊複製給HashEntry最後的一個成員backtrace。最後,還要為整個表格增加計數。
這樣record_backtrace函數完成了向全域表中添加backtrace資訊的任務:要麼新增加一項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數組索引

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);
//上面一行: 在全域表中搜尋該項是否已經存在,即是否該調用路徑是否已經被調用過
if (entry != NULL) {
entry->allocations++;//若調用過,則增加計數
} else {//若沒有調用,則建立一新項
// create a new entry
entry = (HashEntry*)dlmalloc(sizeof(HashEntry) + numEntries*sizeof(intptr_t));//為該項分配記憶體,
if (!entry)//接上一行:因HashEntry最後一項是intptr_t backtrace[0];故它是一動態長度,所有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結構體的後面的記憶體中

gHashTable.slots[slot] = entry;//將新分配的並經過賦值的一項HashEntry添加到HashTable中的數組中去

if (entry->next != NULL) {
entry->next->prev = entry;
}

// we just added an entry, increase the size of the hashtable
gHashTable.count++;//增加計數
}

return entry;
}

在leak_free函數中會釋放上述全域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);
}
}

因此,在全域表中剩下的未被釋放的項,就是分配了記憶體但未被釋放的調用了malloc的呼叫堆疊。

get_malloc_leak_info

函數get_malloc_leak_info用於擷取記憶體泄露資訊。在分配記憶體時,記錄下呼叫堆疊,在釋放時清除它們。這樣,剩下的就很有可能是產生記憶體泄露的根源。那麼如何擷取該記憶體呼叫堆疊全域hash表呢?在檔案malloc_debug_common.c中提供了函數get_malloc_leak_info,可以擷取該堆棧資訊。
函數get_malloc_leak_info接收5個參數,用於各種存放各種變數的地址,調用結束後,這些變數將得到修改。如其代碼注釋所說:
*info將指向在該函數中分配的整塊記憶體,這些記憶體空間大小為overallSize;
整個空間若干小項組成,每項的大小為infoSize,這個小項的資料結構等同於HashEntry中自size成員開始的結構,即第一個成員是malloc分配的記憶體大小,第二個成員是allocations,即多次有著相同呼叫堆疊的計數,最後一項是backtrace,共32(BACKTRACE_SIZE)個指標值的空間。因此,*info指向的大記憶體塊包含了共有overallSize/infoSize個小項。注意HashEntry中backtrace數組是按實際數量分配的,而此處則統一按32個分配空間,若不到32個,則後面的值置0;
totalMemory是malloc分配的所有記憶體的大小;
最後一個參數是backtraceSize,即32(BACKTRACE_SIZE)

函數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;
}

接著查看全域堆棧表中有多少項,然後分配一塊記憶體,用於儲存指標,這些指標用於指向gHashTable中的所有HashEntry項,並順便計數出已指派但未釋放的記憶體總數量totalMemory用於返回給調用者。最後一個參數是呼叫堆疊中的函數指標個數,實際值為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 +//計算總分配的記憶體
((entry->size & ~SIZE_FLAG_MASK) * entry->allocations);
index++;
entry = entry->next;//讓entry指向下一個,即相同的slot值
}
}//經過此for迴圈,將全域表中所有的堆棧項指標存放到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;//計算所有呼叫堆疊項所需記憶體
*backtraceSize = BACKTRACE_SIZE;

最後,為所有呼叫堆疊項資訊分配記憶體,即info指向的地方;並將gHashTable中的呼叫堆疊資訊(即list表中的HashEntry自其結構體成員size後面的值)拷貝到info所指向的記憶體中。

// now get A byte array big enough for this
*info = (uint8_t*)dlmalloc(*overallSize);//為所有堆棧項分配記憶體,包括各項的2個size_t變數

if (*info == NULL) {//分配不成功,沒記憶體了
*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);//呼叫堆疊32項中未填滿的部分
} else {
/* make sure the amount we’re copying doesn’t exceed the limit */
entrySize = *infoSize;
}//下面的一行將32個指標佔用空間加上前面兩個size_t變數的值複製到info項中
memcpy(head, &(entry->size), entrySize);//size_t變數分別為size和allocations
head += *infoSize;//讓head指向下一個info所在記憶體
}

out_nomem_info:
dlfree(list);

done:
pthread_mutex_unlock(&gAllocationsMutex);
}

當程式運行結束時,一般來說,記憶體都應該釋放,這時我們可以調用get_malloc_leak_info擷取未被釋放的呼叫堆疊項。原理上,這些就是記憶體泄露的地方。但實際情況可能是,在我們運行get_malloc_leak_info時,某些記憶體應該保留還不應該釋放。
另外,我們有時要檢查的進程是守護進程,不會退出。所以有些記憶體應該一直保持下去,不被釋放。這時,我們可以選擇某個狀態的一個時刻來查看未釋放的記憶體,比如在剛進入時的idle狀態時的一個時刻,使用get_malloc_leak_info擷取未釋放的記憶體資訊,然後在程式執行某些操作結束後返回Idle狀態時,再次使用get_malloc_leak_info擷取未釋放的記憶體資訊。兩種資訊對比,新多出來的呼叫堆疊項,就存在涉嫌記憶體泄露。
使用get_malloc_leak_info函數的範例代碼如下:

typedef struct {
size_t size;//分配的記憶體
size_t dups;//重複數
intptr_t * backtrace;//呼叫堆疊指標
} 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];//數組

for (size_t i = 0; i < count; i++) {讓擷取的堆棧資訊填充到 AllocEntry數組中
// 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;
}

具體調試步驟:
參考http://freepine.blogspot.com/2010/02/analyze-memory-leak-of-android-native.html
下載其補丁包和python工具包
將代碼補丁達到android源碼中的frameworks/base下,重新編譯產生image,燒進手機板裡,這時會在/system/bin/下有個二進位程式memorydumper。該代碼補丁包向mediaserver進程中添加一個服務,二進位程式通過Binder IPC使用該服務。該服務使用get_malloc_leak_info擷取未釋放記憶體資訊。

step1.設定調試等級並重啟mediaserver進程
adb shell setprop libc.debug.malloc 1
adb shell ps mediaserver
adb shell kill <mediaserver_pid>

它的目的是讓mediaserver進程使用leak_malloc的版本。當設定調試等級後,殺死mediaserver進程,android系統將自動重啟它。這時,它重新載入libc庫,記憶體配置函數通過handle將使用leak_malloc、leak_free版本。
Step2:在某初始狀態下,如在使用“照相機”程式之前,執行memorydumper,記錄下此時未釋放的記憶體:
$ adb shell /system/bin/memorydumper
$ adb pull /data/memstatus_<mediaserver_pid>.0 .

Step3:執行某些操作,如拍照、錄製視頻或播放幾首歌曲,然後退出這些應用程式;

Step4:再次執行memorydumper,記錄下此時未釋放的記憶體;通過比較工具,比較此次和step2中的差異;這些差異就是有記憶體泄露嫌疑的地方。因為第一得到的未釋放的可能就是那個時刻不該釋放的,比較就是將它們排除掉。
$ adb pull /data/memstatus_<mediaserver_pid>.1 .
$ diff memstatus_<mediaserver_pid>.0 memstatus_<mediaserver_pid>.1 >diff_0_1

Step5:擷取maps檔案。根據該檔案,可以得到.so庫檔案所在位址範圍空間,用於將呼叫堆疊函數符號位址解析出來。
$ adb pull /proc/<mediaserver_pid>/maps your_path

Step5.執行參考連結中的python指令碼:
./addr2func.py –root-dir=~/u8500-android-2.3_v4.30 –maps-file=maps –product=u8500 diff._0_1>memleak.backtrace
該指令碼將通過分析maps檔案得到位址區段對應的庫檔案所佔用的地址空間,得到每個呼叫堆疊的地址對應的庫,通過下面的命令,得到對應的經過編譯器mangled後的函數名稱、源檔案及其行號:
[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進行函數的demangle,得到與源碼一致的函數名稱,使我們更易辨認。

一個例子的snapshot:
下面的是第一次使用memorydumper得到的呼叫堆疊地址:

下面的是第二次使用memorydumper得到的呼叫堆疊地址:

兩者進行diff比較後得到的差異:

使用addr2func後得到的呼叫堆疊:

本文連結地址: http://www.redwolf-blog.com/?p=1233

原創文章,著作權紅狼部落格所有, 轉載隨意,但請註明出處。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.