Android Cultivation Series(35)、メモリモニタリングテクニカルソリューション(パート2)

最初の2つのセクションでは、それぞれFDとスレッド数を監視する方法、koomソースコードの観点から、Javaおよびネイティブスレッドスタックのメモリリークを監視する方法について詳しく説明しました。

また、仮想、それぞれmatrixとkoomのソースコードから始めて、2つの主流のJavaメモリリーク監視ソリューションについて説明しました。

このセクションでは、次の3つの観点からネイティブメモリの監視を紹介します。

  1. したがって、大容量メモリアプリケーションの監視。

  2. 全体像のアプリケーション監視。

  3. ネイティブメモリリークの監視。

ネイティブメモリ

ネイティブメモリは通常、ビジネスSOライブラリを指します。c/ c ++を介して動的に適用されるメモリは、malloc関数を呼び出してメモリを適用し、freeを呼び出してメモリを解放することで一般的に使用されます。これらのメモリアプリケーションは、合理的に解放する必要があります。そうしないと、メモリリークが発生します。

/ proc / pid / smapsファイル(詳細は上記を参照)を使用して現在のメモリ状態を分析できます。ここで、Pssは、プロセス内のネイティブレイヤー専用のメモリと、他のプロセスと共有される償却済み物理メモリの合計を表します。

7d4a434000-7d4a435000 rw-p 00010000 fe:00 4380  /system/vendor/lib64/libqdMetaData.so
Size:                  4 kB
Rss:                   4 kB
Pss:                   4 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         4 kB
...
复制代码

アプリケーションがメモリに適用される場合、それは仮想メモリに適用されます。アプリケーションがこのメモリにアクセスして書き込み操作を実行するときに、物理メモリが割り当てられていない場合、ページフォールトが発生し、物理メモリの割り当てがトリガーされます。実際に物理メモリを割り当てる場合、これは「ページ」に基づいており、各ページは通常4KBのメモリスペースです。割り当てが完了すると、各ページの仮想アドレスと物理アドレスの間のマッピング関係がPageTableに記録されます。

現在、主流のソリューションであるネイティブメモリの監視は2つの側面から始まります。1つは大容量メモリアプリケーションの監視であり、もう1つは大容量イメージのアプリケーション監視です。次に、これら2つの大容量メモリユーザーを分析します。

大容量メモリの監視

ほとんどの場合、ビジネスはmallocおよびfree関数を介してメモリを適用および解放します(管理の便宜のために、ビジネス側はmmapを介して直接メモリを適用しないようにする必要があります)。

那么监控方案也就呼之欲出了: 直接 hook 掉系统库 libc.so 的 malloc 和 free 等我们操作内存的函数,在我们的 hook_malloc 和 hook_free 函数内记录每次分配的内存大小和地址,通过系统堆栈回溯机制追踪到业务函数调用堆栈地址,并读取当前的 smap 文件,进行内存分析。

下面以 matrix 为例,讲讲它是如何使用 iqiyi xhook hook 掉 malloc 函数的:

这是 MemoryHook,其提供了内存 hook 配置的接口:

MemoryHook#addHookSo(String)         // 要hook的so库
MemoryHook#addIgnoreSo(String)       // 要过滤的so库
MemoryHook#enableMmapHook(boolean)   // 是否要hook mmap
MemoryHook#enableStacktrace(boolean) // 是否要收集堆栈
MemoryHook#getNativeLibraryName()    // 当前本地库名称
MemoryHook#hook()                    // 开始hook
MemoryHook#dump(String, String)      // 开始jump信息
复制代码

开始 hook:

// com.tencent.matrix.hook.memory.MemoryHook.java
// java层的作用就是: 正确加载so,并将相应配置通过native方法给c++库,最后 install hook
// 具体代码就不一条条贴了,大概步骤:
// 1. MemoryHook#hook() -> HookManager#commitHooks() -> HookManager#commitHooksLocked()
// 2.-> MemoryHook#onConfigure() -> MemoryHook#onHook(boolean) 
// 3.-> MemoryHook#installHooksNative
public void hook() throws HookManager.HookFailedException {
    HookManager.INSTANCE // 内部持有AbsHook对象,能直接调用MemoryHook接口
            .clearHooks()
            .addHook(this)
            .commitHooks();
}
复制代码

