8、Binder节点与引用:binder_node结构、binder_ref结构、强引用与弱引用、死亡通知机制
好,我们继续深入Binder机制的核心地带。前面几章我们把Binder的整体架构和通信流程捋了一遍,今天要聊的这几个概念——binder_node、binder_ref、强引用弱引用、死亡通知——说白了就是Binder世界里“谁认识谁”、“谁依赖谁”、“谁挂了谁该知道”的那点事。
我个人习惯把Binder通信比作一个公司内部的人员协作系统。你想想看,一个服务端进程(比如系统服务)就像一个部门,它对外提供能力。但客户端进程不能直接拽着服务端的代码跑,它得通过一个“工单系统”来发起请求。这个工单系统,就是Binder驱动。而binder_node和binder_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的死亡通知机制就是干这个的。客户端可以注册一个死亡通知回调,当服务端进程死亡时,驱动会主动通知客户端。流程是这样的:
- 客户端调用
linkToDeath(),传入一个DeathRecipient对象。 - 驱动在对应的binder_ref上挂载一个死亡通知结构(binder_ref_death)。
- 当服务端进程死亡时,驱动遍历所有指向该node的binder_ref,找到那些注册了死亡通知的ref。
- 驱动向这些客户端进程发送一个
BR_DEAD_BINDER通知。 - 客户端收到通知后,驱动会调用之前注册的回调函数。
这里有个细节:死亡通知是一次性的。也就是说,驱动发送完通知后,就会把死亡通知结构从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_node,客户端通过binder_ref拿到句柄,binder_ref指向binder_node。当服务端死亡时,驱动通过binder_ref上注册的死亡通知,主动告知客户端。整个链路清晰明了。
8.6 小结
好了,这一章的内容就这些。总结几个关键点:
- binder_node是服务端实体的内核代表,一个服务端BBinder对应一个node。
- binder_ref是客户端对服务端的引用,每个客户端进程内通过句柄(desc)唯一标识。
- 强引用阻止node销毁,弱引用不阻止。IPC调用时必须持有强引用。
- 死亡通知是驱动主动通知客户端服务端已死的机制,一次性的,需要重新注册。
我个人觉得,理解这几个结构体之间的关系,是掌握Binder安全机制的基础。很多权限提升漏洞,本质上就是利用了引用计数管理不当或者死亡通知处理不当,让攻击者能够劫持Binder通信或者绕过权限检查。后面几章我们会具体分析这些漏洞案例。
公众号:蓝海资料掘金营,微信deep3321