Android C/C++ 內存泄漏分析 unreachable

背景

隨着對客戶端穩定性質量的不斷深入,部分的重點、難點問題逐步治理,內存質量逐步成爲了影響客戶端質量的最突出的問題之一。因此淘寶對此進行了系統性的內存治理,成立了內存專項。

“工欲善其事、必先利其器”。本文主要講述內存專項的工具之一,內存泄漏分析memunreachable

內存泄漏

內存泄漏(Memory Leak)是指程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。

對於 c/c++內存泄漏,由於存在指針要精確找到那些對象沒有被引用是非常困難的,一直是困擾 c/c++重點、難點問題之一。目前也有一些基於類似 GC Swap-Mark 的算法去找到內存泄露,常見工具如 libmemunreachable,kmemleak,llvm leaksanitizer 這類工具也需要記錄分配信息。

Android 的 libmemunreachable 是一個零開銷的本地內存泄漏檢測器。 它會使用不精確的“標記-清除”垃圾回收器遍歷所有本機內存,同時將任何不可訪問的塊報告爲泄漏。 有關使用說明,請參閱 libmemunacachable 文檔[1]。雖然 Android 提供了 libmemunreachable 如此優秀的開源 c/c++內存泄漏工具,並內嵌到 Android 的系統環境,幫忙我們去定位內存泄漏問題,但是目前 libmemunreachable 使用依賴線下的 Debug 配置環境,無法支持淘寶 Release 包。

本文結合 libmemunreachable 源碼,我們一起來欣賞 libmemunreachable 的實現原理以及淘寶對 libmemunreachable 改造用來實現對 Release 包的支持,幫助淘寶定位和排查線上的內存泄漏問題。

libmemunreachable 分析

基本原理

我們知道 JAVA GC 算法中,如果內存中的對象中,如果不在被 GcRoot 節點直接或間接持有,那麼 GC 在適當的時間會觸發垃圾回收機制,去釋放內存。那麼哪些節點可以被作爲 GC 的 Root 節點:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  2. 方法區中的類靜態屬性引用的對象;
  3. 方法區中常量引用的對象;
  4. 本地方法棧中 JNI(即一般說的 Native 方法)中引用的對象。(JVM 中判斷對象是否清理的一種方法是可達性算法.可達性算法就是採用 GC Roots 爲根節點, 採用樹狀結構,向下搜索.如果對象直接到達 GC Roots ,中間沒有任何中間節點.則判斷對象可回收. 而堆區是 GC 的重點區域,所以堆區不能作爲 GC roots。)

而 C/C++內存模型,堆 heap 、棧 stack、全局/靜態存儲區 (.bss 段和.data 段)、常量存儲區 (.rodata 段) 、代碼區 (.text 段)。libmemunreachable 通過 C/C++內存模型結合可達性算法,將棧 stack、全局/靜態存儲區 (.bss 段和.data 段)作爲 GC Root 節點,判斷堆 heap 中的內存是否被 GC Root 所持有,如果不被直接或間接持有,則被判定爲泄漏(別較真,不一定要 100%的判斷 C/C++的內存泄漏,而是可以分析可能存在的潛在泄漏)。

圖 1 C/C++內存模型可達性算法示意圖

libmemunreachable 會使用不精確的“標記-清除”垃圾回收器遍歷所有本機內存,同時將任何不可訪問的塊報告爲泄漏。

libmemunreachable 流程圖

圖 2 memunreachable 時序圖