这是 Jni 的入口:

// MemoryHookJNI.cpp
nstallHooksNative(JNIEnv* env, jobject thiz,
                  jobjectArray hook_so_patterns,
                  jobjectArray ignore_so_patterns,
                  jboolean enable_debug) {
    memory_hook_init(); // 开启一个线程,并执行 BufferManagement::process_routine, 定时检查 busy_ratio
    LOGI(TAG, "memory_hook_init");

    xhook_block_refresh();
    {
        jsize size = env->GetArrayLength(hook_so_patterns);
        for (int i = 0; i < size; ++i) { // 拿到每个要 hook 的 so name
            auto jregex = (jstring) env->GetObjectArrayElement(hook_so_patterns, i);
            const char* regex = env->GetStringUTFChars(jregex, nullptr);
            hook(regex); // 开始hook
            env->ReleaseStringUTFChars(jregex, regex);
        }
    }
    ... // ignore_so 同理,并通 过xhook_grouped_ignore 忽略部分 hook 信息
    xhook_unblock_refresh();
}
复制代码

这是 hook 接口:

// MemoryHookJNI.cpp
static void hook(const char *regex) {

    for (auto f : HOOK_MALL_FUNCTIONS) {
        // 真正执行 hook 操作,将原 f.name 方法 hook 到我们新方法 f.handler_ptr 上
        int ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, f.name, f.handler_ptr, f.origin_ptr);
        LOGD(TAG, "hook fn, regex: %s, sym: %s, ret: %d", regex, f.name, ret);
    }
    LOGD(TAG, "mmap enabled ? %d", enable_mmap_hook);
    if (enable_mmap_hook) { // 是否需要 hook mmap
        for (auto f: HOOK_MMAP_FUNCTIONS) {
            xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, f.name, f.handler_ptr, f.origin_ptr);
        }
    }
}
复制代码

这是 HOOK_MALL_FUNCTIONS:

// HookCommon.h
typedef struct {
    const char *name;        // 要 hook 的函数 name
    void       *handler_ptr; // 要被替换成的 PLT 入口点地址值
    void       **origin_ptr; // 调用函数的 PLT 入口点的地址值
} HookFunction;

// MemoryHookJNI.cpp
const HookFunction HOOK_MALL_FUNCTIONS[] = {
        {"malloc", (void *) h_malloc, NULL},
        {"calloc", (void *) h_calloc, NULL},
        {"realloc", (void *) h_realloc, NULL},
        {"free", (void *) h_free, NULL},
        {"memalign", (void *) HANDLER_FUNC_NAME(memalign), NULL},
        {"posix_memalign", (void *) HANDLER_FUNC_NAME(posix_memalign), NULL}
        ...
}
复制代码

hook 成功后,当我们调用 malloc 时,会执行 h_malloc:

// MemoryHookFunctions.cpp
// CALL_ORIGIN_FUNC_RET 是被定义的多行宏函数
// DEFINE_HOOK_FUN 相当于 void* h_malloc(size_t __byte_count)
DEFINE_HOOK_FUN(void *, malloc, size_t __byte_count) {
    CALL_ORIGIN_FUNC_RET(void*, p, malloc, __byte_count);
    LOGI(TAG, "+ malloc %p", p);
    DO_HOOK_ACQUIRE(p, __byte_count);
    return p;
}

// HookCommon.h
#define HANDLER_FUNC_NAME(fn_name) h_##fn_name
#define DEFINE_HOOK_FUN(ret, sym, params...) \
    ORIGINAL_FUNC_PTR(sym); \
    ret HANDLER_FUNC_NAME(sym)(params)
    
// MemoryHookFunctions.cpp   
#define DO_HOOK_ACQUIRE(p, size) \
    GET_CALLER_ADDR(caller); \
    on_alloc_memory(caller, p, size);
复制代码

随后会执行 on_alloc_memory:

