在 Android 使用开发中,Binder 能够说是运用最为遍及的 IPC 机制了。咱们考虑监控 Binder 这一 IPC 机制,一般是出于以下两个意图:
- 卡顿优化:IPC 流程完好链路较长,且依靠于其他进程,耗时不可控,而 Binder 调用本身一般又是以 RPC 形式对外供给才干的,使得咱们在运用时更简略疏忽其 IPC 的实质。总的来说,主线程阻塞在同步 Binder 调用上是导致使用卡顿的一大典型原因。
- 溃散优化:Binder 调用过程中可能呈现各类异常状况,典型的是 Binder 缓冲区耗尽的状况( 经典 TransactionTooLargeException )。Binder 的缓冲区巨细大约为 1M( 注意到这儿的缓冲区是最早 mmap 出来进程内大局同享的,而非单次 Binder 调用的约束 ),而异步调用( oneway )的状况更是被约束只能运用缓冲区的一半巨细( 约 512K )。
考虑只监控某些个体系服务的状况,得益于 ServiceManager 和 AIDL 的规划,咱们能够简略根据动态署理替换当前进程对应的 Proxy 方针来完结监控;而要完结进程内大局 Binder 监控的话,咱们需求考虑怎么阻拦到 Binder 调用通用的 transact 办法。
根据 Binder.ProxyTransactListener
注意到 Android 10 上体系引进了 Binder.ProxyTransactListener,在 Binder 调用前后( BinderProxy 的 transactNative 办法内部 )会触发回调。从提交记载来看,ProxyTransactListener 引进的意图之一就在于支撑 SystemUI 监控主线程的 Binder 调用。
美中不足的当地在于,ProxyTransactListener 和相应的设置接口都是 hide 的,且 BinderProxy 的类特点 sTransactListener 在 hidden api 名单中。不过到目前为止,咱们始终是有稳定的计划能够绕过 hidden api 的约束的,因而咱们能够根据动态署理创立一个 ProxyTransactListener 实例设置给 BinderProxy,来完结对进程内 Java 的 Binder 调用的大局监控。
这儿稍微提一下 hidden api 的绕过计划,在 Android 11 体系禁掉元反射之后,一个简略的计划是先创立一个 Native 线程再 Attach 拿到 JNIEnv,就能够在该线程内正常运用
VMRuntime.getRuntime().setHiddenApiExemptions(new String[]{"L"});
来完结大局 hidden api 加白。原理是体系关于根据 JNI 拜访 Java API 的状况,在回溯 Java 仓库找不到 caller 的状况,会信任该次调用不做 hidden api 的阻拦,详细逻辑见 GetJniAccessContext。因而咱们能够经过创立 Native 线程再 AttachCurrentThread 拜访 JNI 接口的方法来构造没有 Java caller 的状况( 这也是 Native 线程 AttachCurrentThread 无法拜访使用类的原因,没有 caller 就找不到可用的 ClassLoader )。比较有意思的当地是实践上官方早就意识到了这类 hidden api 后门的存在,但由于改动风险太大一类的原因一直没有合入约束逻辑,相似的讨论能够参考 Don’t trust unknown caller when accessing hidden API。
这一计划完结简略,兼容性好,首要的毛病在于:
- 只支撑 Android 10 及以上版本,不过现状 Android 10 以下的设备占比已经不高了。
- ProxyTransactListener 的接口规划上不带 data 参数( Binder 调用的传入数据 ),咱们也就无法做传输数据巨细的计算。此外,关于多进程使用来说,实践上可能会以一致的 AIDL 方针作为通信通道封装一层屏蔽了匿名 Binder 方针传递和 AIDL 模版代码的 IPC 结构,实践 IPC 调用的方针逻辑则以一致的调用约定封装在 data 参数中。这种状况下,只有拿到 data 参数( 将 IPC 调用一致放到线程池中履行的状况,仓库没有意义 )咱们才干实践承认 IPC 调用的真正方针逻辑。
JNI Hook BinderProxy.transactNative
实践上 Java 的 Binder 调用总是会走到 BinderProxy 的 JNI 办法 transactNative,咱们能够根据 JNI Hook 来 hook transactNative,完结全版本的进程内 Java 的 Binder 调用的大局监控,也能够拿到 Binder 调用的完好参数和返回结果。
这儿稍微提一下 JNI Hook,JNI Hook 根据 JNI 边界 hook Java JNI 办法对应的 Native 函数完结,详细完结上 hack 点少,稳定性较好,算得上是线上比较常用的一类通用 Native Hook 计划。大体上讲,JNI Hook 完结上分为两步,找到原 Native 函数和替换该 Native 函数。
- Native 函数的替换比较简略,咱们经过调用 JNI 的 RegisterNatives 接口就能够完结 Native 函数的覆盖。( 注意到如果原始 JNI 办法也是经过 RegisterNatives 注册的,咱们需求确保 JNI Hook 的 RegisterNatives 履行在后 )
- 找到原 Native 函数则稍微杂乱些,并且咱们总是需求依靠原 Native 函数已经先行注册才干找到该 Native 函数。
- 一个可行的计划是手动完结一个 JNI 办法,用来计算实践 ArtMethod 方针( 即 Java 办法在 art 中的实践表示 )中存放 Native 函数的特点的偏移。得到这个偏移之后即能够根据被 Hook JNI 办法的 ArtMethod 方针拿到原 Native 函数。怎么拿到 ArtMethod 方针?实践上,在 Android 11 曾经,jmethodID 就是 ArtMethod 指针,在 Android 11 之后,jmethodID 默许变成了直接引用,但咱们依然能够经过 Java Method 方针的 artMethod 特点拿到 ArtMethod 指针。详细介绍可参考一种通用超简略的 Android Java Native 办法 Hook,无需依靠 Hook 结构。
- 另一个可行的计划是能够根据 art 的内部函数 GetNativeMethods 来直接查询得到原 Native 函数。GetNativeMethods 几个函数是 art 用于支撑 NativeBridge 的,稳定性上也有所确保。NativeBridge 详细介绍可参考用于 Android ART 虚拟机 JNI 调用的 NativeBridge 介绍。
详细到 JNI Hook BinderProxy.transactNative,实践在跑到使用的榜首行事务代码( Application 的 attachBaseContext )之前,就已经有 Java 的 Binder 调用发生,因而咱们底子不需求手动触发 Binder 调用来确保 BinderProxy.transactNative 的 Native 函数注册。另外,注意到 BinderProxy 的 transactNative 也是 hidden api,这儿也需求先行绕过 hidden api 的约束。
Hook BinderProxy.transactNative 的计划能够很好地满足监控进程内大局 Java Binder 调用的需求,但却监控不到 Native 的 Binder 调用。注意到这儿的 Java/Native Binder 调用的差异在于 IPC 通信逻辑的完结方位,而非实践事务逻辑的完结方位。典型的如 MediaCodec 一类的音视频接口,实践的 Binder 调用封装都完结在 Native 层,咱们运用 Java 调用这些接口,经过 BinderProxy.transactNative 也无法监控到实践的 Binder 调用。要完结包括 Native 的大局 Binder 调用监控,咱们需求考虑 Hook 更下一层的 Native 的 transact 函数。
PLT Hook BpBinder::transact
和 Java 层的 Binder 接口规划相似,Native 层 Client 端发起的 Binder 调用,总是会走到 libbinder.so 中 BpBinder 的 transact 函数。注意到 BpBinder 的 transact 函数是一个导出的虚函数,并且运用上总是根据基类 IBinder 指针做动态绑定调用( 也就是说,其他 so 总是根据 BpBinder 的虚函数表来调用 BpBinder::transact,而不是直接依靠 BpBinder::transact 这一符号,而 BpBinder 的虚函数表在 libbinder.so 内部 ),因而咱们直接 PLT Hook libbinder.so 关于 BpBinder::transact 的调用即可。
详细看下 BpBinder::transact 的函数声明:
// NOLINTNEXTLINE(google-default-arguments)
virtual status_t transact( uint32_t code,
const Parcel& data,
Parcel* reply,
uint32_t flags = 0) final;
其中,status_t 实践上仅仅 int32_t 的别名,但 Parcel 则不是 NDK 露出的接口,咱们没有途径拿到绝对稳定的 Parcel 方针的布局,好在 transact 函数关于 Parcel 的运用是根据引用和指针的( 引用在汇编层面的完结和指针相似 ),咱们不需求依靠 Parcel 方针的布局也能够完结一个 transact 的替代函数。
在成功阻拦到 BpBinder::transact 的调用之后,咱们还需求考虑怎么根据 transact 的调用参数和返回值来获取到咱们需求的信息。
关于 Binder 方针( 即 transact 的隐含调用参数 this 指针 )本身,咱们一般会重视它的 descriptor( 再结合 code 参数能够定位到实践 IPC 调用的方针逻辑 ),这儿咱们直接调用导出接口 BpBinder::getInterfaceDescriptor 即可。
virtual const String16& getInterfaceDescriptor() const;
比较费事的是 String16 也不是 NDK 露出的接口,并且它用来转成 char16_t* 字符创的函数完结是内联的,
inline const char16_t* string() const;
咱们只能从头 hardcode 声明一个相似的 String16 类来做强转。好在从体系源码看,String16 的方针布局比较简略且稳定,只有一个 const char16_t* 类型的私有特点 mString,并且不存在虚函数。相似这样:
class String16 {
public:
[[nodiscard]] inline const char16_t *string() const;
private:
const char16_t* mString;
};
inline const char16_t* String16::string() const {
return mString;
}
拿到 String16 对应的 char16_t* 字符串之后,咱们直接在回调 Java 时用 JNI 接口将其转为 jstring 即可。
另一个常用的信息是 data 的数据巨细。咱们能够直接调用导出接口 Parcel::dataSize 来获取。注意到 transact 函数 的 data 参数是 Parcel 的引用,咱们直接声明一个空类来承接 data 参数,再对拿到的 data 取址,让编译器能够正常完结引用到指针的转换即可。相似这样:
class Parcel {};
// size_t dataSize() const;
typedef size_t(*ParcelDataSize)(const Parcel *);
// virtual status_t transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) = 0;
status_t HijackedTransact(void *thiz, uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags);
ParcelDataSize g_parcel_data_size = nullptr;
auto data_size = g_parcel_data_size(&data);
此外,注意到关于 Java 的 Binder 调用而言,会在 BinderProxy.transactNative 的内部再调用到 BpBinder::transact,咱们能够结合 JNI Hook 和 PLT Hook 两个计划,对 Java 的 Binder 调用,根据 JNI Hook 拿到完好的 Java 参数,便利咱们在 Java 回调中直接根据 Java 参数做进一步处理。
One More Thing
阻拦到 Binder 调用仅仅监控的榜首步,更为重要的是在这个基础上如何做数据处理来发现和定位问题。
前面提到的两类经典问题:IPC 耗时卡顿和传输数据过大溃散,能够经过前后打点计算 transact 耗时以及调用前获取传输数据巨细的方法来挖掘。
定位问题上,仓库和当次 Binder 调用的 descriptor 和 code 是比较有价值的信息。
本文提到的全体计划,我根据 xDL 和 ByteHook 给出了一个基本完结 AndroidBinderMonitor,供参考。