Unknown 内存拆解方案

修订记录:

修改时间修改人修改说明
2023.03.16徐天齐方案设计文档
2023.03.23徐天齐新增执行参数,默认不生效
2023.12.3徐天齐针对匿名映射,增加mapping owner信息

一、背景

查看进程占用内存信息可以通过dumpsys meminfo [process name]命令获取,通过读取进程的smaps节点获取(图形内存通过memtrack库获取),具体可见Android meminfo 字段详解 。在发现进程占用内存异常时,通过分析meminfo提供的信息可以初步定位异常内存占用。但是若meminfo中Unknown部分占比较大时,会干扰对进程异常占用内存进行判断和追踪,所以对Unknown内存进行拆解是很有必要的。

案例:

 adb shell dumpsys meminfo system_server​
Applications Memory Usage (in Kilobytes):​
Uptime: 82747319 Realtime: 255832629​
                   Pss  Private  Private  SwapPss      Rss     Heap     Heap     Heap​
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free​
                ------   ------   ------   ------   ------   ------   ------   ------​
  Native Heap    49550    46236     3288    72186    50316        0        0        0​
  Dalvik Heap   119087    69404    49644     6804   119784        0        0        0​
 Dalvik Other    14273     6804     4788     1320    17424
        Stack     3508     2780      728     4348     3516
       Ashmem       20        0        0        0       60
      Gfx dev    10548    10420      128        0    10552
    Other dev       64       16       40        0      496
     .so mmap    28285     1712    16096    12512   103324
    .jar mmap    31737        0     5404        0   126696
    .apk mmap    16098        0     8320       44    31632
    .ttf mmap     8518        0     5512        0    32104
    .dex mmap    61707       80    13504       96   160892
    .oat mmap      986        0       36        0    34276
    .art mmap    10204     5120     4676     1032    18424
   Other mmap     1154        8     1120        0     1784
   EGL mtrack     5100     5100        0        0     5100
    GL mtrack      704      704        0        0      704
      Unknown   350918   349760     1148   317153   351092
        TOTAL  1127956   498144   114432   415495  1068176        0        0        0​

在smaps或maps中,匿名映射的vma可以通过prctl方法主动添加名字字段,否则name字段默认为空。当前meminfo中对内存的统计,匿名映射除了下面这几个字段前缀的可以被统计以外,其他设置名字的匿名映射和没有名字的vma则被统计到Unknown字段中。

"[anon:dalvik-", "[anon:stack_and_tls:", "[anon:libc_malloc]",
"[anon:scudo:", "[anon:GWP-ASan"

下面是根据分类规则获取抖音和system_server的smaps筛选出来Unknown部分内存。

com.ss.android.ugc.aweme

item name                                Pss size​
Total                                    572
[anon:partition_alloc]                   432​
[anon:.bss]                              132​
no_vma_name                              4​
[anon:System property context nodes]     4​
[anon:libwebview reservation]            0​
[anon:thread signal stack]               0​
[anon:bytehook-stack]                    0​
[anon:atexit handlers]                   0​
[anon:bionic_alloc_small_objects]        0​
[anon:cfi shadow]                        0​
[anon:bionic_alloc_lob]                  0​
[anon:bytehook-plt-trampolines]          0​
[anon:linker_alloc]                      0​
[anon:arc4random data]                   0

system_server

item name                                Pss size​
Total                                    1820
[anon:.bss]                              1022​
[anon:linker_alloc]                      460​
[anon:thread signal stack]               180​
[anon:bionic_alloc_small_objects]        72​
[anon:System property context nodes]     24​
[anon:atexit handlers]                   18​
[anon:cfi shadow]                        16​
no_vma_name                              12​
[anon:bionic_alloc_lob]                  8​
[anon:arc4random data]                   8​
[anon:libwebview reservation]            0​

二、功能设计

未被统计的内存字段

根据上一节介绍,被统计到Unknown 部分的vma分为两个部分。第一部分是形如”[anon:xxx]“的部分未被统计的匿名映射,还有一部分无名字的匿名映射。

针对第一部分,可以通过测试筛选出来,或者在设置name的代码处查找该部分内存的分配。

