Binder(三):客户端拿到的 BinderProxy,到底指向远端什么对象?

0 阅读11分钟

Binder(三):客户端拿到的 BinderProxy,到底指向远端什么对象?

前两篇已经完成:

第一篇:一次调用怎样跨进程并返回。
第二篇:方法、参数和返回值怎样变成 AIDL 协议。

现在剩下整个 Binder 对象模型最关键的问题:

客户端明明不可能直接持有另一个进程中的 Java/C++ 对象地址,为什么一个本地 BinderProxy 却能代表远端服务?

沿着这个代理逐层向下看,得到的不是远端地址,而是一组映射:

客户端持有的不是服务端对象地址,而是一条分层远端引用链。

Java:业务 Proxy → BinderProxy
Native:BinderProxy → BpBinder(handle)
Kernel:当前进程的 binder_ref(handle) → 服务端 binder_node
服务端:事务到达拥有进程后,通过 ptr/cookie 恢复 BBinder / JavaBBinder → Java Stub

BinderProxy 只是这条链在客户端 Java 层的入口。真正让“远端对象引用”成立的是:用户态代理、进程内 handle、驱动 binder_ref 和服务端 binder_node 共同维护的映射。

源码按导读第四节“系列统一基线”定位,驱动对象使用其中固定的 Android Common Kernel commit。

远端引用为什么不能是一段地址

地址只在所属进程里有意义

假设服务端创建一个已经实现全部 AIDL 方法的 Stub。下面一行是只强调“本地对象属于服务端地址空间”的伪代码,不是 BinderLab CalculatorService 的构造方式:

ICalculator.Stub calculatorBinder = ...;

这个 Java 对象、对应的 native JavaBBinder,以及它们使用的内存地址都属于服务端进程地址空间。

即使客户端得到一个数值相同的地址:

0x7a12340000

它在客户端地址空间也可能:

没有映射;
映射到完全无关的数据;
在下一次运行中变化;
不具备访问权限。

因此 Binder 不能把普通对象指针直接交给客户端。它必须建立一种“可跨进程引用、但不暴露远端内存”的对象模型。

三层对象映射

image.png

这张图按层次阅读:

第一行:业务接口对象;
第二行:Java Binder 端点;
第三行:Native Binder 对象;
第四行:Kernel 中的远端引用和本地实体。

不要把图里所有对象都简称为“Binder 代理”。它们的职责不同:

对象所在位置代表什么
ICalculator.Proxy引用持有进程 Java类型化业务接口代理
BinderProxy引用持有进程 Java通用远端 IBinder 代理
BpBinder引用持有进程 Native持有目标 handle 的 native 代理
binder_ref引用持有进程驱动上下文当前进程对远端 node 的引用
handle引用持有进程用户态当前进程查找 binder_ref 的 key
binder_node驱动node 拥有进程本地 Binder 实体的内核表示
BBinder / JavaBBindernode 拥有进程 Native本地 Binder 实体
AIDL Stubnode 拥有进程 Java接收事务并分发业务方法

从 BinderProxy 追到 handle

业务 Proxy 与 BinderProxy 不是同一个对象。沿下面这条链继续向 Native 和 Kernel 追踪:

ICalculator.Proxy
    ↓ mRemote.transact()
BinderProxy

用户态代理与驱动引用怎样接起来

BinderProxy 在 native 层持有 IBinder

Java BinderProxy 的 native 数据中保存一个 sp<IBinder>。Android 16 的 javaObjectForIBinder() 会区分两种情况:

如果 native IBinder 是本进程 JavaBBinder:
    返回原来的 Java Binder 对象。

否则:
    为这个 native IBinder 创建或复用 Java BinderProxy。

关键结构可以整理成:

BinderProxyNativeData* nativeData = new BinderProxyNativeData();
nativeData->mObject = val;  // sp<IBinder>

对 kernel Binder 远端端点来说,这个 IBinder 通常是 BpBinder

BpBinder::transact() 最终取出:

binderHandle()

交给 IPCThreadState::transact()

因此更准确的说法不是:

BinderProxy 指向服务端 Java 对象。

而是:

BinderProxy 持有一个 native 远端 IBinder;
对 kernel Binder 来说通常是 BpBinder;
BpBinder 用当前进程中的 handle 标识远端 Binder node。

BBinder / BpBinder:本地端点与远端代理

可以先把名字记成两个方向:

BBinder:本进程拥有的本地 Binder 实体。
BpBinder:当前进程持有的远端 Binder 代理。