// MemoryHook.cpp
// 不仅 malloc,realloc、mmap 都会统一到 on_acquire_memory 方法
// 同理 free,munmap 都会统一调用on_release_memory
// 当然我们也可以分开监听
void on_alloc_memory(void *caller, void *ptr, size_t byte_count) {
    on_acquire_memory(caller, ptr, byte_count, message_type_allocation);
}
复制代码

这是 on_acquire_memory:

// MemoryHook.cpp
static inline void on_acquire_memory(
        void *caller,
        void *ptr,
        size_t byte_count,
        message_type type) {
    ...
    // 1. 是否需要堆栈回溯
    memory_backtrace_t backtrace{0};
    if (LIKELY(byte_count > 0 && is_stacktrace_enabled && should_do_unwind(byte_count))) {
        size_t frame_size = 0;
        do_unwind(backtrace.frames, MEMHOOK_BACKTRACE_MAX_FRAMES,
                  frame_size);
        backtrace.frame_size = frame_size;
    }
    container->lock();
    ...
    // 2. 记录函数地址和申请内存大小和地址
    message->ptr = reinterpret_cast<uintptr_t>(ptr);
    message->size = byte_count;
    message->caller = reinterpret_cast<uintptr_t>(caller);
    if (backtrace.frame_size) {
        message->backtrace = backtrace;
    }
    container->unlock();
    }
    ...
复制代码

到这里 hook 的大致逻辑就完了。

大图监控

创建 Bitmap 的方式很多,

  • 可以通过 SDK 提供的 API 来创建 Bitmap
  • 加载某些布局或资源时会创建 Bitmap
  • Glide 等第三方图片库会创建 Bitmap

先说通过 API 创建 Bitmap。SDK 中创建 Bitmap 的 API 很多,分成三大类:

  • 创建 Bitmap - Bitmap.createBitmap() 方法在内存中从无到有地创建 Bitmap

  • 拷贝 Bitmap - Bitmap.copy() 从已有的 Bitmap 拷贝出一个新的 Bitmap

  • 解码 - 从文件或字节数组等资源解码得到 Bitmap,这是最常见的创建方式

image.png

Java 层的创建 Bitmap 的所有 API 进入到 Native 层后,全都会走如下这四个步骤。

  • 资源转换 - 这一步将 Java 层传来的不同类型的资源转换成解码器可识别的数据类型
  • 内存分配 - 分配内存时会考虑是否复用 Bitmap、是否缩放 Bitmap 等因素
  • 图片解码 - 实际的解码工作由第三方库完成,解码结果填在上一步分配的内存中。注,Bitmap.createBitmap() 和 Bitmap.copy() 创建的 Bitmap 不需要进行图片解码
  • 创建对象 - 这一步将包含解码数据的内存块包装成 Java 层的 android.graphics.Bitmap 对象,方便 App 使用

摘自 Bitmap 之从出生到死亡

我们知道在 Android8.0 及更高版本,图片的内存申请是在 Native 层的。

通过源码能发现,所有的图片创建最终都会走到 BitmapFactory.cpp 的 doDecode() 函数中,通过 HeapAllocate 等内存分配器来分配内存并存储图片的像素数据。完成内存分配后,会创建一个 SkBitmp 对象,它持有的 SkPixelRef 存储了内存地址。最后调用JNI层的 createBitmap 函数,在这里创建了Java 层的 bitmap 对象。Bitmap.cpp 见下:

image.png

所以监控方案:只需要 hook 掉这个 createBitmap 函数,就能够拿到每次图片创建时的 bitmap 的 Java 对象。通过该对象,就可以获得每次创建的图片的尺寸大小、内存占用大小,堆栈等信息。

  • JNI createBitmap関数はlibandroid_runtime.soにコンパイルされているため、シンボル名が変更されており、createBitmapを直接フックすることはできません。では、新しい関数名を取得するにはどうすればよいですか?次のコマンドを直接渡すことができます(最初にデバイスを接続することを忘れないでください)。
> adb pull system/lib/libandroid_runtime.so
> arm-linux-androideabi-nm -D libandroid_runtime.so | grep bitmap
复制代码
  • システムバージョンが異なれば、関数名も異なる場合があります。互換性があるため、つまり、フックのバージョンが異なれば、関数も異なります。

