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

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

编码文章call10242025-05-04 13:32:112A+A-
  • 问题概述

几天前,收到一个同事的求助: 在做了新的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_info(&leak_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_info(info->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<android_mallopt_leak_info_t*>(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<get_malloc_leak_info_func_t>(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更详细的信息我们后面专门写篇文章进行解析.有兴趣的同学可以持续关注. 背后的工作原理需要有链接相关的知识做背景, 缺少的同学可以建议看看<程序员的自我修养>以及<linkers and loaders>

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4