Java 服务端 Binder 进入 native 后由 JavaBBinder 承接;JavaBBinder 继承本地 Binder 实体路径。远端端点进入 Java 时则包装为 BinderProxy,native 侧由 BpBinder 提供 transact()

这不是固定的“服务端类”和“客户端类”。同一个进程可以同时:

拥有本地 BBinder / JavaBBinder;
持有其他进程的 BpBinder;
对某笔事务是服务端;
对下一笔事务又是客户端。

binder_node 归属于端点拥有进程

驱动中的 binder_node 记录本地 Binder 实体的内核表示,其中关键关系包括:

node->proc
    这个 node 由哪个 binder_proc 承载

node->ptr / node->cookie
    事务到达拥有进程时,用于还原本地 BBinder 的用户态标识

node->refs
    哪些外部进程持有对它的 binder_ref

binder_node 不是 Java 对象本身,也不理解 AIDL 接口。它是驱动维护路由、引用和生命周期所需的端点表示。

binder_ref 归属于引用持有进程

驱动中的 binder_ref 同时保存:

ref->proc
    哪个进程持有这条引用

ref->node
    引用指向哪个服务端 binder_node

ref->data.desc
    用户态看到的 handle

源码注释直接把它描述为:

从 procA 指向 procB 中目标 node 的引用结构。

所以对象关系是:

客户端 binder_proc
    └─ binder_ref(desc = handle)
            └─ target binder_node
                    └─ 服务端 binder_proc

handle 只是当前进程的索引

handle 是某个客户端进程查找自己 binder_ref 的编号。

同一个服务端 binder_node

在客户端进程 A 中可能是 handle 3;
在客户端进程 C 中可能是 handle 7;
在 node 自己的服务端进程中不是靠这两个 handle 找本地对象。

驱动处理事务时会在“发送方进程的 ref 表”中解析 handle。Android Common Kernel 的 binder_transaction() 清楚表现了这个范围:

ref = binder_get_ref_olocked(
        proc,
        tr->target.handle,
        true);

target_node = binder_get_node_refs_for_txn(
        ref->node,
        &target_proc,
        &return_error);

这里的 proc 是发送方 binder_proc。因此 handle 不能脱离进程上下文解释,更不是全系统唯一对象 ID。

ProcessState 复用同一 handle 的 BpBinder

Native 用户态的 ProcessState::getStrongProxyForHandle() 会在进程级表中查 handle:

handle_entry* e = lookupHandleLocked(handle);

if (e->binder == nullptr || oldProxyCannotBePromoted) {
    sp<BpBinder> bp = BpBinder::create(handle);
    e->binder = bp.get();
}

这个缓存让同一进程中同一远端 handle 尽量复用 BpBinder 身份,而不是每次读取 Parcel 都创建毫无关联的新代理。

进一步到 Java,javaObjectForIBinder() 也会尝试复用仍存活的 BinderProxy

但“本地代理对象身份被复用”仍不等于“远端进程永远存活”。第八篇会解释这个生命周期差异。

引用可以继续传播,也可以反向调用

Binder 引用写进 Parcel 时会被转换

普通 int 可以复制其值,Binder 引用不能复制远端对象内存。概念上只需要知道:Parcel 会把 Binder 引用作为特殊对象记录,驱动再按接收进程视角传播对同一个 node 的引用。

传递本地 Binder:接收进程获得自己的远端引用;
转发远端 Binder:新接收进程获得指向同一 node 的自己的引用;
引用回到 node 拥有进程:可以恢复为本地端点语义。

BINDER_TYPE_BINDERBINDER_TYPE_HANDLE 与驱动转换函数统一放在第四篇“特殊对象转换”章节

接收方把新 handle 包回 BinderProxy

接收侧最终得到两类结果之一:

引用回到 node 拥有进程:恢复本地 Binder;
接收进程只持有远端引用:创建或复用 BinderProxy。

最后 IResultCallback.Stub.asInterface() 等类型化桥接函数,再把通用 IBinder 包成业务接口。具体 flattenBinder() / unflattenBinder() 与内核对象翻译仍由第四篇负责。

callback 会反转引用持有关系

Binder 的 client/server 是“一笔事务中的角色”,不是进程永久身份。

加入 callback:

interface IResultCallback {
    oneway void onResult(int requestId, int value);
}

interface ICalculator {
    void registerCallback(IResultCallback callback);
}

BinderLab 的异步 callback可以验证 Binder 引用传播和 client/server 角色反转,但不形成同步嵌套事务,也不能证明客户端原等待线程被复用;第六篇使用非 onewayISyncResultCallback 验证重入。

