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] 0system_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_file→f_dentry→d_inode→i_sb→s_dev | 第五列,如fd:00 | 映射文件所属设备号。对匿名映射来说,因为没有文件在磁盘上,所以没有设备号,始终为00:00。对有名映射来说,是映射的文件所在设备的设备号 |
| vm_file→f_dentry→d_inode→i_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