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

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

yund56 2025-03-29 17:48 3 浏览

  • 问题概述

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

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

相关推荐

仍需打磨:首款Windows 10X模拟器上手

今天,微软发布了适用于Windows10X的首款模拟器,以便于开发人员初步了解适用于双屏设备的操作系统调整。微软希望在SurfaceNeo今年年底正式发售之前,让开发人员对应用程序进行优化。因此...

Windows10 编译OpenCV4.5源码

在OpenCV4.5+VisualStudio2017开发环境配置中,介绍了OpenCV4.5的下载和安装,待扩展内容OpenCV源码编译,在本文中做补充。研究源码无疑是学习OpenCV的一...

微软7年磨一剑,Windows 10X抢先上手体验

2月22日消息,微软在去年10月正式推出了Windows10X系统,该系统除了可用于传统的电脑外,同样适用于双屏设备或者折叠屏设备,拥有更好的触控操作体验。Windows10X在操作系统底层、命令...

Office重新设计了图标,你觉得如何?

微软重新设计了Office的应用图标,在接下来的几个月里,这些图标将从移动端和网页端开始陆续推广至各大平台。距离Office图标的最近一次更新还是在2013年,那时鲍尔默时代的产物,那时微软还在纠结是...

微软发布 Win10 Build 21376 内测版:重新设计默认用户界面字体

IT之家5月7日消息今年早些时候,微软意外地确认正在为Windows10进行UI改进,并在预览版中发现了相关的非活动代码。微软今天宣布向开发渠道中的内测用户发布Windows1...

前端开发需要了解常用7种JavaScript设计模式

作者|Deven译者|王强策划|小智转发链接:https://mp.weixin.qq.com/s/Lw4D7bfUSw_kPoJMD6W8gg前言JavaScript中的设计模式指的是...

「Qt入门第二篇」基础(二)编写Qt多窗口程序

导语程序要实现的功能是:程序开始出现一个对话框,按下按钮后便能进入主窗口,如果直接关闭这个对话框,便不能进入主窗口,整个程序也将退出。当进入主窗口后,我们按下按钮,会弹出一个对话框,无论如何关闭这个对...

在吴中 ,哪里有学网页设计的培训班?

网页设计介绍Web2.0标准布局之网页长期签约就业班(全日制)课程收费:7680元课程周期:5-6个月(45分钟/课)使用教材:《教师自编教材》考核发证:Adobe《网页设计师》培训内容第一部份:...

Qt快速入门(工程的创建、UI界面布局、多线程、项目)

本文档将介绍QT工程的创建、UI界面布局,并以计数器为例了解QT中多线程的用法,最终完成一个基础的QT项目。1创建QT工程文件在安装好QT之后,能够在其安装组件中找到QtCreator,点击设置项...

应用崩溃有救啦!Windows新更新将解决应用崩溃问题

【CNMO新闻】对于不少上班族来说,当自己的电脑在运行某个应用程序时,突然出现应用程序崩溃问题,常常会让人十分苦恼。尤其是对于设计师或者编辑,当自己的作品未能及时保存应用崩溃全部消失的时候,简直就是痛...

Python Qt GUI设计:窗口布局管理方法【强化】(基础篇—6)

在PythonQtGUI设计:窗口布局管理方法【基础篇】(基础篇—5)文章中,聊到了如何使用QtDesigner进行窗口布局管理,其实在QtDesigner中可以非常方便进行窗口布局管理设计,...

思考:如何设计游戏业务框架

虽然现在连主机游戏都纷纷加入了网战部分,不过其身份主要充当状态同步,矛盾点集中在同步即时性上。以大量数值逻辑为主的业务功能侧重点则不同。如果说写代码就是用状态的操作给问题建模,那么编程范式和设计模式种...

用.NET设计一个假装黑客的屏幕保护程序

本文主要介绍屏幕保护程序的一些相关知识,以及其在安全方面的用途,同时介绍了如何使用.NET开发一款屏幕保护程序,并对核心功能做了介绍,案例代码开源:https://github.com/sangy...

光的艺术:灯具创意设计

本文转自|艺术与设计微信号|artdesign_org_cn“光”是文明的起源,是思维的开端,同样也是人类睁眼的开始。每个人在出生一刻,便接受了光的照耀和洗礼。远古时候,人们将光奉为神明,用火来...

Python Qt GUI设计:将UI文件转换Python文件三种妙招(基础篇—2)

在开始本文之前提醒各位朋友,Python记得安装PyQt5库文件,Python语言功能很强,但是Python自带的GUI开发库Tkinter功能很弱,难以开发出专业的GUI。好在Python语言的开放...