百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Android bionic自带内存检查工具排查一次内存泄漏及原理源码解析

csdh11 2025-03-14 15:56 1 浏览

  • 问题概述

几天前,收到一个同事的求助: 在做了新的wifi模组匹配后,在做Miracast投屏煲机时,煲机1.5小时左右会退出Miracast.

该同事反馈他们做过相同的对比试验"使用原来模组不会存在该问题". 可能由于他们所说的上述对比试验的错误结论误导了他们导致很久没有查出问题.

  • 问题排查过程以及方法

看到问题后感觉很好,毕竟是问题有必现的路径, 并且时间很短,像内存泄漏这种问题,在debug问题时只要找到必现路径,一般不需要进行煲机测试debug的,方法得当的情况下可以很快找到问题点.

第一步: 由于我们的debug系统本身是会抓取一些log的,从log中分析得出两点结论: a) free命令抓到的内存越来越少, b) 会看到大量的lowmemkiller,特别是在出问题的时候,Miracast是被lowmemkiller给杀死的,说明系统很缺内存,并且已经将前台进程杀死.

第二步: 由于我这边没有平台,所以只能远程请该同事帮忙debug, 确定是内存泄漏后,请该同事使用procrank命令每隔30S钟, 抓取了几分钟后发现mediaserver进程一直在增长, 随后为再次确定使用如下脚本过滤mediaserver:

#!/bin/sh
while [[ 1 ]]; do
        procrank |grep mediaserver
        sleep 30
done

抓取到的数据如下: 从抓取到的数据看Vss/Rss/Pss都在增长,所以基本可以确定是mediaserver产生了内存泄漏.

看到上述信息后,想会不会是之前帮另一个同事查问题时解的patch没有同步过去,于是请该同事使用如下脚本抓取了几分钟,排查是否不停的在创建线程而没有销毁, 然而抓取几分钟后, current thread count is: 40 始终不会有大的改变,所以基本可以排除该问题点.

#! /bin/sh
echo "please input your progressname:";
read progressname;
echo "progressname is: " $progressname;
while [[ 1 ]]; do
  progresscount=$(busybox pidof ${progressname});
  #echo ${progresscount}
  counts=0;
  for progresspid in ${progresscount};
  do
    #statements
    #echo $progresspid;
    count=$(ps -A -T -p $progresspid |busybox wc -l);
    counts=$((counts+count));
  done
  echo "current thread count is: "$counts
  sleep 1
done

