前言

公司的一款app最近在上架厂商的过程中,被对方指出了IO读写过于频繁,然后不给上架; 但是IO读写的操作非常零散,而且很多第三方框架内都会有写入操作

  • 所以就变得非常难以监控和修改,有没有一种非常简单的方式可以帮助我们去定位这个问题呢?

之后我参考了下腾讯的Matrix的IOCanary监控组件,其原理是通过hook(ELF hook)的机制,hook 了 IO的读取/写入的操作,然后打印出调用堆栈,从而帮助开发同学定位问题

一般来说,一套Apm(Application Performance Monitor)系统是要分成多个部分的; 比如开发阶段工具,测试阶段工具以及线上收集数据等等;而IO监控则是其中的开发测试阶段工具

IOCanary 原理分析

在开始接介绍IOCanary之前,我们要先介绍一些奇怪的黑科技,通过这些东西我们才能完成IO监控系统,而且能讲明白到底IOCanary是如何实现的

动态Hook

提到这个的话,大家可能以为我要写什么Aop切片啥的。但是不好意思你猜错了,还有很多别的手段可以去做无插入式的Hook代码调用的操作的。Aop切片毕竟还是要做字节码修改操作,同时作为一个调试工具的话,的确是有点太复杂了。

简单的介绍下动态Hook,我们可以通过Art虚拟机的机制,在一个方法调用的前后进行钩子操作,然后进行我们所需要的一些动态的监控的操作,已达到我们对于代码的动态监控能力;由于Hook在的是虚拟机层面,所以能监控的就不仅仅只是我们自己的代码,所有第三方库甚至源代码的调用都可以进行Hook

比如Xposed,但是这套框架依赖于手机的Root; 另外Epic也可以做到在安卓上的动态Hook, ,而听说腾讯的IOCanary则是参考了爱奇艺的xHook的原理

从上面讲述的ART方法调用原理可以得到一种很自然的Hook办法————直接替换entrypoint。通过把原方法对应的ArtMethod对象的entrypoint替换为目标方法的entrypoint,可以使得原方法被调用过程中取entrypoint的时候拿到的是目标方法的entry,进而直接跳转到目标方法的code段;从而达到Hook的目的。

IOCanary监控

监控IO是不是意味着只需要有方法能监控到文件的写入读取流就可以了呢?我们先简单的看下腾讯的Matrix的IOCanary是如何实现的

采用 hook(ELF hook) 的方案收集IO信息,代码无侵入,从而使得开发者可以无感知接入。方案主要通过 hook os posix 的四个关键的文件操作接口:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n19" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">int open(const char *pathname, int flags, mode_t mode);//成功时返回值就是fd
ssize_t read(int fd, void *buf, size_t size);
ssize_t write(int fd, const void *buf, size_t size);
int close(int fd);</pre>

以上看到,通过 hook 这几个接口,可以拿到大部分关键操作信息。这里举 open 的例子介绍下原理,简单起见,只结合 Android M 的代码以及大家最常用的 FileInputStream 分析。关键要找到 posix open 是在哪里被调用。由上往下列了大致的调用关系:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n21" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">java : FileInputStream -> IoBridge.open -> Libcore.os.open
-> BlockGuardOs.open -> Posix.open↓
jni : libcore_io_Posix.cpp
static jobject Posix_open(...) {...int fd = throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));...
}</pre>

以上看到, android 框架的 FileInputStream ,最终是在 libcore_io_Posix.cpp 那里调到了posix的open接口。那么再找它被编到哪个 so ,查阅源码对应的 NativeCode.mk ,得到LOCAL_MODULE := libjavacore

于是只要 hook libjavacore.so 的 open 符号就 ok 了。找到 hook 目标 so 的目的是把 hook 的影响范围尽可能地降到最小。 同样, write,read,close 也是大同小异。不同的 Android 版本会有些坑需要填,这里不细述, 目前兼容到Android P。

由此便可以收集到应用在文件读写时的相关信息:文件路径、fd、buffer 大小等,并可以统计耗时、操作次数等。基于这些信息,就可以设定一些策略进行检测判断。