memunreachable 時序:

  • 創建 LeakPipe:用來與子進程通信,子進程發送數據,父進程接受數據;
  • Fork 子進程:通過 fork 子進程的方式來保護當前進程的狀態;
  • CaptureThreads:通過 Ptrace 的方式使得目標進程可以被子進程 Dump,從而使得子進程獲取父進程的信息;
  • CaptureThreadInfo:通過 PTRACE_GETREGSET 獲取寄存器的信息,部分 Heap 的內存可能被寄存器持有,這些被寄存器持有的 Heap 不應該被判定爲泄漏;
  • ProcessMappings:解析/proc/self/maps 文件信息,maps 文件記錄了堆 heap 、棧 stack、全局/靜態存儲區 (.bss 段和.data 段)、常量存儲區 (.rodata 段) 、代碼區 (.text 段)等內存相關的信息;
  • ReleaseThreads:通過 Ptrace 的方式恢復目標進程的 Ptrace 狀態,並且主進程結束等待,開始接受數據;
  • 第二次 Fork 子進程:這裏又 Fork 一次子進程,我的理解可能是爲了性能,第一次 Fork 的是收集了需要分析內存泄漏的相關信息,第二次 Fork 則在收集的相關信息基礎上去分析;
  • CollectAllocations:從/proc/pid/maps 的信息中分類,將棧 stack、全局/靜態存儲區 (.bss 段和.data 段)放入 GC Root 節點,堆 heap 放入被檢查的對象;
  • GetUnreachableMemory:獲取不可達的泄漏內存,C/C++內存模型結合可達性算法開始工作,去分析可能泄漏的 Heaps;
  • PipeSend:通過 Pipe 將泄漏信息發送給主進程;
  • PipeReceiver:主進程接受泄漏數據。

核心代碼如下:

//MemUnreachable.cpp
bool GetUnreachableMemory(UnreachableMemoryInfo &info, size_t limit) {
    int parent_pid = getpid();
    int parent_tid = gettid();
    Heap heap;
    Semaphore continue_parent_sem;
    LeakPipe pipe;
    PtracerThread thread{[&]() -> int {
        /
        // Collection thread
        /
        ALOGE("collecting thread info for process %d...", parent_pid);
        ThreadCapture thread_capture(parent_pid, heap);
        allocator::vector<ThreadInfo> thread_info(heap);
        allocator::vector<Mapping> mappings(heap);
        allocator::vector<uintptr_t> refs(heap);
        // ptrace all the threads
        if (!thread_capture.CaptureThreads()) {
            LOGE("CaptureThreads failed");
        }
        // collect register contents and stacks
        if (!thread_capture.CapturedThreadInfo(thread_info)) {
            LOGE("CapturedThreadInfo failed");
        }
        // snapshot /proc/pid/maps
        if (!ProcessMappings(parent_pid, mappings)) {
            continue_parent_sem.Post();
            LOGE("ProcessMappings failed");
            return 1;
        }
        // malloc must be enabled to call fork, at_fork handlers take the same
        // locks as ScopedDisableMalloc.  All threads are paused in ptrace, so
        // memory state is still consistent.  Unfreeze the original thread so it
        // can drop the malloc locks, it will block until the collection thread
        // exits.
        thread_capture.ReleaseThread(parent_tid);
        continue_parent_sem.Post();
        // fork a process to do the heap walking
        int ret = fork();
        if (ret < 0) {
            return 1;
        } else if (ret == 0) {
            /
            // Heap walker process
            /
            // Examine memory state in the child using the data collected above and
            // the CoW snapshot of the process memory contents.
            if (!pipe.OpenSender()) {
                _exit(1);
            }
            MemUnreachable unreachable{parent_pid, heap};
            //C/C++內存模型結合可達性算法開始工作
            if (!unreachable.CollectAllocations(thread_info, mappings)) {
                _exit(2);
            }
            size_t num_allocations = unreachable.Allocations();
            size_t allocation_bytes = unreachable.AllocationBytes();
            allocator::vector<Leak> leaks{heap};
            size_t num_leaks = 0;
            size_t leak_bytes = 0;
            bool ok = unreachable.GetUnreachableMemory(leaks, limit, &num_leaks, &leak_bytes);
            ok = ok && pipe.Sender().Send(num_allocations);
            ok = ok && pipe.Sender().Send(allocation_bytes);
            ok = ok && pipe.Sender().Send(num_leaks);
            ok = ok && pipe.Sender().Send(leak_bytes);
            ok = ok && pipe.Sender().SendVector(leaks);
            if (!ok) {
                _exit(3);
            }
            _exit(0);
        } else {
            // Nothing left to do in the collection thread, return immediately,
            // releasing all the captured threads.
            ALOGI("collection thread done");
            return 0;
        }
    }};
    /
    // Original thread
    /
    {
        // Disable malloc to get a consistent view of memory
        ScopedDisableMalloc disable_malloc;
        // Start the collection thread
        thread.Start();
        // Wait for the collection thread to signal that it is ready to fork the
        // heap walker process.
        continue_parent_sem.Wait(300s);
        // Re-enable malloc so the collection thread can fork.
    }
    // Wait for the collection thread to exit
    int ret = thread.Join();
    if (ret != 0) {
        return false;
    }
    // Get a pipe from the heap walker process.  Transferring a new pipe fd
    // ensures no other forked processes can have it open, so when the heap
    // walker process dies the remote side of the pipe will close.
    if (!pipe.OpenReceiver()) {
        return false;
    }
    bool ok = true;
    ok = ok && pipe.Receiver().Receive(&info.num_allocations);
    ok = ok && pipe.Receiver().Receive(&info.allocation_bytes);
    ok = ok && pipe.Receiver().Receive(&info.num_leaks);
    ok = ok && pipe.Receiver().Receive(&info.leak_bytes);
    ok = ok && pipe.Receiver().ReceiveVector(info.leaks);
    if (!ok) {
        return false;
    }
    LOGD("unreachable memory detection done");
    LOGD("%zu bytes in %zu allocation%s unreachable out of %zu bytes in %zu allocation%s",
         info.leak_bytes, info.num_leaks, plural(info.num_leaks),
         info.allocation_bytes, info.num_allocations, plural(info.num_allocations));
    return true;
}