第三步: 确定是mediaserver单纯的内存泄漏后,让我想到了android bionic库自带的malloc_debug工具以及网络上有人写好的heapsnap(源代码下载路径:
https://github.com/albuer/heapsnap), 接下来先不解释原理先直接看套路:

下载heapsnap 进行源码编译, 编译出 libheapsnap.so

把libheapsnap.so 推送到/system/lib 下

开启malloc debug 调试:

setprop libc.debug.malloc.options backtracesetprop libc.debug.malloc.program mediaserver

配置环境变量: export LD_PRELOAD=system/lib/libheapsnap.so

命令行输入停止: stop mediaserver

命令行输入启动mediaserver: mediaserver &

通过串口命令输入: kill -21 $(pidof mediaserver) 触发一次backtrace 到/data/local/tmp/heap_snap/ 目录下, 生成文件heap_${PID}_0000.txt

过一段时间后再在命令行输入上一步: kill -21 $(pidof mediaserver) 触发一次backtrace 到/data/local/tmp/heap_snap/ 目录下, 生成文件heap_${PID}_0001.txt

对比前后两次抓到的数据, 发现第二次抓取的多了好几笔这样的backtrace,至此问题点可以确认,根据symbol找到对应的点修复即可.

以上就是解决问题的套路.大家遇到问题后可以按照上述步骤解决native进程泄漏内存的问题点.

  • 原理以及源代码解析

看完了套路我们来一步一步解析这些套路当中的原理,只有深入理解了原理才能以不变应万变.

  1. 我们先来看看套路libheapsnap.so的关键源代码libheapsnap.cpp:
//libheapsnap.cpp 
#define DEFAULT_HEAPSNAP_SIG        SIGTTIN
extern "C" void heapsnap_init() {
    //这个pid实际上在本例当中就会是对应额mediaserver的pid
    myPid = getpid();
    dbg_log("PID(%d): register snapshot SIG: %d\n", myPid, DEFAULT_HEAPSNAP_SIG);
    //注册信号量SIGTIN的捕获函数heapsnap_signal_handler 
    signal(DEFAULT_HEAPSNAP_SIG, &heapsnap_signal_handler);
    info_log("PID(%d): Heap Snap enabled\n", myPid);
}
//SIGTTIN 的捕获函数
static void heapsnap_signal_handler(int sig)
{
    dbg_log("PID(%d): catch SIG: %d\n", myPid, sig);
    switch (sig) {
    case DEFAULT_HEAPSNAP_SIG: {
        heapsnap_save();//实际做事情的
        break;
     ...
}
//真正捕获SIGTTIN 的函数, 看看它做了什么
extern "C" void heapsnap_save(void){
    hs_malloc_leak_info_t leak_info;
    FILE *fp = heapsnap_getfile();//为路径/data/local/tmp/heap_snap
    if (fp == NULL)
        return;
    if (!get_malloc_info(&leak_info)) {//调用malloc_debug API
        fprintf(fp, "Native heap dump not available. To enable, run these"
                    " commands (requires root):\n");
        fprintf(fp, "# adb shell stop\n");
#if (PLATFORM_SDK_VERSION<24)
        fprintf(fp, "# adb shell setprop libc.debug.malloc 1\n");
#else
        fprintf(fp, "# adb shell setprop libc.debug.malloc.options backtrace\n");
#endif
        fprintf(fp, "# adb shell start\n");
        fclose(fp);
        return;
    }
    //存储从malloc_debug 里面dump到d额leak_info的信息,保存到对应文件.
    demangle_and_save(&leak_info, fp);
    //调用malloc_debug 提供的API释放.
    free_malloc_info(&leak_info);
    fclose(fp);


    info_log("PID(%d): Heap Save Done.\n", myPid);
}
// 下面两个函数都调用了android_mallopt的api来进行处理,
//  留意: 不同的android版本malloc_debug 提供的api不同
// 所以我们更需要解读malloc_debug的源代码以及弄清楚原理.
static bool get_malloc_info(hs_malloc_leak_info_t* leak_info)
{
#if (PLATFORM_SDK_VERSION<29) get_malloc_leak_infoleak_info->buffer, &leak_info->overall_size, &leak_info->info_size,
            &leak_info->total_memory, &leak_info->backtrace_size);
#else
    if (!android_mallopt(M_GET_MALLOC_LEAK_INFO, leak_info, sizeof(*leak_info))) {
      return false;
    }
#endif
   ....
    return true;
}


static void free_malloc_info(hs_malloc_leak_info_t* info)
{
#if (PLATFORM_SDK_VERSION<29) free_malloc_leak_infoinfo->buffer);
#else
    android_mallopt(M_FREE_MALLOC_LEAK_INFO, info, sizeof(*info));
#endif
}
//这个prepare的 __attribute__((constructor)) 表示它会在进程的main函数之前被调用
// 关于它的原理我们稍后分析bionic 源码部分时会进行说明,
extern "C" void __attribute__((constructor)) prepare()
{
    dbg_log("prepare heapsnap\n");
    heapsnap_init();
}

总结一下第一步的含义: 就是在需要debug的进程执行之前(实际上就是该进程启动的linker阶段,稍后会进行源码剖析),会先给该进程注册一个信号量的捕获函数,捕获函数通过bionic提供的api调用malloc_debug来进行内存信息的统计比较.

2. "把libheapsnap.so 推送到/system/lib 下"不需要做过多解释,就是为了linker时能link到该lib.

3. 设置的两个属性: "setprop libc.debug.malloc.options backtrace"与"setprop libc.debug.malloc.program mediaserver"针对mediaserver设置了backtrace的hooks而已, 当mediaserver触发时就会打印backtrace

//android/bionic/libc/bionic/malloc_common_dynamic.cpp
// 该函数同样会在第四步解释当中的constructor中会被一步一步调用到.
static void MallocInitImpl(libc_globals* globals) {
  ...
  /* malloc的hooks, 为malloc_debug的实现方式.
  * CheckLoadMallocDebug首先会check 环境变量"LIBC_DEBUG_MALLOC_OPTIONS" 
  * 如果没有会获取上面两个属性,对应额options 以及特定进程名 
  */
  if (CheckLoadMallocDebug(&options)) {
    // 安装对应的hook函数,这样在调用对应函数时会先call 对应d额hook函数.例如内存释放的mallocfree等
    hook_installed = InstallHooks(globals, options, kDebugPrefix, kDebugSharedLib);
  } else if (CheckLoadMallocHooks(&options)) {
    hook_installed = InstallHooks(globals, options, kHooksPrefix, kHooksSharedLib);
  }
  ...
}

4. " export LD_PRELOAD=system/lib/libheapsnap.so": 这个的作用是将libheapsnap.so在bionic在做linker时加入到needed_library_name_list当中,然后找到它并进行加载并且会call到我们libheapsnap.so当中的__attribute__((constructor)) 下面为相关bionic linker时的相关sourcecode:

//android/bionic/linker/linker_main.cpp
static ElfW(Addr) linker_main(KernelArgumentBlock& args, const char* exe_to_load) 
{
   ...
   //获取环境变量LD_PRELOAD的值给ldpreload_env
   ldpreload_env = getenv("LD_PRELOAD");
   if (ldpreload_env != nullptr) {
      INFO("[ LD_PRELOAD set to \"%s\" ]", ldpreload_env);
   }
   ...
   //解析ldpreload_env以:作为分隔符,复制给g_ld_preload_names
   parse_LD_PRELOAD(ldpreload_env);
   ...
   //将获取到的分割值加入到needed_library_name_list列表当中
   for (const auto& ld_preload_name : g_ld_preload_names) {
    needed_library_name_list.push_back(ld_preload_name.c_str());
    ++ld_preloads_count;
  }
  ...
  //讲对应的lib进行加载
  if (needed_libraries_count > 0 &&!find_libraries(&g_default_namespace,
                      si,needed_library_names,needed_libraries_count,
                      nullptr,&g_ld_preloads,ld_preloads_count,RTLD_GLOBAL,
                      nullptr,true /* add_as_children */,true /* search_linked_namespaces */,
                      &namespaces)) {
    __linker_cannot_link(g_argv[0]);
    }
    ...
    //你锁牵挂的libheapsnap.so 的void __attribute__((constructor)) prepare();被调用.
    //libheapsnap.so初始化.
    si->call_pre_init_constructors();
    si->call_constructors();
}

5: "命令行输入停止: stop mediaserver": 停止mediaserver,目的是为了再次启动加载上第3和第4步的资讯.

6. "命令行输入启动mediaserver: mediaserver &", 再次启动meidaserver并且触发了第3和第4步

7. 后面的两个步骤就会来触发malloc_debug的backtrace,该backtrace会使用前面第3步注册的hooks做malloc/free 等统计,会将调用这些函数的堆栈信息记录下来. 当通过接口android_mallopt(M_GET_MALLOC_LEAK_INFO, leak_info, sizeof(*leak_info))获取信息, bionic源代码如下:

//android/bionic/libc/bionic/malloc_common_dynamic.cpp
__BIONIC_WEAK_FOR_NATIVE_BRIDGE
extern "C" bool android_mallopt(int opcode, void* arg, size_t arg_size) {
{
   ...
   //处理libheapsnap.so 对应的处理方法
   if (opcode == M_GET_MALLOC_LEAK_INFO) {
    if (arg == nullptr || arg_size != sizeof(android_mallopt_leak_info_t)) {
      errno = EINVAL;
      return false;
    }
    //malloc_debug中对应额处理函数
    return GetMallocLeakInfo(reinterpret_cast(arg));
  }
}
//找到对一个的function, 那么这个func 会是谁呢? 关键就是看gFunctions是什么
bool GetMallocLeakInfo(android_mallopt_leak_info_t* leak_info) {
  void* func = gFunctions[FUNC_GET_MALLOC_LEAK_INFO];
  if (func == nullptr) {
    errno = ENOTSUP;
    return false;
  }
  reinterpret_cast(func)(
      &leak_info->buffer, &leak_info->overall_size, &leak_info->info_size,
      &leak_info->total_memory, &leak_info->backtrace_size);
  return true;
}
bool InitSharedLibrary(void* impl_handle, const char* shared_lib, const char* prefix, MallocDispatch* dispatch_table) {
  static constexpr const char* names[] = {
    "initialize",
    "finalize",
    "get_malloc_leak_info",
    "free_malloc_leak_info",
    "malloc_backtrace",
    "write_malloc_leak_info",
  };
  //从对应的ib当中解析,一个一个找到上面额几个对应函数赋值过去的
  for (size_t i = 0; i < FUNC_LAST; i++) {
    char symbol[128];
    snprintf(symbol, sizeof(symbol), "%s_%s", prefix, names[i]);
    gFunctions[i] = dlsym(impl_handle, symbol);
    if (gFunctions[i] == nullptr) {
      error_log("%s: %s routine not found in %s", getprogname(), symbol, shared_lib);
      ClearGlobalFunctions();
      return false;
    }
  }
  ...
}

继续我们的追踪发现是我们在第3步中InstallHooks函数中传过去的

//android/bionic/libc/bionic/malloc_common_daynamic.cpp
static constexpr char kDebugSharedLib[] = "libc_malloc_debug.so";
static constexpr char kDebugPrefix[] = "debug";


 static bool InstallHooks(libc_globals* globals, const char* options, const char* prefix,
                         const char* shared_lib) {
   /* 由于前面属性的设置,走到的函数最后一个参数会是libc_malloc_debug.so
   * prefix 参数为"debug"
   void* impl_handle = LoadSharedLibrary(shared_lib, prefix, &globals->malloc_dispatch_table);
   ...
}

上面解析完后,实质上就是在找libc_malloc_debug.so 当中的symbol为
debug_get_malloc_leak_info函数

//android/bionic/libc/malloc_debug/malloc_debug.cpp
 void debug_get_malloc_leak_info(uint8_t** info, size_t* overall_size, size_t* info_size,
                                size_t* total_memory, size_t* backtrace_size) {
   ....//一堆各种检查
   PointerData::GetInfo(info, overall_size, info_size, total_memory, backtrace_size);
}
//android/bionic/libc/malloc_debug/PointerData.cpp


void PointerData::GetInfo(uint8_t** info, size_t* overall_size, size_t* info_size,
                          size_t* total_memory, size_t* backtrace_size) {
  //获取标记后的栈帧信息给对应.
  ...
} 

libheapsnap.so 拿到后把结果输出到对应文件.

  • 总结:

以上就是这些套路背后的工作原理, 关于malloc_debug更详细的信息我们后面专门写篇文章进行解析.有兴趣的同学可以持续关注. 背后的工作原理需要有链接相关的知识做背景, 缺少的同学可以建议看看<程序员的自我修养>以及

最后欢迎关注我的个人微信公众号:

相关推荐

PromptDA:4K分辨率精准深度估计!(分辨率4k是多少p)

这里是FoxFeed,一个专注于科技的内容平台。背景介绍在计算机视觉领域,深度估计一直是一个重要的研究方向。近日,由DepthAnything团队开发的...

m4a怎么转换成mp3?教你这样转换音频格式

m4a怎么转换成mp3?M4A是MPEG-4音频标准的文件的扩展名,它可以存储各种类型的音频内容,运用比较广泛,尽管m4a被很多媒体应用兼容,但仍有很多应用无法打开它,将m4a转换成mp3就是一个很不...

“讲述初心故事 传递使命情怀”2019第五届江苏医院微电影节启动

“讲述初心故事传递使命情怀”,2019第五届江苏医院微电影节9月16日启动。江苏医院微电影节由新华网江苏有限公司和江苏省医院协会联合举办,扬子江药业集团协办,秉承“讲述初心故事传递使命情怀”为活动...

短视频宝贝=慢?阿里巴巴工程师这样秒开短视频

前言随着短视频兴起,各大APP中短视频随处可见,feeds流、详情页等等。怎样让用户有一个好的视频观看体验显得越来越重要了。大部分feeds里面滑动观看视频的时候,有明显的等待感,体验不是很好。针对这...

阿里巴巴工程师这样秒开短视频(阿里巴巴的工程师多少钱一个月)

前言随着短视频兴起,各大APP中短视频随处可见,feeds流、详情页等等。怎样让用户有一个好的视频观看体验显得越来越重要了。大部分feeds里面滑动观看视频的时候,有明显的等待感,体验不是很好。针对这...

旗鱼浏览器1.0 RC正式版候选版:增账户同步等

从9月19日发布第一个Beta版至今,约80天的时间便这么飞走了,作为2015年底的一个答卷,今天旗鱼浏览器1.0RC(正式版候选版)发布,如果没有意外,明天我们将发布电脑版和安卓版的第一个1.0正...

5种方法,教你将m3u8转换为mp4格式

m3u8格式在许播放器中不受支持,只能在浏览器中进行在线观看,然而,在线观看可能会不大方便,如果网络卡顿的话就会影响观感。想要将...

kgma格式怎么转换为mp3?试试这5种简单的音频转换方法!

由于kgma格式的特殊性和平台限制,除了专属的音乐平台外,其他设备和网络平台是无法识别或播放kgma格式的音乐的,因此为了方便使用,我们就必须将kgma格式转换为mp3。接下来,小编就为大家推荐5种简...

500+本程序员值得看的书籍,7大类,1大合集,收藏,日后有用

一、Golang书籍推荐入门《Go入门指南》...

教你编写最简单的CM3操作系统,160行实现任务创建与切换

如题,任务创建与上下文切换是跟硬件息息相关的,而这恰恰是RTOS编写的最难点,抛开这些功能,剩下的就是双向链表增删改操作了,本例用最精简的方式实现了任务创建与切换,OS启动等功能,并运用了Cortex...

Hot 3D 人体姿态估计 HPE Demo复现过程

视频讲解...

各编程语言相互调用示例,代码简单,生成的软件体积也很小

aardio支持混入很多不同的编程语言,代码简单,生成的软件体积也很小。下面看示例。...

你知道shell脚本中$0 $1 $# $@ $* $? $$ 都是什么意思吗?

一、概述shell中有两类字符:普通字符、元字符。1.普通字符...

NDK打印调用堆栈(logger.error打印堆栈信息)

虽然android源码里有android::CallStack用来打印堆栈,但是NDK里面并没有包含它,所以不能直接调用它,所以要尝试用动态调用的方式来实现。我测试的手机是安卓8.1.0版本,...

小白都能看得懂的Cgo入门教程(cgo2.0教程)

在Go语言开发过程中,尽管Go本身功能强大,但仍然有许多C语言库可以复用,如操作系统API、高性能计算库、数据库驱动等。Go提供了一种强大的机制——Cgo,让我们可以在Go代码中调用C...