**prctl - operations on a process,**这个系统调用指令是为进程制定而设计的。

#include <sys/prctl.h>​

int prctl(int option, unsigned long arg2, unsigned long arg3,​
         unsigned long arg4, unsigned long arg5);

option参数决定了调用prctl()方法所执行的功能。通过PR_SET_VMA配合PR_SET_VMA_ANON_NAME可以对制定地址和大小的vma设置名字。

 
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, size, "name"); // [anon:name]

下面是通过抓取进程smaps统计出来的未被拆分统计的vma字段,其中有一些是APP申请的内存。还有一些低概率的内存申请可以通过在源代码中搜索"prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME" 来查找。

'[anon:.bss]',​
'[anon:bionic_alloc_lob]',​
'[anon:cfi shadow]',​
'[anon:Allocate]',​
'[anon:InternalMmapVector]',​
'[anon:System property context nodes]',​
'[anon:atexit handlers]',​
'[anon:thread signal stack]',​
'[anon:libwebview reservation]',​
'[anon:ReadFileToBuffer]',​
'[anon:linker_alloc]',​
'[anon:arc4random data]',​
'[anon:bionic_alloc_small_objects]',​

下面对这些条目进行简单的介绍和分类。

有名的匿名映射

Linker

.bss

 
// bionic/linker/linker_main.cpp​
if (seg_page_end > seg_file_end) {
  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,​
        reinterpret_cast<void*>(seg_file_end), seg_page_end - seg_file_end,​
        ".bss");
}​

// bionic/linker/linker_phdr.cpp​
if (seg_page_end > seg_file_end) {
  size_t zeromap_size = seg_page_end - seg_file_end;
  void* zeromap = mmap(reinterpret_cast<void*>(seg_file_end),​
                       zeromap_size,​
                       PFLAGS_TO_PROT(phdr->p_flags),​
                       MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,​
                       -1,​
                       0);
  if (zeromap == MAP_FAILED) {
    DL_ERR("couldn't zero fill \"%s\" gap: %s", name_.c_str(), strerror(errno));
    return false;
  }​

  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, zeromap, zeromap_size, ".bss");
}
linker_alloc
// bionic/linker/linker_block_allocator.cpp​

void LinkerBlockAllocator::create_new_page() {​
  static_assert(sizeof(LinkerBlockAllocatorPage) == kAllocateSize,​
                "Invalid sizeof(LinkerBlockAllocatorPage)");

  LinkerBlockAllocatorPage* page = reinterpret_cast<LinkerBlockAllocatorPage*>(
      mmap(nullptr, kAllocateSize, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0));
  CHECK(page != MAP_FAILED);

  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, page, kAllocateSize, "linker_alloc");

  FreeBlockInfo* first_block = reinterpret_cast<FreeBlockInfo*>(page->bytes);
  first_block->next_block = free_block_list_;
  first_block->num_free_blocks = sizeof(page->bytes) / block_size_;

  free_block_list_ = first_block;

  page->next = page_list_;
  page_list_ = page;
}​

cfi shadow

// bionic/linker/linker_cfi.cpp​

void CFIShadowWriter::FixupVmaName() {​
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, *shadow_start, kShadowSize, "cfi shadow");
}

Bionic libc alloc

bionic_alloc_small_objects

BionicAllocator 是一个与malloc/free/realloc/memalign功能类似的通用分配器。当分配大小大于1k时,分配器直接调用mmap分配;当小于1k时,分配器使用BionicSmallObjectAllocator 分配最接近的2的幂大小。在释放内存时,如果使用mmap分配内存,直接使用munmap进行释放;若使用BionicSmallObjectAllocator 分配的内存,将要释放的块加入对应page 的free_blocks_list 中。需要释放的page 达到2时,使用munmap释放其中的一个。