CaptureThreads(核心函數)

//ThreadCapture.cpp
bool ThreadCaptureImpl::CaptureThreads() {
    TidList tids{allocator_};
    bool found_new_thread;
    do {
        //從/proc/pid/task中獲取全部線程Tid
        if (!ListThreads(tids)) {
            LOGE("ListThreads failed");
            ReleaseThreads();
            return false;
        }
        found_new_thread = false;
        for (auto it = tids.begin(); it != tids.end(); it++) {
            auto captured = captured_threads_.find(*it);
            if (captured == captured_threads_.end()) {
                //通過ptrace(PTRACE_SEIZE, tid, NULL, NULL)使得線程tid可以被DUMP
                if (CaptureThread(*it) < 0) {
                    LOGE("CaptureThread(*it) failed");
                    ReleaseThreads();
                    return false;
                }
                found_new_thread = true;
            }
        }
    } while (found_new_thread);
    return true;
}

CaptureThreads 存在兩個核心核心函數

  • ListThreads:從/proc/pid/task 中獲取全部線程 Tid
  • CaptureThread:通過 ptrace(PTRACE_SEIZE, tid, NULL, NULL)使得線程 tid 可以被 DUMP

CaptureThreadInfo(核心函數)

//ThreadCaptureImpl.cpp
bool ThreadCaptureImpl::CapturedThreadInfo(ThreadInfoList &threads) {
    threads.clear();
    for (auto it = captured_threads_.begin(); it != captured_threads_.end(); it++) {
        ThreadInfo t{0, allocator::vector<uintptr_t>(allocator_),
                     std::pair<uintptr_t, uintptr_t>(0, 0)};
        //ptrace(PTRACE_GETREGSET, tid, reinterpret_cast<void *>(NT_PRSTATUS), &iovec)
        if (!PtraceThreadInfo(it->first, t)) {
            return false;
        }
        threads.push_back(t);
    }
    return true;
}

CaptureThreads 的個核心函數

  • PtraceThreadInfo:ptrace(PTRACE_GETREGSET, tid...),通過 ptrace 獲寄存器信息,部分 Heap 的內存可能被寄存器持有,這些被寄存器持有的 Heap 不應該被判定爲泄漏。

