从KOOM看native内存泄漏检测

240 阅读3分钟

对于Java而言,语言本身有自动垃圾回收机制,所以导致在内存泄漏检测上需要确定对象应该被回收的特征,以便进行检查,如LeakCanary,Matrix中监听生命周期,KOOM中检查finished或者destroyed的Activity,究其根本,主要还是要对对象应该被回收的状态进行定义。不同于Java,在Native层主要使用C或者C++来进行编码,语言本身并没有垃圾回收机制,对于对象的回收依赖于开发者手动释放空间,这也就意味着在native层进行泄漏检测相对而言更加困难,那么Native层内存泄漏就没办法监控了吗?

当然有的,在Android N(7.0)以后系统新增了libmemunreachable模块,该模块是一个零开销的本地内存泄漏检测器,其使用不精确的标记-清楚垃圾回收器遍历所有Native内存,并将不可访问的内存块报告为泄漏内存区域。基于libmemunreachable我们可以设计一套机制用于监控Native层内存泄漏问题,主要原理如下:

  1. hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据「大小、堆栈、地址等」
  2. 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」
  3. 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据「不可达内存块地址、大小、分配堆栈等」

Native层内存泄漏对象 = 不可达的内存块信息

接下来我们来看下koom-native-leak模块的实现,与koom-java-leak模块的实现类似,该模块也是通过XXXMonitor对象来启动监控的,我们可以通过LeakMonitor.INSTANCE.start();启动监控,通过MonitorManager.addMonitorConfig(config)为其添加通用配置(配置参数可以查看github上的说明)。

LeakMonitor.INSTANCE.start()

image-20230902175202437

通过start代码可以看到,最终真正执行的函数是nativeInstallMonitor和nativeGetLeakAllocs。

nativeInstallMonitor

image-20230903113706778

如上代码所示,install阶段的主要功能是hook malloc/free 等内存分配器方法。

nativeGetLeakAllocs

image-20230903114408034

memory_analyzer_->CollectUnreachableMem
 namespace kwai {
 namespace leak_monitor {
 static const char *kLibMemUnreachableName = "libmemunreachable.so";
   
 // GetUnreachableMemory函数名在libmemunreachable.so中在不同Android版本上的标记
 // Just need the symbol in arm64-v8a so
 // API level > Android O
 static const char *kGetUnreachableMemoryStringSymbolAboveO =
     "_ZN7android26GetUnreachableMemoryStringEbm";
 // API level <= Android O
 static const char *kGetUnreachableMemoryStringSymbolBelowO =
     "_Z26GetUnreachableMemoryStringbm";
 ​
 MemoryAnalyzer::MemoryAnalyzer()
     : get_unreachable_fn_(nullptr), handle_(nullptr) {
   auto handle = kwai::linker::DlFcn::dlopen(kLibMemUnreachableName, RTLD_NOW);
   if (!handle) {
     ALOGE("dlopen %s error: %s", kLibMemUnreachableName, dlerror());
     return;
   }
 ​
   // 通过kwai-linker去调用libmemunreachable.so的GetUnreachableMemory函数(分Android版本)
   if (android_get_device_api_level() > __ANDROID_API_O__) {
     get_unreachable_fn_ =
         reinterpret_cast<GetUnreachableFn>(kwai::linker::DlFcn::dlsym(
             handle, kGetUnreachableMemoryStringSymbolAboveO));
   } else {
     get_unreachable_fn_ =
         reinterpret_cast<GetUnreachableFn>(kwai::linker::DlFcn::dlsym(
             handle, kGetUnreachableMemoryStringSymbolBelowO));
   }
 }
 ​
 MemoryAnalyzer::~MemoryAnalyzer() {
   if (handle_) {
     kwai::linker::DlFcn::dlclose(handle_);
   }
 }
 ​
 bool MemoryAnalyzer::IsValid() { return get_unreachable_fn_ != nullptr; }
 ​
 std::vector<std::pair<uintptr_t, size_t>>
 MemoryAnalyzer::CollectUnreachableMem() {
   std::vector<std::pair<uintptr_t, size_t>> unreachable_mem;
 ​
   if (!IsValid()) {
     ALOGE("MemoryAnalyzer NOT valid");
     return std::move(unreachable_mem);
   }
 ​
   // libmemunreachable NOT work in release apk because it using ptrace
   if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) {
     ALOGE("Set process dumpable Fail");
     return std::move(unreachable_mem);
   }
 ​
   // Note: time consuming
   std::string unreachable_memory = get_unreachable_fn_(false, 1024);
 ​
   // Unset "dumpable" for security
   prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
 ​
   // 正则表达式匹配输出内容
   std::regex filter_regex("[0-9]+ bytes unreachable at [A-Za-z0-9]+");
   std::sregex_iterator unreachable_begin(
       unreachable_memory.begin(), unreachable_memory.end(), filter_regex);
   std::sregex_iterator unreachable_end;
   for (; unreachable_begin != unreachable_end; ++unreachable_begin) {
     const auto& line = unreachable_begin->str();
     auto address =
         std::stoul(line.substr(line.find_last_of(' ') + 1,
                                line.length() - line.find_last_of(' ') - 1),
                    0, 16);
     auto size = std::stoul(line.substr(0, line.find_first_of(' ')));
     unreachable_mem.emplace_back(address, size);
   }
   return std::move(unreachable_mem);
 }
 }  // namespace leak_monitor
 }  // namespace kwai

通过libmemunreachable.so的GetUnreachableMemory函数获取native内存情况。

Is_leak
 auto is_leak = [&](decltype(unreachable_allocs)::value_type &unreachable,
                    std::shared_ptr<AllocRecord> &live) -> bool {
   auto live_start = CONFUSE(live->address);
   auto live_end = live_start + live->size;
   auto unreachable_start = unreachable.first;
   auto unreachable_end = unreachable_start + unreachable.second;
   return live_start == unreachable_start ||
          live_start >= unreachable_start && live_end <= unreachable_end;
 };

参考链接

libmemunreachable