// libc/bionic/bionic_allocator.cpp​
void BionicSmallObjectAllocator::alloc_page() {​
  void* const map_ptr = mmap(nullptr, PAGE_SIZE, PROT_READ | PROT_WRITE,​
                             MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (map_ptr == MAP_FAILED) {
    async_safe_fatal("mmap failed: %s", strerror(errno));
  }​

  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, map_ptr, PAGE_SIZE,​
        "bionic_alloc_small_objects");
bionic_alloc_lob
// bionic/libc/bionic/bionic_allocator.cpp​

void* BionicAllocator::alloc_mmap(size_t align, size_t size) {  size_t header_size = __BIONIC_ALIGN(kPageInfoSize, align);
  size_t allocated_size;
  if (__builtin_add_overflow(header_size, size, &allocated_size) ||
      PAGE_END(allocated_size) < allocated_size) {
    async_safe_fatal("overflow trying to alloc %zu bytes", size);
  }​
  allocated_size = PAGE_END(allocated_size);
  void* map_ptr = mmap(nullptr, allocated_size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,​
                       -1, 0);

  if (map_ptr == MAP_FAILED) {
    async_safe_fatal("mmap failed: %s", strerror(errno));
  }​

  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, map_ptr, allocated_size, "bionic_alloc_lob");

  void* result = static_cast<char*>(map_ptr) + header_size;
  page_info* info = get_page_info_unchecked(result);
  memcpy(info->signature, kSignature, sizeof(kSignature));
  info->type = kLargeObject;
  info->allocated_size = allocated_size;

  return result;
}​
 

Bionic libc other

arc4random data

arc4random, arc4random_buf, arc4random_uniform是一系列提供高质量随机数的生成器。

// bionic/libc/upstream-openbsd/android/include/arc4random.h​
static inline int​
_rs_allocate(struct _rs **rsp, struct _rsx **rsxp)​
{
 // OpenBSD's arc4random_linux.h allocates two separate mappings, but for​
 // themselves they just allocate both structs into one mapping like this.​
 struct {​
  struct _rs rs;​
  struct _rsx rsx;​
 } *p;​

 if ((p = mmap(NULL, sizeof(*p), PROT_READ|PROT_WRITE,​
     MAP_ANON|MAP_PRIVATE, -1, 0)) == MAP_FAILED)​
  return (-1);​

 prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, p, sizeof(*p), "arc4random data");​

 *rsp = &p->rs;​
 *rsxp = &p->rsx;​

 return (0);​
}​

thread signal stack

// bionic/libc/bionic/pthread_create.cpp​
static void __init_alternate_signal_stack(pthread_internal_t* thread) {  // Create and set an alternate signal stack.void* stack_base = mmap(nullptr, SIGNAL_STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  if (stack_base != MAP_FAILED) {
    // Create a guard to catch stack overflows in signal handlers.if (mprotect(stack_base, PTHREAD_GUARD_SIZE, PROT_NONE) == -1) {
      munmap(stack_base, SIGNAL_STACK_SIZE);
      return;
    }​
    stack_t ss;
    ss.ss_sp = reinterpret_cast<uint8_t*>(stack_base) + PTHREAD_GUARD_SIZE;
    ss.ss_size = SIGNAL_STACK_SIZE - PTHREAD_GUARD_SIZE;
    ss.ss_flags = 0;
    sigaltstack(&ss, nullptr);
    thread->alternate_signal_stack = stack_base;

    // We can only use const static allocated string for mapped region name, as Android kernel// uses the string pointer directly when dumping /proc/pid/maps.​
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ss.ss_sp, ss.ss_size, "thread signal stack");
  }​
}

atexit handlers

atexit: 注册一个可以在进程正常终止时运行的function

// bionic/libc/bionic/atexit.cpp​
bool AtexitArray::expand_capacity() {  size_t new_capacity;
  if (!next_capacity(capacity_, &new_capacity)) return false;
  const size_t new_capacity_bytes = page_end_of_index(new_capacity);

  set_writable(true, 0, capacity_);

  bool result = false;
  void* new_pages;
  if (array_ == nullptr) {
    new_pages = mmap(nullptr, new_capacity_bytes, PROT_READ | PROT_WRITE,​
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  } else {
    // mremap fails if the source buffer crosses a boundary between two VMAs. When a single array// element is modified, the kernel should split then rejoin the buffer's VMA.​
    new_pages = mremap(array_, page_end_of_index(capacity_), new_capacity_bytes, MREMAP_MAYMOVE);​
  }​
  if (new_pages == MAP_FAILED) {​
    async_safe_format_log(ANDROID_LOG_WARN, "libc",​
                          "__cxa_atexit: mmap/mremap failed to allocate %zu bytes: %s",​
                          new_capacity_bytes, strerror(errno));​
  } else {​
    result = true;​
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, new_pages, new_capacity_bytes, "atexit handlers");​
    array_ = static_cast<AtexitEntry*>(new_pages);​
    capacity_ = new_capacity;​
  }​
  set_writable(false, 0, capacity_);​
  return result;​
}​