其中C++的代码基本就是采用了xhook的类似。比较方便我们学习的是io_canary_jni.cc这个类。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n26" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">namespace iocanary {// hook 这三个核心的so包,其中所有的IO流式操作全部在这三个SO中。const static char *TARGET_MODULES[] = {"libopenjdkjvm.so","libjavacore.so","libopenjdk.so"};// hook 流的open操作int ProxyOpen(const char *pathname, int flags, mode_t mode) {...}// jni 开启动态hook 通过xhook, hook住 io 打开写入关闭等操作Java_com_bilibili_apm_io_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {__android_log_print(ANDROID_LOG_INFO, kTag, "doHook");for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {const char *so_name = TARGET_MODULES[i];__android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);// 将上面需要hook的so包内传递给xhookvoid *soinfo = xhook_elf_open(so_name);if (!soinfo) {__android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.",so_name);continue;}// io 打开操作 并代理成当前类的自定义方法xhook_hook_symbol(soinfo, "open", (void *) ProxyOpen, (void **) &original_open);xhook_hook_symbol(soinfo, "open64", (void *) ProxyOpen64, (void **) &original_open64);bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);if (is_libjavacore) {// hook read 操作if (xhook_hook_symbol(soinfo, "read", (void *) ProxyRead,(void **) &original_read) != 0) {__android_log_print(ANDROID_LOG_WARN, kTag,"doHook hook read failed, try __read_chk");if (xhook_hook_symbol(soinfo, "__read_chk", (void *) ProxyReadChk,(void **) &original_read_chk) != 0) {__android_log_print(ANDROID_LOG_WARN, kTag,"doHook hook failed: __read_chk");xhook_elf_close(soinfo);return JNI_FALSE;}}// hook 写入操作if (xhook_hook_symbol(soinfo, "write", (void *) ProxyWrite,(void **) &original_write) != 0) {__android_log_print(ANDROID_LOG_WARN, kTag,"doHook hook write failed, try __write_chk");if (xhook_hook_symbol(soinfo, "__write_chk", (void *) ProxyWriteChk,(void **) &original_write_chk) != 0) {__android_log_print(ANDROID_LOG_WARN, kTag,"doHook hook failed: __write_chk");xhook_elf_close(soinfo);return JNI_FALSE;}}}//hook 关闭操作xhook_hook_symbol(soinfo, "close", (void *) ProxyClose, (void **) &original_close);xhook_elf_close(soinfo);}__android_log_print(ANDROID_LOG_INFO, kTag, "doHook done.");return JNI_TRUE;}
}</pre>

上面是腾讯的Matrix的官方说明啊,我只是简单的copy了一下。其实原理就和我们一开始介绍的Epic框架基本类似,通过动态Hook底层的实现的方式,让我们可以对于某些方法进行动态的监控。

这里给大家简单的列一下sdk整体流程:

1.初始化IOCanaryJniBridge,然后完成基础初始化。

2.JNI调用Native xhook的代码,hook原生so libopenjdkjvm.so,libjavacore.so,libopenjdk.so 的open write read close方法。

  1. 当读写操作被调用之后,通过jni native调用java方法记录。

  2. 当close方法被触发之后,记录一个io数据结构。

在IOCanary的基础上进行二次封装

Matrix的IOCanary由于只兼容到Android9版本,所以我们在实际的使用中其实碰到了很多问题。同时由于hook的不安全性和不稳定性,建议各位不要把这种功能带到线上去,而是在为debug版本的一个调试能力存在。

我们在实际使用中IOCanary只监控了主线程的IO读写操作,并不足矣帮助我们去定位项目内的所有IO读写操作,所以我们队其进行了二次开发操作。

  1. 去除掉线程判断逻辑

  2. 把IO的堆栈从close,变更到open操作中

  3. 在java层汇总所有的流写入操作,然后统一对写入大小进行计算。

移除主线程判断

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n47" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> ssize_t ProxyWriteChk(int fd, const void *buf, size_t count, size_t buf_size) {/*  if (!IsMainThread()) {return original_write_chk(fd, buf, count, buf_size);}*/int64_t start = GetTickCountMicros();ssize_t ret = original_write_chk(fd, buf, count, buf_size);long write_cost_us = GetTickCountMicros() - start;__android_log_print(ANDROID_LOG_DEBUG, kTag,"ProxyWrite fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, buf_size,ret,write_cost_us);iocanary::IOCanary::Get().OnWrite(fd, buf, count, ret, write_cost_us);return ret;}</pre>

