一种Android已加载类检测方法

662 阅读3分钟

本文介绍一种适用于线上的,低侵入的,无包体影响的 ,高效全面稳定的已加载类检测方法,仅需plt hook一处方法调用。

已加载类检测解决方案适用以下场景使用。

  1. 三方库代码加载检测
  2. 启动阶段类加载统计
  3. 代码覆盖率统计

背景

需要检测三方sdk的类有没有被加载。

现有方案

  1. 静态代码插桩方案, 比如Bytex的coverage github.com/bytedance/B… ,会带来一定的包体积和性能影响,无法检测出动态加载的代码。
  2. Hook PathClassLoader方案, 需要找到所有的classloader,无法识别出新增classloader的类加载 ,而且hook逻辑执行之前的部分是无法被检测的。
  3. Hack访问ClassTable方案, 使用dlopen,dlsym动态调用底层ClassTable的Lookup方法遍历判断。这里就有个问题传进去的类必须是精确的,也就是无法按定义规则去判断,同时也需要找到所有classloader中的classtable,需要hack实现比较复杂。
  4. 复制ClassTable指针,通过标准 API 间接访问类加载状态的方案 ,mp.weixin.qq.com/s/qCwBF-BNG… 也没详细说什么方案,猜测也存在需要指定类名去遍历。

自研方案

通过调用系统 api android.os.Debug.printLoadedClasses 结合plt hook android::base::LogMessage::stream() 获取已加载类数据 支持>= Android 8.0

阅读代码,发现类加载完后会存入classtable中,方便下一次直接获取。

观察art/runtime/class_table.cc,确实没有找到合适的api去遍历获取。 好像也只有一个ClassTable::Lookup(const char* descriptor, size_t hash)api能用了。 再跟一下classtable中classes_的使用。 哦豁,原来自带visitor模式。

继续分析一下哪个ClassVisitor我们是直接能用的, 发现了下面这个好东西 , 原来直接有打印方法。。 在java层也就是 android.os.Debug的printLoadedClasses方法。

不过这个是把已加载类信息直接打印到控制台了,线上我们需要想办法自己拦截到这个数据处理。

寻找hook点

  1. art::mirror::Class::DumpClass(std::__1::basic_ostream<char, std::__1::char_traits >&, int) 只能暂时inline hook放弃

  2. android::base::LogMessage::stream() 可以通过plthook 替换stream ,这里放到内存还是磁盘自己决定。

Demo 如下。

std::ofstream fout;

void *test_origin = NULL;
thread_local bool hook_start = false;

typedef std::ostream &(*type_t5)(void *instance);


std::ostream &testProxy(void *instance) {
    //通过线程判断 只在我们自己线程操作时进行hook
    if (hook_start && fout.is_open()) {
        return fout;
    }
    return ((type_t5) test_origin)(instance);
}


JNIEXPORT void JNICALL
Java_com_example_libs_hook_ArtMethodBridge_printLoadedClassStart(JNIEnv *env,
                                                                           jclass clazz, jstring path) {
    hook_start = true;
    xh_core_register(
            "libart.so",
            "_ZN7android4base10LogMessage6streamEv",
            (void *) testProxy,
            (void **) &test_origin);
    xh_core_refresh(0);
    xh_core_clear();
    const char *c_path = env->GetStringUTFChars(path, nullptr);
    fout.open(c_path, std::ios::out);
    env->ReleaseStringUTFChars(path, c_path);

}

JNIEXPORT void JNICALL
Java_com_example_libs_hook_ArtMethodBridge_printLoadedClassEnd(JNIEnv *env,
                                                                         jclass clazz) {
    if (fout.is_open()) {
        fout.flush();
        fout.close();
    }
    hook_start = false;
}
public static void printLoadedClass(String path) {
    ArtMethodBridge.printLoadedClassStart(path);
    Debug.printLoadedClasses(2);
    ArtMethodBridge.printLoadedClassEnd();
}

结果分析

内容格式:0x0的为BootClassLoader加载

文件大小:4w个类,原始2.8MB, 压缩后300KB

性能:输出到1个文件在百毫秒级别。 完全可以在特定场景比如切后台后获取一次

适用场景:

  1. 三方库代码调用检测
  2. 启动阶段类加载统计
  3. 代码覆盖率