System property context nodes

 
// bionic/libc/system_properties/contexts_serialized.cpp​
bool ContextsSerialized::InitializeContextNodes() {  auto num_context_nodes = property_info_area_file_->num_contexts();
  auto context_nodes_mmap_size = sizeof(ContextNode) * num_context_nodes;
  // We want to avoid malloc in system properties, so we take an anonymous map instead (b/31659220).void* const map_result = mmap(nullptr, context_nodes_mmap_size, PROT_READ | PROT_WRITE,​
                                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (map_result == MAP_FAILED) {
    return false;
  }​

  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, map_result, context_nodes_mmap_size,​
        "System property context nodes");

  context_nodes_ = reinterpret_cast<ContextNode*>(map_result);
  num_context_nodes_ = num_context_nodes;
  context_nodes_mmap_size_ = context_nodes_mmap_size;

  for (size_t i = 0; i < num_context_nodes; ++i) {
    new (&context_nodes_[i]) ContextNode(property_info_area_file_->context(i), filename_);
  }​

  return true;
}

abort message

void android_set_abort_message(const char* msg) {  ScopedPthreadMutexLocker locker(&__libc_shared_globals()->abort_msg_lock);
  size_t size = sizeof(magic_abort_msg_t) + strlen(msg) + 1;
  void* map = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
  // Name the abort message mapping to make it easier for tools to find the// mapping.​
  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, map, size, "abort message");

Other

libwebview reservation

// frameworks/base/native/webview/loader/loader.cpp​
jboolean DoReserveAddressSpace(jlong size) {  size_t vsize = static_cast<size_t>(size);

  void* addr = mmap(NULL, vsize, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (addr == MAP_FAILED) {
    ALOGE("Failed to reserve %zd bytes of address space for future load of ""libwebviewchromium.so: %s",​
          vsize, strerror(errno));
    return JNI_FALSE;
  }​
  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, vsize, "libwebview reservation");
  gReservedAddress = addr;
  gReservedSize = vsize;
  ALOGV("Reserved %zd bytes at %p", vsize, addr);
  return JNI_TRUE;
}​

leak_detector_malloc

// system/memory/libmemunreachable/Allocator.cpp#157​
​
#if defined(PR_SET_VMA)​
  prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, reinterpret_cast<uintptr_t>(ptr), size,​
        "leak_detector_malloc");​
#endif

libmemunreachable stack

// system/memory/libmemunreachable/PtracerThread.cpp#53​
​
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, base_, size_, "libmemunreachable stack");

partition_alloc

PartitionAlloc 是一个为安全优化过的、高效的内存分配器

  • 每个Partition的都是独立,且Partition中所拥有的内存,在释放相对应的物理内存后,虚拟内存仍被保留。因此Partition的虚拟内存是不会被别的Partition所使用(安全性)

  • 每个Partition拥有一个PartitionBucket数组,PartitionBucket包含着大小相近的对象

  • PartitionAlloc的allocation 和 deallocation 操作都只有hot 或者 fast 两个分支(高效)

  • PartitionAlloc 很多方法都是inline的

  • 在单线程中,支持无锁操作。在多线程中,使用高效的自旋锁来进行同步

// external/pdfium/third_party/base/allocator/partition_allocator/page_allocator_internals_posix.h​
const char* PageTagToName(PageTag tag) {  // Important: All the names should be string literals. As per prctl.h in// //third_party/android_ndk the kernel keeps a pointer to the name instead// of copying it.//// Having the name in .rodata ensures that the pointer remains valid as// long as the mapping is alive.switch (tag) {​
    case PageTag::kBlinkGC:​
      return "blink_gc";​
    case PageTag::kPartitionAlloc:​
      return "partition_alloc";​
    case PageTag::kChromium:​
      return "chromium";​
    case PageTag::kV8:​
      return "v8";​
    default:​
      DCHECK(false);​
      return "";​
  }​
}

