Ananbox虚拟系统中Binder通信问题的解决方案
前言
这一节我还没想好好名字,叫虚拟Binder的话,我觉得自己也没有完全写一个用户层的binder设备出来;叫虚拟binder组件的实现的话,又没能像Google那样在新的系统试图通过Unix Socket来替换掉当前binder内核驱动的binder进程间通信实现。个人评价的话,其实这个东西很简单,我觉得可能叫做binder on binder shim也许是合适的,就是一个垫片,一个略微hacked的实现。
问题描述
如何在binder内核驱动限制只有一个用户层且在用户模式下再跑起来一个service manager,且其他的进程能够拿到这个service manager?
解决方案简介
虚拟系统的service manager向宿主机app的BroadcastReceiver发送自己的binder完成注册,之后虚拟系统中进程首先从宿主机servicemanager拿到宿主机的activity_manager,发送包含接受虚拟service manager回调binder的broadcast,broadcastreceiver会通过这个binder发送虚拟service manager的binder,然后虚拟系统中的进程通过ioctl等待回调,如果回调收不到binder,不停重试直到拿到虚拟service manager,后续的通信流程还是正常系统的流程。
Binder机制简要介绍
android中的进程间通信主要依赖binder实现。两个进程想要进行通信,是不能像tcp一样拿到地址端口直接连接的,必须要经由至少一个系统中存在的binder进行。等等,这不是鸡生蛋蛋生鸡的循环吗,进程间想要通信必须借助至少一个第三方binder进行,那系统最早的两个进程是怎么通信的?这里binder驱动做了一个定义:每个binder驱动有且只有一个service manager,他的handle=0,这样两个进程如果要通信的话(最简单的情况),其中一端(进程A)向service mananger注册(与handle=0)的进程通信,将自己的binder发送给service manager,然后另一个进程B向service manager请求获取这个进程的binder。在进程B拿到进程A的binder后就可以进行通信了。
也许有读者会有疑问,这里进程A发送binder和service manager发送binder好抽象啊,这里发送binder究竟进行什么?真的是发送binder吗,其实不完全是。这里发送binder的话我目前的理解是(以service mananger向进程B发送binder为例):内核驱动为每一个进程维护了他目标通信进程的记录的数据结构(红黑树),这个数据结构里记录了对应目标进程的信息(可以简单粗暴的理解为是目标进程的地址和端口,只是功能目标上相似,用于在内核驱动总的进程树找到对应的进程并向他发送消息),内核驱动在看到一个binder_obj类型的消息后,会在发送进程的红黑树中添加要通信进程的信息,得到一个handle(此时可以视作为进程AB建立了一个可通信的通道),也就是说b接收的binder实际接收的是handle。进程b拿到handle,后续通信过程中,binder驱动通过handle在这个红黑树里拿到proc_id,驱动再根据proc_id拿到目标进程的内存并拷贝数据。
上面实际上是我们能够借助BroadcastReceiver传递binder其中一个底层支持。
binder消息实际上是一个RPC,以handle区分提供服务的进程,以消息中的transaction_id指定进程上的什么服务。
Android四大组件中的service,content provider,broadcast receiver都是借助binder通信完成的。(因此也有人说android是一个消息系统?)
以上是通信的大致流程,部分细节不一定准确,建议感兴趣的读者可以阅读gityuan的博客,有十分详细的介绍和描述。
service manager是怎么注册的?
service manager启动的时候会发送一个become_context_manager进行注册,内核驱动会检查当前是否已经有service manager注册,没有的话就注册成功。
那我们也直接向已经注册的service manager注册行不行呢?
不行!
首先,我们虚拟系统最终目标是运行在用户层上,向service mananger注册服务只有系统用户以及授权的用户才可以,普通app进程来说做不到。
其次,假如我们以某种方式向service manager注册,但是虚拟系统服务的名字会和宿主机系统服务的名字冲突(不过改名字也可以emmm)。
除了改名字外,更好的方法是让虚拟系统service manager以一个另外的名字注册到宿主机的service mananger,这样我们只要跟宿主机的service manager通信,拿到虚拟主机的service manager,这样后续的通信过程其实是不变的,只需要对native层代码进行少量修改就可以完成。这也是我的方案最终实现的选择的方案的一部分,虽然不是直接从service manager获取。
一般app是如何利用binder通信的?
比如bindService实际上是从AMS拿到service的binder与之通信,设置callback就是把我的binder给到对端服务。
sendBroadcast则是向AMS发送broadcast,AMS查找注册的BroadcastReceiver,再向对应的BroadcastReceiver发送数据。ContentProvider同理。
因此,虚拟系统之于真实系统而言,是真实系统一个app里的一系列采用binder通信的普通用户进程。因此binder通信的实现方式在在Service,BroadcastReceiver和ContentProvider里选择。
此外,这里与直接和service mananger通信的区别在于,我们是透过service manager拿到AMS,再拿到对应service的binder。
同时我们也能将我们的binder发送给service,service也能给我们发送binder。
这就是虚拟系统中binder实现的关键,虚拟系统中的虚拟service manager可以视作这里的activity
如何让普通用户的Native进程使用binder通信
首先ndk并没有提供一套binder通信的API(当然在android api 29之后有了ndkbinder,但是关键的getActivityMananger也不是公开稳定的接口),也没有提供Parcel,这一Binder消息序列化与反序列化所需要的机制,更不用说Intent这类数据结构和其对应的序列化以及反序列化。其次,Service和BroadcastReceiver都需要与activity_manager通信,而activity_manager发送所需binder消息的代理类只有在java层的实现。
在网上找到了一个项目KeepAlive,他这个项目在ndk项目里打包了一个parcel库(Framework里的libbinder提取出来)解决了序列化的问题,而Intent可以在parcel库提供的基本数据结构上组装就行了。
自己打包一份parcel库的劣势也很明显,不同安卓版本parcel序列化和反序列格式上面存在一点差异,比如11开始对序列化后的消息头部多了一个校验字段,12比起11对binder_obj又加入了版本字段和额外的校验字段等。
为什么选择BroadcastReceiver?
也就是说,要实现用户native进程的binder通信,我们借助Service或BroadcastReceiver交换binder,但是需要我们构造传递给他们的Intent,而BroadcastReceiver构造相对简单,最终选择了BroadcastReceiver原因之一就是这里。
更重要的是看到shizuku的binder通信通样是BroadcastReceiver,在shizuku的issuse中提到,因为应用启动的独立进程和adb启动的进程没有绑定到AMS,没有context,bindService里其中一个参数是context