Android Cultivation Series (35), Solución técnica de monitoreo de memoria (Parte 2)

Las dos primeras secciones presentaron respectivamente cómo monitorear el FD y la cantidad de subprocesos y, desde la perspectiva del código fuente de koom, detallaron cómo monitorear las fugas de memoria de Java y las pilas de subprocesos nativos.

Y presentó cómo monitorear la memoria virtual y la memoria del montón de Java , y comenzó desde el código fuente de matrix y koom respectivamente, y describió dos soluciones principales de monitoreo de fugas de memoria de Java.

Esta sección presentará el monitoreo de la memoria nativa desde tres perspectivas:

  1. Monitoreo de aplicaciones de memoria tan grande.

  2. Monitoreo de aplicaciones de panorama general.

  3. Supervisión de fugas de memoria nativa.

memoria nativa

La memoria nativa generalmente se refiere a la biblioteca Business So. La memoria solicitada dinámicamente a través de c/c++ se usa comúnmente llamando a la función malloc para solicitar memoria y llamando a free para liberar la memoria. Estas aplicaciones de memoria deben liberarse razonablemente, de lo contrario, se producirán pérdidas de memoria.

Podemos analizar el estado actual de la memoria a través del archivo /proc/pid/smaps ( ver detalles arriba ), donde Pss representa la suma de la memoria exclusiva de la capa nativa en el proceso y la memoria física amortizada compartida con otros procesos.

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
...
复制代码

Cuando una aplicación solicita memoria, solicita memoria virtual. Cuando la aplicación accede a esta memoria y realiza una operación de escritura, si no se ha asignado la memoria física, se producirá un error de página y se activará la asignación de la memoria física. Cuando se asigna memoria física, se basa en "páginas", y cada página suele tener 4 KB de espacio de memoria. Una vez completada la asignación, la relación de mapeo entre la dirección virtual y la dirección física de cada página se registra en el PageTable.

Supervisión de memoria nativa, las principales soluciones ahora parten de dos aspectos, uno es la supervisión de aplicaciones de gran memoria en el SO, y el otro es la supervisión de aplicaciones de imágenes de gran tamaño. A continuación, analizaremos estos dos grandes usuarios de memoria:

Monitoreo de memoria grande

En la mayoría de los casos, la empresa aplica y libera memoria a través de malloc y funciones gratuitas (para comodidad de la administración, el lado comercial debe tratar de no solicitar memoria directamente a través de 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,这是最常见的创建方式

imagen.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 对象。通过该对象,就可以获得每次创建的图片的尺寸大小、内存占用大小,堆栈等信息。

  • Debido a que la función createBitmap de JNI se compila en libandroid_runtime.so, el nombre del símbolo ha cambiado, por lo que no podemos enlazar directamente createBitmap, entonces, ¿cómo obtenemos el nuevo nombre de la función? Puede pasar directamente el siguiente comando (recuerde conectar el dispositivo primero):
> adb pull system/lib/libandroid_runtime.so
> arm-linux-androideabi-nm -D libandroid_runtime.so | grep bitmap
复制代码
  • Diferentes versiones del sistema, el nombre de la función también puede ser diferente, para ser compatible, es decir, diferentes versiones de funciones diferentes gancho:

image.png

Con respecto al esquema de gancho, puede intentar usar el marco de gancho en línea android-inline-hook of bytes , que no se presentará aquí, y lo compartiré más adelante si tengo la oportunidad.

Fuga de memoria nativa

El esquema de fuga de memoria nativa es similar al esquema de fuga de subprocesos de escucha mencionado anteriormente . Aquí hay un breve análisis:

También tome KOOM como ejemplo, esta es la entrada de 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));
}
复制代码

Aquí está la instalación:

// 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
复制代码

Después de llamar a malloc, ejecute:

// 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__)
复制代码

Luego llame a OnMonitor:

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

Luego llame a 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));
}
复制代码

Del mismo modo, después de ejecutar free, elimine de live_alloc_records_:

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

Finalmente, obtenga la información de live_alloc_records_#Dump cuando llame a jni_leak_monitor.cpp#GetLeakAllocs.

Bueno, estos son los métodos comunes para monitorear la memoria nativa.

Este capítulo ha terminado.

Supongo que te gusta

Origin juejin.im/post/7084934068416512008
Recomendado
Clasificación