bytehook-stack、bytehook-plt-trampolines

抖音应用hook工具申请的内存。

未命名的匿名映射

通常进程通过mmap申请内存,同时设置该内存块的属性(prot)和标志(flags),默认不显示名字字段,即使通过prctl设置vma name,也无法在现有统计规则中进行统计。可以通过属性值将其分类为数据段或者代码段,即数据段具有可读可写属性(rw-p),代码段具有可执行属性(r-xp)。

#include <sys/mman.h>​
​
void *mmap(void *addr, size_t length, int prot, int flags,​
          int fd, off_t offset);
内核每进程的vm_area_struct项/proc/pid/maps中的项含义
vm_start“-”前一列,如00377000此段虚拟地址空间起始地址
vm_end“-”后一列,如00390000此段虚拟地址空间结束地址
vm_flags第三列,如r-xprw-p r-xp ---p此段虚拟地址空间的属性。每种属性用一个字段表示,r表示可读,w表示可写,x表示可执行,p和s共用一个字段,互斥关系,p表示私有段,s表示共享段,如果没有相应权限,则用’-’代替
vm_pgoff第四列,如00000000对有名映射,表示此段虚拟内存起始地址在文件中以页为单位的偏移。对匿名映射,它等于0或者vm_start/PAGE_SIZE
vm_filef_dentryd_inodei_sbs_dev第五列,如fd:00映射文件所属设备号。对匿名映射来说,因为没有文件在磁盘上,所以没有设备号,始终为00:00。对有名映射来说,是映射的文件所在设备的设备号
vm_filef_dentryd_inodei_ino第六列,如9176473映射文件所属节点号。对匿名映射来说,因为没有文件在磁盘上,所以没有节点号,始终为00:00。对有名映射来说,是映射的文件的节点号
第七列,如/lib/ld-2.5.so对有名来说,是映射的文件名。对匿名映射来说,是此段虚拟内存在进程中的角色。[stack]表示在进程中作为栈使用,[heap]表示堆。其余情况则无显示

分类与聚合

根据上述对字段的分类描述,可以将Unknown 进行如下分类,并在dumpsys meminfo中进行修改。

mmap匿名映射新增owner信息

在上述的分类中,未命名的匿名映射只能被归类到Unnamed字段。想要进一步对未命名字段进行来源追踪,我们首先在每一次mmap匿名映射的时候,都强制为地址添加了名字。在添加的vma名字中记录了调用地址。

在内存拆解中,针对”[anon:mappingowner”的字段单独进行统计,按照内存大小进行排序。并且对调用地址进行解析,得到动态库以及偏移地址。

三、功能验证与使用

集成系统功能

dumpsys位于/frameworks/native/cmds/dumpsys。运行时它首先后获得ServiceManager,然后根据参数查找服务,找到后binder调用服务的dump方法。meminfo位于/system/memory/libmeminfo,是系统的一个服务(内部则是一个MemBinder的类实现的),当dumpsys找到这个服务后,调用dump方法,通过传递的参数,显示出进程内存使用情况。

执行dumpsys meminfo -u <process name> | <PID> 命令(添加-u参数)可以查看到对Unknown的详细拆解,如下图中的Unknown Details部分

本地脚本测试

通过执行脚本加进程名或者PID,获取meminfo中Unknown部分的详细数据。

下载地址:https://git.n.xiaomi.com/xutianqi/toolkit/-/blob/master/scripts/split_unknown_from_device.py

代码提交

原始方案(T 版本):https://gerrit.pt.mioffice.cn/c/platform/frameworks/base/+/2759246

新增执行参数(默认不生效):https://gerrit.pt.mioffice.cn/c/platform/frameworks/base/+/2824542

新增mapping owner信息(U 版本):https://gerrit.pt.mioffice.cn/q/topic:mapping_owner