本文介绍一种适用于线上的,低侵入的,无包体影响的 ,高效全面稳定的已加载类检测方法,仅需plt hook一处方法调用。
已加载类检测解决方案适用以下场景使用。
- 三方库代码加载检测
- 启动阶段类加载统计
- 代码覆盖率统计
背景
需要检测三方sdk的类有没有被加载。
现有方案
- 静态代码插桩方案, 比如Bytex的coverage github.com/bytedance/B… ,会带来一定的包体积和性能影响,无法检测出动态加载的代码。
- Hook PathClassLoader方案, 需要找到所有的classloader,无法识别出新增classloader的类加载 ,而且hook逻辑执行之前的部分是无法被检测的。
- Hack访问ClassTable方案, 使用dlopen,dlsym动态调用底层ClassTable的Lookup方法遍历判断。这里就有个问题传进去的类必须是精确的,也就是无法按定义规则去判断,同时也需要找到所有classloader中的classtable,需要hack实现比较复杂。
- 复制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点
-
art::mirror::Class::DumpClass(std::__1::basic_ostream<char, std::__1::char_traits >&, int) 只能暂时inline hook放弃
-
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个文件在百毫秒级别。 完全可以在特定场景比如切后台后获取一次
适用场景:
- 三方库代码调用检测
- 启动阶段类加载统计
- 代码覆盖率