8、Binder节点与引用:binder_node结构、binder_ref结构、强引用与弱引用、死亡通知机制

好,我们继续深入Binder机制的核心地带。前面几章我们把Binder的整体架构和通信流程捋了一遍,今天要聊的这几个概念——binder_nodebinder_ref、强引用弱引用、死亡通知——说白了就是Binder世界里“谁认识谁”、“谁依赖谁”、“谁挂了谁该知道”的那点事。

我个人习惯把Binder通信比作一个公司内部的人员协作系统。你想想看,一个服务端进程(比如系统服务)就像一个部门,它对外提供能力。但客户端进程不能直接拽着服务端的代码跑,它得通过一个“工单系统”来发起请求。这个工单系统,就是Binder驱动。而binder_nodebinder_ref,就是驱动里维护的两张核心表:一张是“服务名录”,一张是“客户通讯录”。

8.1 binder_node:服务端的“身份牌”

每个向Binder驱动注册了的服务端实体,在驱动层都会对应一个binder_node结构体。你可以把它理解成服务端进程在Binder世界里的“身份证”。

这个结构体里记录了什么?我挑几个关键的说说:

  • proc:指向所属进程的binder_proc结构。说白了就是“这服务是谁家的”。
  • ptr:服务端本地持有的Binder对象指针。在用户空间,服务端通过BBinder对象提供服务,这个ptr就指向那个对象的地址。
  • cookie:附加数据,通常是服务端用来识别具体服务实例的。
  • refs:一个链表,挂载了所有指向这个node的引用(binder_ref)。
  • 强弱引用计数:internal_force_refs和local_force_refs,用来控制node的生命周期。

嗯,这里要注意一点:binder_node的生命周期是跟着服务端进程走的。服务端进程挂了,或者主动销毁了Binder对象,驱动才会清理对应的node。但这里有个坑——如果还有客户端持有对这个node的引用,驱动不会立刻销毁node,它会等所有引用都释放了再动手。这就是引用计数的作用。

核心要点:binder_node是服务端实体的内核态代表。一个服务端BBinder对象,在驱动层有且仅有一个binder_node与之对应。

8.2 binder_ref:客户端的“通讯录条目”

客户端要调用服务端,它不能直接操作binder_node。客户端拿到的其实是一个整数值——Binder句柄(handle)。这个handle在驱动层对应的,就是binder_ref结构体。

我打个比方:binder_node是公司的总机号码,binder_ref就是客户手机里存的“张三-137xxxx”这个联系人条目。每个客户端进程,都会为它要访问的每个服务端实体,维护一个binder_ref。

binder_ref里最重要的字段:

  • node:指向它对应的binder_node。这是关键关联。
  • desc:就是客户端看到的句柄值(handle)。在客户端进程内,这个值是唯一的。
  • strong_refs / weak_refs:当前客户端对这个node持有的强引用和弱引用计数。
  • death:指向死亡通知结构的指针。后面会细说。

我在项目中遇到过一个问题:某个系统服务频繁重启,结果客户端进程里的Binder调用时不时返回DEAD_BINDER错误。查了半天,发现是客户端在服务重启后没有重新获取Binder引用,而是拿着旧的handle继续用。旧handle对应的binder_ref虽然还在,但指向的binder_node已经标记为死亡了。这就是引用管理没做好的典型后果。

8.3 强引用与弱引用:谁在“持有”谁?

这两个概念很多人容易搞混。我直接说结论:

  • 强引用(Strong Ref):表示“我在用你,你不能走”。只要有一个客户端持有某个binder_node的强引用,服务端的Binder对象就不会被销毁。
  • 弱引用(Weak Ref):表示“我知道你,但我不拦着你走”。弱引用不会阻止服务端Binder对象的销毁,但可以在对象还活着的时候,尝试升级为强引用。

为什么要区分这两种引用?说白了就是为了解决生命周期管理的难题。你想想看,如果所有客户端都只持有强引用,那服务端想退出都退不了——因为总有人拽着它。但如果只用弱引用,服务端可能在你调用到一半的时候就被销毁了。

Binder的解决方案是:客户端发起IPC调用时,必须持有强引用。平时可以只持有弱引用,等到真要调用服务了,再尝试把弱引用升级为强引用。如果升级失败,说明服务端已经挂了,客户端就知道该重新连接了。

避坑指南:我曾经在一个项目中,为了省事,让客户端一直持有服务端的强引用。结果服务端需要热升级时,发现进程怎么也杀不掉——因为客户端进程还活着,强引用计数不为零,驱动不让销毁node。后来改成弱引用+按需升级,问题就解决了。

驱动层是怎么维护这些计数的?看一段伪代码你就明白了:

// 驱动层处理BC_INCREFS(增加弱引用)和BC_ACQUIRE(增加强引用)
static int binder_inc_ref_for_node(struct binder_proc *proc,
                                    struct binder_node *node,
                                    bool strong, ...)
{
    struct binder_ref *ref;
    
    // 先找这个客户端是否已经有这个node的ref
    ref = binder_get_ref_for_node(proc, node);
    if (!ref) {
        // 没有就新建一个
        ref = kzalloc(sizeof(*ref), GFP_KERNEL);
        ref->node = node;
        // 分配一个唯一的句柄值
        ref->desc = proc->next_desc++;
        hlist_add_head(&ref->node_entry, &node->refs);
    }
    
    if (strong) {
        ref->strong_refs++;
        node->internal_force_refs++; // 防止node被销毁
    } else {
        ref->weak_refs++;
    }
    
    return ref->desc; // 返回句柄给客户端
}

你看,驱动层通过internal_force_refs这个计数器,确保只要还有强引用存在,binder_node就不会被释放。弱引用则只影响binder_ref本身的生命周期,不影响node。

8.4 死亡通知机制:当服务端“猝死”时

这是Binder里一个非常实用的设计。你想,客户端拿着服务端的Binder引用,正调得开心呢,突然服务端进程崩溃了。这时候客户端如果继续调用,就会收到DEAD_BINDER错误。但问题是——客户端怎么知道服务端已经死了?难道要每次调用都去检查一下?

Binder的死亡通知机制就是干这个的。客户端可以注册一个死亡通知回调,当服务端进程死亡时,驱动会主动通知客户端。流程是这样的:

  1. 客户端调用linkToDeath(),传入一个DeathRecipient对象。
  2. 驱动在对应的binder_ref上挂载一个死亡通知结构(binder_ref_death)。
  3. 当服务端进程死亡时,驱动遍历所有指向该node的binder_ref,找到那些注册了死亡通知的ref。
  4. 驱动向这些客户端进程发送一个BR_DEAD_BINDER通知。
  5. 客户端收到通知后,驱动会调用之前注册的回调函数。

这里有个细节:死亡通知是一次性的。也就是说,驱动发送完通知后,就会把死亡通知结构从binder_ref上移除。如果客户端还想继续监听,需要重新注册。

注意事项:死亡通知回调是在Binder线程池中执行的,不是在你注册回调的那个线程。所以回调里不能做耗时操作,也不能直接持有锁去调用Binder方法——否则很容易死锁。我见过好几个线上崩溃都是因为这个原因。

驱动层处理死亡通知的核心代码大致是这样的:

// 当服务端进程死亡时,驱动调用此函数
static void binder_node_release(struct binder_node *node, ...)
{
    struct binder_ref *ref;
    struct binder_ref_death *death;
    
    // 遍历所有指向这个node的ref
    list_for_each_entry(ref, &node->refs, node_entry) {
        death = ref->death;
        if (death) {
            // 向客户端发送死亡通知
            binder_send_death_notification(ref->proc, death);
            ref->death = NULL; // 一次性,发完就清掉
        }
    }
    
    // 清理node本身
    // ...
}

嗯,这里要特别提一下:死亡通知的发送是异步的。驱动只是把通知消息放到客户端进程的事务队列里,客户端Binder线程池后续会处理。所以从服务端死亡到客户端收到通知,中间是有时间差的。在这个时间差内,客户端如果发起Binder调用,还是会失败。

8.5 知识结构总览

说了这么多,我画了一张图帮你把这几者的关系理清楚:

Binder节点与引用关系图 服务端进程 BBinder对象 (提供具体服务) 注册 binder_node ptr / cookie refs链表 (驱动层唯一实体) 被引用 binder_ref desc (句柄值) strong/weak refs 返回句柄 客户端进程 BpBinder(handle) (通过句柄调用) 死亡通知 linkToDeath / BR_DEAD_BINDER 注册 通知

从这张图你可以看到:服务端注册后产生binder_node,客户端通过binder_ref拿到句柄,binder_ref指向binder_node。当服务端死亡时,驱动通过binder_ref上注册的死亡通知,主动告知客户端。整个链路清晰明了。

8.6 小结

好了,这一章的内容就这些。总结几个关键点:

  • binder_node是服务端实体的内核代表,一个服务端BBinder对应一个node。
  • binder_ref是客户端对服务端的引用,每个客户端进程内通过句柄(desc)唯一标识。
  • 强引用阻止node销毁,弱引用不阻止。IPC调用时必须持有强引用。
  • 死亡通知是驱动主动通知客户端服务端已死的机制,一次性的,需要重新注册。

我个人觉得,理解这几个结构体之间的关系,是掌握Binder安全机制的基础。很多权限提升漏洞,本质上就是利用了引用计数管理不当或者死亡通知处理不当,让攻击者能够劫持Binder通信或者绕过权限检查。后面几章我们会具体分析这些漏洞案例。


公众号:蓝海资料掘金营,微信deep3321