ProcessMappings(核心函數)

//ProcessMappings.cpp
bool ProcessMappings(pid_t pid, allocator::vector<Mapping> &mappings) {
    char map_buffer[1024];
    snprintf(map_buffer, sizeof(map_buffer), "/proc/%d/maps", pid);
    android::base::unique_fd fd(open(map_buffer, O_RDONLY));
    if (fd == -1) {
        LOGE("ProcessMappings parent pid failed to open %s: %s", map_buffer, strerror(errno));
        //get self pid to replace
        //Release 包有權限問題只能訪問自身進程
        snprintf(map_buffer, sizeof(map_buffer), "/proc/self/maps");
        fd.reset(open(map_buffer, O_RDONLY));
        if (fd == -1) {
            LOGE("ProcessMappings failed to open %s: %s", map_buffer, strerror(errno));
            return false;
        }
    }
    LineBuffer line_buf(fd, map_buffer, sizeof(map_buffer));
    char *line;
    size_t line_len;
    while (line_buf.GetLine(&line, &line_len)) {
        int name_pos;
        char perms[5];
        Mapping mapping{};
        if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR " %4s %*x %*x:%*x %*d %n",
                   &mapping.begin, &mapping.end, perms, &name_pos) == 3) {
            if (perms[0] == 'r') {
                mapping.read = true;
            }
            if (perms[1] == 'w') {
                mapping.write = true;
            }
            if (perms[2] == 'x') {
                mapping.execute = true;
            }
            if (perms[3] == 'p') {
                mapping.priv = true;
            }
            if ((size_t) name_pos < line_len) {
                strlcpy(mapping.name, line + name_pos, sizeof(mapping.name));
            }
            mappings.emplace_back(mapping);
        }
    }
    return true;
}
  • ProcessMappings 解析 maps 文件信息。

CollectAllocations(核心函數)