io_canary_jni.cc的c++代码中,我们只要简单的把几个proxy方法中的线程检查逻辑屏蔽掉即可。这样就可以获取到所有线程下IO操作了。

堆栈打印

Matrix的IOCanary中,有个IOCanaryJniBridge,这个就是其中的jni调用的类。他还有另外一个功能,就是把hook到的IO操作中的堆栈进行转化。

首先内部定义了一个实体类,这个类在构造的时候会抛出一个异常,其实这个异常就是负责获取到当前IO操作的堆栈信息的。因为代码的调用顺序其实是会被收集在线程内部的,而这个构造则是在我们IO监控的Open方法内被执行的。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n52" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> private static final class JavaContext {private final String stack;private String threadName;private JavaContext() {stack = IOCanaryUtil.getThrowableStack(new Throwable());if (null != Thread.currentThread()) {threadName = Thread.currentThread().getName();}HasakiLog.i(TAG, "JavaContext:" + threadName);}}</pre>

Matrix的IOCanary是在一个流Close的时候才会将JavaContext对象上报,其中才会有内存的堆栈,但是我们在实际的测试中发现,在高版本的设备上xHook的IO close操作并没有被很好的触发,这块我真的不是特别擅长,所以我们在构造的时候就对堆栈进行了打印。

大小计算调整

由于实际开发中,我们碰到了很多设备由于Close函数没有触发,导致了IO监控数据不准确的问题。我们在write函数增加了额外的jni调用。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="c++" cid="n56" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// 声明 writeFrame方法static jmethodID kMethodIDWriteFrame;// 通过jni 获取到java 类的writeFrame方法kMethodIDWriteFrame = env->GetStaticMethodID(kJavaBridgeClass, "writeFrame","(Ljava/lang/String;Ljava/lang/String;)V");void writeFrame(int frame, long size) {JNIEnv *env = NULL;// 判断实例是否存在 kJvm->GetEnv((void **) &env, JNI_VERSION_1_6);if (env == NULL || !kInitSuc) {__android_log_print(ANDROID_LOG_ERROR, kTag, "writeFrame env null or kInitSuc:%d",kInitSuc);} else {// 将写入时间 和写入大小调用java方法记录__android_log_print(ANDROID_LOG_DEBUG, kTag,"writeFrame  size:%d frame:%d", size, frame);char charSize[256];char charFrame[256];sprintf(charSize, "%d", size);sprintf(charFrame, "%d", frame);jstring str1 = env->NewStringUTF(charFrame);jstring str2 = env->NewStringUTF(charSize);env->CallStaticVoidMethod(kJavaBridgeClass, kMethodIDWriteFrame, str1, str2);}}</pre>

我们在proxyWrite方法内进行了一部分改造,将所有的写入大小和时间等在java层进行汇总计算。由于写入放开了线程的限制,所以我们把这部分记录操作放在了一个Executors.newSingleThreadExecutor()中记录

总结

以上就是今天的所有内容,相信大家看完之后都会有着自己的收获,但是盲目的学习并不能让你的技术稳定的增长

我自荐一套 《完整的Android学习资料,以及一些视频课讲解 现在点击此链接即可免费获取

现在私信发送 “进阶” 或 “笔记” 即可免费获取

最后我想说:

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们

技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面

Android 架构师之路还很漫长,与君共勉