注册时:

App:
    ICalculator 的客户端
    IResultCallback 的服务端

远端进程:
    ICalculator 的服务端
    IResultCallback 的客户端

Callback 让客户端与服务端角色反转转存失败,建议直接上传图片文件

调用 registerCallback(callback) 时,App 把自己的本地 Binder 实体作为 Parcel 特殊对象发给远端。驱动为远端建立对这个 callback node 的引用。之后远端调用:

callback.onResult(requestId, 3);

方向就变成:

远端 callback Proxy
    → Binder Driver
    → App Binder 线程
    → IResultCallback.Stub.onTransact()

这说明 ServiceManager 不是获得 Binder 引用的唯一方式。找到根服务之后,更多 Binder 引用可以作为参数和返回值继续传播。

Binder 引用也是一项可传递能力

当一个进程持有 Binder 引用时,可以把它理解为:

当前进程获得了向这个 Binder node 发起事务尝试的能力。

但这不代表:

拥有服务端对象内存;
继承服务端进程权限;
自动通过业务权限检查;
能访问服务端所有内部状态。

这里要把“取得引用”和“使用引用”拆成两个授权面,否则很容易把能力模型误写成“一旦拿到就能调用”:

取得引用:
  ServiceManager 查询时的 find 权限;
  上游服务是否愿意把某个 Binder 引用作为参数或返回值交给当前调用方。

使用或继续传递引用:
  SELinux binder call / transfer 规则;
  服务端基于 calling UID、可用时的 calling SID、声明权限等做的授权;
  接口参数、对象状态、用户范围和业务规则校验。

因此,“持有引用”只说明本地已经有一个引用入口,可以尝试发起 transaction。事务能否通过传输层与 MAC 检查、服务端是否接受请求,仍由各层独立决定;不能把“拿到引用”缩写成“已经获准访问端点”。

真机证据:对象类型与 callback 方向

客户端拿到接口后打印:

Log.i(TAG, "calculator=" + calculator.getClass().getName());
Log.i(TAG, "binder=" + calculator.asBinder().getClass().getName());

服务端注册 callback 时打印:

@Override
public void registerCallback(IResultCallback callback) {
    Log.i(TAG, "callback=" + callback.getClass().getName());
    Log.i(TAG, "callbackBinder="
            + callback.asBinder().getClass().getName());
}

跨进程时应观察:

App 中 calculator 是 ICalculator$Stub$Proxy;
App 中 calculator.asBinder() 是 BinderProxy;
远端进程中 callback 是 IResultCallback$Stub$Proxy;
远端调用 callback 后,App 的 IResultCallback.Stub 在 App Binder 线程执行。

如果服务和客户端同进程,类型可能直接是本地 Stub,不能套用远端对象链。

API 36 async-callback 日志同时记录了正向 ICalculator$Stub$Proxy / BinderProxy 和反向 IResultCallback$Stub$Proxy / BinderProxy,并用 PID、TID、方向和 requestId 关联 App callback。它证明了正反向事务中的引用持有角色发生反转;Java class、对象相等或 hash 仍不能证明远端 binder_node 的身份、存活性或 generation。

错误排障:跨进程比较 handle 数字

错误排障记录:

进程 A 日志里目标 handle=3;
进程 C 日志里目标 handle=7;
所以它们不是同一个服务。

这个结论不成立,因为 handle 的作用域是“当前持有引用的进程”。

正确核对方式需要结合:

handle 属于哪个 binder_proc;
该 binder_ref 指向哪个 binder_node;
node 由哪个服务端 binder_proc 承载。

只比较两个进程中的整数 handle,没有跨进程对象身份意义。

BinderProxy 代表的是一条引用链

更准确的答案是:

BinderProxy 不直接指向服务端 Java 对象。

它在客户端 Java 层包装一个 native IBinder;
对 kernel Binder 远端端点,这个对象通常是 BpBinder;
BpBinder 持有当前进程中的 handle;
handle 对应当前进程的 binder_ref;
binder_ref 指向服务端 binder_node;
binder_node 不直接持有 Java/C++ 对象;事务交付到拥有进程时,接收侧通过 ptr/cookie 恢复本地 BBinder / JavaBBinder。

现在可以把“代理指向远端对象”改写成更精确的话:代理持有当前进程的远端引用入口,驱动再把这条引用解析到目标 node。至于一笔事务如何沿这张对象图准备 buffer、转换特殊对象并进入目标工作队列,就是下一层问题。

源码与官方文档