image.png

フックスキームに関しては、バイトのインラインフックフレームワークandroid-inline-hookを使用してみることができますが、ここでは紹介しませんが、機会があれば後で共有します。

ネイティブメモリリーク

ネイティブメモリリークスキームは、上記のリスニングスレッドリークスキームに似ています。簡単な分析を次に示します。

また、例としてKOOMを取り上げます。これは、JNIのエントリです。

// jni_leak_monitor.cpp
static bool InstallMonitor(JNIEnv *env, jclass clz, jobjectArray selected_array,
                           jobjectArray ignore_array,
                           jboolean enable_local_symbolic) {
  ...
  // hook的so和要过滤的so
  std::vector<std::string> selected_so = array_to_vector(env, selected_array);
  std::vector<std::string> ignore_so = array_to_vector(env, ignore_array);
  return CheckedClean(
      env, LeakMonitor::GetInstance().Install(&selected_so, &ignore_so));
}
复制代码

インストールは次のとおりです。

// leak_monitor.cpp
bool LeakMonitor::Install(std::vector<std::string> *selected_list,
                          std::vector<std::string> *ignore_list) {
  ...
  // 定义要hook的函数,和重定向后的函数
  std::vector<std::pair<const std::string, void *const>> hook_entries = {
      std::make_pair("malloc", reinterpret_cast<void *>(WRAP(malloc))),
      std::make_pair("realloc", reinterpret_cast<void *>(WRAP(realloc))),
      std::make_pair("calloc", reinterpret_cast<void *>(WRAP(calloc))),
      std::make_pair("memalign", reinterpret_cast<void *>(WRAP(memalign))),
      std::make_pair("posix_memalign",
                     reinterpret_cast<void *>(WRAP(posix_memalign))),
      std::make_pair("free", reinterpret_cast<void *>(WRAP(free)))};

  // 开始hook
  if (HookHelper::HookMethods(register_pattern, ignore_pattern, hook_entries)) {
    has_install_monitor_ = true;
    return true;
  }
  ...
}

// 宏定义,如 WRAP(malloc) -> mallocMonior
#define WRAP(x) x##Monitor
复制代码

mallocを呼び出した後、以下を実行します。

// leak_monitor.cpp
HOOK(void *, malloc, size_t size) {
  auto result = malloc(size);
  LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
                                       size);
  CLEAR_MEMORY(result, size);
  return result;
}

// 多行宏定义
#define HOOK(ret_type, function, ...) \
  static ALWAYS_INLINE ret_type WRAP(function)(__VA_ARGS__)
复制代码

次に、OnMonitorを呼び出します。

// leak_monitor.cpp
ALWAYS_INLINE void LeakMonitor::OnMonitor(uintptr_t address, size_t size) {
  ...
  RegisterAlloc(address, size);
}
复制代码

次に、RegisterAllocを呼び出します。

// leak_monitor.cpp
ALWAYS_INLINE void LeakMonitor::RegisterAlloc(uintptr_t address, size_t size) {
  ...
  // 记录内存大小和地址指针,并加入 live_alloc_records_
  thread_local ThreadInfo thread_info;
  auto alloc_record = std::make_shared<AllocRecord>();
  alloc_record->address = CONFUSE(address);
  alloc_record->size = size;
  alloc_record->index = alloc_index_++;
  memcpy(alloc_record->thread_name, thread_info.name, kMaxThreadNameLen);
  unwind_backtrace(alloc_record->backtrace, &(alloc_record->num_backtraces));
  live_alloc_records_.Put(CONFUSE(address), std::move(alloc_record));
}
复制代码

同様に、freeを実行した後、live_alloc_records_から削除します。

// leak_monitor.cpp
ALWAYS_INLINE void LeakMonitor::UnregisterAlloc(uintptr_t address) {
  live_alloc_records_.Erase(address);
}
复制代码

最後に、jni_leak_monitor.cpp#GetLeakAllocsを呼び出すときに、live_alloc_records_#Dump情報を取得します。

さて、これらはネイティブメモリモニタリングの一般的な方法です。

この章は終わりました。

おすすめ

転載: juejin.im/post/7084934068416512008