【黑科技】腾讯的 IOCanary 监控系统原理分析相关推荐

  1. 华为折叠x2是鸿蒙系统吗,华为发布折叠旗舰Mate X2:各种黑科技设计,率先升级鸿蒙系统...

    2月22日晚上8点,华为正式发布了自己新一代折叠旗舰手机--Mate X2,这款预热已久的手机终于出现在大众眼前.就如同华为早前宣传的一样,这款手机在设计上不但拥有各种黑科技,同时在价格上似乎也没有想 ...

  2. 监控之美——监控之美-监控系统选型分析及误区探讨

    朱政科 读完需要 29 分钟 速读仅需 10 分钟 本文摘自于朱政科撰写的<Prometheus 云原生监控:运维与开发实战>,重点介绍了在监控系统选型中应该考虑的问题. 上一期监控之美- ...

  3. [激光原理与应用-47]:《焊接质量检测》-4-普雷茨特激光焊接过程监控系统LWM分析

    目录 第1章 激光焊接过程监控系统LWM概述 第2章 产品特性与功能 2.1 生产相关的信息 2.2 原始信息检测 2.3 焊接质量分析信息 2.4 缺陷报告与生产控制 2.5 LWM给客户带来的好处 ...

  4. 网络化机房的绿色安全卫士——万联OMM网络化机房动力环境监控系统案例分析...

    随着网络业务的快速扩张,通信网络在金融行业的业务支撑系统中扮演着越来越重要的角色,由于大量的业务进行需要依赖联网实现,业务的拓展必然涉及机房的建设,而当机房计算机系统的数量与日俱增后,保障通信系统的安 ...

  5. 排污单位生产设施及污染治理设施用电(能)监控系统原理、作用、组成及功能

    一.用电监控原理 根据企业的生产和治理工艺,分别对产污设施和治污设施安装环保用电监控模块,利用传感技术实时收集设施用电监测数据,再将数据用无线方式传输到环保用电监控系统,实现对企业总用电量.生产设施用 ...

  6. 电气火灾监控系统技术分析

    1引言 根据所统计的数据来看,电气火灾在各类火灾中已经占据了主要的地位.从2011年到2016年为止,我们国家的电气火灾数量约为52.4万起,2017年电气引发的火灾也有7.4万起.我们可以看到,电气 ...

  7. 系统监控——监控系统选型分析及误区探讨

    本文摘自于朱政科撰写的<Prometheus 云原生监控:运维与开发实战>,重点介绍了在监控系统选型中应该考虑的问题.在本文中,你将会了解监控应用程序的黑盒和白盒方法,也会了解监控执行检查 ...

  8. 光电玻璃LED透明屏是黑科技?揭秘玻璃LED透明屏原理

    光电玻璃透明屏屏被广泛用于写字楼.办公室.护栏等玻璃的地方.并且可以根据特殊需求定制玻璃LED透明屏的颜色.个性化效果更好,变化多样,还来的广告效果也是空前的. 光电玻璃LED透明屏是黑科技? LED ...

  9. 智慧工地:“千里眼”视频远程监控系统案例分析

    一.行业背景 互联网宽带技术与数字化.网络化视频监控技术的发展,为远程监控提供了更加完善和专业的解决方案.建筑行业是一个安全隐患及风险较大的行业,如何监督施工现场管理.防范安全事故的发生,一直是政府管 ...

最新文章

  1. 自动机器学习:团队如何在自动学习项目中一起工作?
  2. STL的一些基本概念
  3. 【Java Web开发指南】Mybatis 中的延迟加载
  4. DataTable添加列和行的三种方法
  5. 流动python - 字符串KMP匹配
  6. python中的pandas怎么安装_如何优雅的安装Python的pandas?
  7. 数据仓库之电商数仓-- 3.3、电商数据仓库系统(DWT层)
  8. java 内存泄漏问题_JAVA内存泄漏问题处理方法经验总结
  9. linux内核内存映射实验报告,动手实践-Linux内存映射基础(上)
  10. 编译安装mysql5.7.24踩的坑
  11. POJ3264(分桶法)
  12. 数据库备分复制到另一台机器
  13. 华为网络-ensp实验
  14. 卡内基梅陇大学计算机学院,卡内基梅隆大学计算机学院
  15. 数显之家快讯:【SHIO世硕心语】2021年中国10大最赚钱的机会!
  16. 安卓机更新系统会卡吗_都说安卓手机用一两年就卡到不行,但知道这3招,同样可以用很久...
  17. 姜维拥兵10万 为何守不住刘备的半壁江山
  18. 双屏(Daul Monitor)很爽
  19. C#(pronounced: see sharp) 与 .NET
  20. hexo博客的备份和迁移

热门文章

  1. arcgis 合并名字相同的要素_ArcGIS中各种合并要素异同
  2. 微信营销的功能和作用
  3. 惠普服务器装Linux7系统,惠普DL580 G7服务器系统安装与环境部署
  4. 爱国者u盘linux驱动,爱国者u盘驱动下载-aigo爱国者u盘驱动下载电脑版-121软件园...
  5. 物联网IoT大挑战:计算电池寿命
  6. 语音算法:CE/MMI准则
  7. Java中用类名声明变量
  8. QQ中对话框图片的拉伸问题
  9. Win10专业版如何删除微软输入法
  10. 2022 新零售的转折与机会