//MemUnreachable.cpp
bool MemUnreachable::ClassifyMappings(const allocator::vector<Mapping> &mappings,
                                      allocator::vector<Mapping> &heap_mappings,
                                      allocator::vector<Mapping> &anon_mappings,
                                      allocator::vector<Mapping> &globals_mappings,
                                      allocator::vector<Mapping> &stack_mappings) {
    heap_mappings.clear();
    anon_mappings.clear();
    globals_mappings.clear();
    stack_mappings.clear();
    allocator::string current_lib{allocator_};
    for (auto it = mappings.begin(); it != mappings.end(); it++) {
        if (it->execute) {
            current_lib = it->name;
            continue;
        }
        if (!it->read) {
            continue;
        }
        const allocator::string mapping_name{it->name, allocator_};
        if (mapping_name == "[anon:.bss]") {
            // named .bss section
            globals_mappings.emplace_back(*it);
        } else if (mapping_name == current_lib) {
            // .rodata or .data section
            globals_mappings.emplace_back(*it);
        } else if (has_prefix(mapping_name, "[anon:scudo:secondary]")) {
            // named malloc mapping
            heap_mappings.emplace_back(*it);
        } else if (has_prefix(mapping_name, "[anon:scudo:primary]")) {
            // named malloc mapping
            heap_mappings.emplace_back(*it);
        } else if (mapping_name == "[anon:libc_malloc]") {
            // named malloc mapping
            heap_mappings.emplace_back(*it);
        } else if (has_prefix(mapping_name, "/dev/ashmem/dalvik")
                   || has_prefix(mapping_name, "[anon:dalvik")) {
            // named dalvik heap mapping
            globals_mappings.emplace_back(*it);
        } else if (has_prefix(mapping_name, "[stack")) {
            // named stack mapping
            stack_mappings.emplace_back(*it);
        } else if (mapping_name.size() == 0 || mapping_name == "") {
            globals_mappings.emplace_back(*it);
        } else if (has_prefix(mapping_name, "[anon:stack_and_tls")) {
            stack_mappings.emplace_back(*it);
        } else if (has_prefix(mapping_name, "[anon:") &&
                   mapping_name != "[anon:leak_detector_malloc]") {
            // TODO(ccross): it would be nice to treat named anonymous mappings as
            // possible leaks, but naming something in a .bss or .data section makes
            // it impossible to distinguish them from mmaped and then named mappings.
            globals_mappings.emplace_back(*it);
        }
    }
    return true;
}
bool MemUnreachable::CollectAllocations(const allocator::vector<ThreadInfo> &threads,
                                        const allocator::vector<Mapping> &mappings) {
    ALOGI("searching process %d for allocations", pid_);
    allocator::vector<Mapping> heap_mappings{mappings};
    allocator::vector<Mapping> anon_mappings{mappings};
    allocator::vector<Mapping> globals_mappings{mappings};
    allocator::vector<Mapping> stack_mappings{mappings};
    if (!ClassifyMappings(mappings, heap_mappings, anon_mappings,
                          globals_mappings, stack_mappings)) {
        return false;
    }
    for (auto it = heap_mappings.begin(); it != heap_mappings.end(); it++) {
        HeapIterate(*it, [&](uintptr_t base, size_t size) {
            if (!heap_walker_.Allocation(base, base + size)) {
                LOGD("Allocation Failed base:%p size:%d name:%s", base, size, it->name);
            }
        });
    }
    for (auto it = anon_mappings.begin(); it != anon_mappings.end(); it++) {
        if (!heap_walker_.Allocation(it->begin, it->end)) {
            LOGD("Allocation Failed base:%p end:%d name:%s", it->begin, it->end, it->name);
        }
    }
    for (auto it = globals_mappings.begin(); it != globals_mappings.end(); it++) {
        heap_walker_.Root(it->begin, it->end);
    }
     if (threads.size() > 0) {
        for (auto thread_it = threads.begin(); thread_it != threads.end(); thread_it++) {
            for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
                if (thread_it->stack.first >= it->begin && thread_it->stack.first <= it->end) {
                    heap_walker_.Root(thread_it->stack.first, it->end);
                }
            }
            //寫入寄存器的信息,作爲根節點
            heap_walker_.Root(thread_it->regs);
        }
    } else {
        //由於獲取寄存器信息失敗,採取降級邏輯
        for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
            heap_walker_.Root(it->begin, it->end);
        }
    }
    
     if (threads.size() > 0) {
        for (auto thread_it = threads.begin(); thread_it != threads.end(); thread_it++) {
            for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
                if (thread_it->stack.first >= it->begin && thread_it->stack.first <= it->end) {
                    heap_walker_.Root(thread_it->stack.first, it->end);
                }
            }
            //寫入寄存器的信息,作爲根節點
            heap_walker_.Root(thread_it->regs);
        }
    } else {
        //由於獲取寄存器信息失敗,採取降級邏輯
        for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
            heap_walker_.Root(it->begin, it->end);
        }
    }
    ALOGI("searching done");
    return true;
}

CollectAllocations 將 maps 分四個模塊,分別是 1.heap_mappings 存放堆信息,stack_mappings 存放線程棧信息(GC Root),globals_mappings 存放.bss .data 信息(GC Root),anon_mappings 其他可讀的內存信息(GC Root,這些也會作爲 GC Root 防止有泄漏誤報):

  • ClassifyMappings:將 maps 信息存放到目標模塊中;
  • HeapIterate:遍歷有效內存分佈;Android 內存分配算法,在申請的過程中會通過 mmap 申請一塊塊的大內存,最後通過內存分配器進行內存管理,Android 11 以上使用了scudo 內存分配[2](Android 11 以下使用的是jemalloc 內存分配器[3]),無論是那種分配器,Android 都提供了遍歷有效內存的便利函數 malloc_iterate 這使得我們獲取有效內存變得容易很多。相關內容可以看malloc_debug[4]。

GetUnreachableMemory(核心函數)

//HeapWalker.cpp
void HeapWalker::RecurseRoot(const Range &root) {
    allocator::vector<Range> to_do(1, root, allocator_);
    while (!to_do.empty()) {
        Range range = to_do.back();
        to_do.pop_back();
        //將GC Root的節點的一個塊內存作爲指針,去遍歷,直到隊列爲空
        ForEachPtrInRange(range, [&](Range &ref_range, AllocationInfo *ref_info) {
            if (!ref_info->referenced_from_root) {
                ref_info->referenced_from_root = true;
                to_do.push_back(ref_range);
            }
        });
    }
}
bool HeapWalker::DetectLeaks() {
    // Recursively walk pointers from roots to mark referenced allocations
    for (auto it = roots_.begin(); it != roots_.end(); it++) {
        RecurseRoot(*it);
    }
    Range vals;
    vals.begin = reinterpret_cast<uintptr_t>(root_vals_.data());
    vals.end = vals.begin + root_vals_.size() * sizeof(uintptr_t);
    RecurseRoot(vals);
    return true;
}
bool MemUnreachable::GetUnreachableMemory(allocator::vector<Leak> &leaks,
                                          size_t limit, size_t *num_leaks, size_t *leak_bytes) {
    ALOGI("sweeping process %d for unreachable memory", pid_);
    leaks.clear();
    if (!heap_walker_.DetectLeaks()) {
        return false;
    }
    //數據統計
    ...
    return true;
}

核心函數 DetectLeaks

  • ForEachPtrInRange:將 GC Root 的節點的一個塊內存作爲指針,去遍歷,直到隊列爲空
  • DetectLeaks:遍歷 GC Root 節點,將能訪問到的 Heap 對象標記;
  • 數據統計:沒有遍歷到的 Heap 對象設置爲泄漏。

淘寶 Release 包改進

Android 10 之後系統收回了進程私有文件的權限,如 /proc/pid/maps,/proc/pid/task 等,fork 出來的子進程無法獲取父進程目錄下的文件,否則會拋“Operation not permitted”的異常。因此當我們通過 dlsym 的方式去調用系統 libmemunreachable.so 庫的時候在 Release 包的時候會拋“Failed to get unreachable memory if you are trying to get unreachable memory from a system app (like com.android.systemui), disable selinux first using setenforce 0”(當然我們無法去設置用戶的系統環境)。

針對這問題,淘寶選擇重新編譯了 libmemunreachable 庫,並且修改了相關所需權限的配置,如/proc/pid/maps 的獲取不在獲取父進程(目標進程)的 maps(沒有權限),而獲取/proc/self/maps,因爲子進程保留了父進程的內存信息,這與獲取/proc/pid/maps 的效果是一致。

Ptrace 失敗的修復:google unreachable 在 debug 包可以,在 release 包裏不能運行,原因是 PR_GET_DUMPABLE 在 debug 的時候默認是 1,直接可以 ATTACH,而在 release 的默認是 0,不可以 attach,導致 release 跑 unreach 不正常工作 (google 太壞了),修復方案:設置下 prctl(PR_SET_DUMPABLE, 1);

其他改造:

  1. 工程化的改造,打通 TBRest,使得線上的泄漏數據上報到 EMAS;
  2. 非核心權限繞開,如/proc/pid/task 獲取線程寄存器信息,如果獲取失敗不終止流程(雖然線程寄存器有可能會指向內存,並且這個內存不被.bss .data 和 stack 等持有,導致誤判,但是這樣的場景不多)。

可能的誤報場景

base+offset 的場景特定的內存分析會失敗。比如他申請的內存是 A,但是堆棧和 Global 是通過 Base+offset=A 這種方法來引用的 ,就會誤判,因爲 Base 和 offset 在堆和.bss 裏,但是堆和.bss 沒有 A ,就判斷 A 泄漏了 就誤報了。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章