Binder跨进程Mock回放从入门到实战 - 第4节
Binder协议与数据格式:从BC到事务结构
Binder通信的本质,就是客户端和驱动之间用一堆固定编号的协议码在对话。我刚开始啃源码时,被BC_XXX和BR_XXX搞得眼花缭乱,后来发现其实就两套:一套是用户态发给驱动的(BC),一套是驱动回给用户态的(BR)。打个比方,BC就像你下订单,BR就是快递员送上门。
BC协议:谁在发号施令?
客户端(或者服务端)要干活,得先通过ioctl()往Binder驱动里塞一个命令。这个命令就叫做BC码。所有的BC码都是整数常量,定义在linux/binder.h里。
| BC码 | 值 | 含义 |
|---|---|---|
| BC_TRANSACTION | 0x00 | 发起一次事务(调用或返回) |
| BC_REPLY | 0x01 | 回复上一次调用 |
| BC_FREE_BUFFER | 0x02 | 通知驱动释放一块映射缓冲区 |
| BC_INCREFS | 0x03 | 增加弱引用计数 |
| BC_ACQUIRE | 0x04 | 增加强引用计数 |
| BC_RELEASE | 0x05 | 减少强引用计数 |
| BC_DECREFS | 0x06 | 减少弱引用计数 |
| BC_REGISTER_LOOPER | 0x08 | 注册一个线程到Binder线程池 |
我个人习惯在分析Binder日志时,先过滤BC_TRANSACTION和BC_REPLY,因为90%的性能问题都跟事务有关。其他BC码多半是生命周期管理,一般不会踩坑。
BR协议:驱动如何回应?
驱动处理完BC后,会把结果塞到一块共享内存里,然后唤醒用户态的线程去读。读到的就是BR码。BR码是驱动发给用户态的“通知”。
| BR码 | 值 | 含义 |
|---|---|---|
| BR_TRANSACTION | 0x00 | 驱动通知:有事务需要处理 |
| BR_REPLY | 0x01 | 驱动返回:之前发的事务的回复 |
| BR_ACQUIRE | 0x02 | 驱动要求:增加强引用 |
| BR_RELEASE | 0x03 | 驱动要求:减少强引用 |
| BR_INCREFS | 0x04 | 驱动要求:增加弱引用 |
| BR_DECREFS | 0x05 | 驱动要求:减少弱引用 |
| BR_SPAWN_LOOPER | 0x07 | 驱动建议:再创建一个线程 |
| BR_DEAD_BINDER | 0x08 | 通知:某个Binder节点死亡 |
提示:BR_TRANSACTION和BR_REPLY一个是“你要处理请求”,一个是“你之前的请求有结果了”。在Mock回放时,我们主要盯的就是这两个BR码。
绑定数据:flat_binder_object
事务里除了整数,还可能传递Binder对象(比如IBinder引用)。驱动用struct flat_binder_object来描述这些对象。结构体如下:
struct flat_binder_object {
__u32 type; // BINDER_TYPE_BINDER, BINDER_TYPE_HANDLE, ...
__u32 flags;
union {
__u32 binder; // 本地Binder指针
__u32 handle; // 远程句柄
};
__u32 cookie; // 附加数据
};
我遇到过一个坑:type字段用的是__u32,但驱动内核对齐要求很高。如果你在用户态手动拼接这个结构体,没按8字节对齐,驱动会直接返回 -EINVAL。我曾经在Mock工具里踩过这个雷,后来统一用memcpy到缓存区并确保偏移量是8的倍数才搞定。
深入Binder事务结构:一个完整的调用包
把BC、BR和flat_binder_object串起来,就是一次完整的Binder事务。驱动用struct binder_transaction_data来承载所有信息。这个结构体是Binder协议里最核心的“信封”。
binder_transaction_data 结构解析
struct binder_transaction_data {
// 下面两组表示谁在说话
binder_size_t target_handle; // 目标句柄
binder_size_t cookie; // 目标本地指针(仅用于本地)
__u32 code; // 操作码 (如 FOO_TRANSACTION)
__u32 flags; // 标志位(如TF_ONE_WAY)
// 数据段描述
binder_size_t data_size; // data.ptr.buffer 中有效数据长度
binder_size_t offsets_size; // data.ptr.offsets 中偏移数组长度
union {
struct {
binder_uintptr_t buffer; // 指向数据缓存区
binder_uintptr_t offsets; // 指向偏移数组(每个偏移指向一个flat_binder_object)
} ptr;
__u8 buf[8]; // 当数据很小且不需要跨进程拷贝时,直接内联
} data;
};
说白了这个结构体干了三件事:告诉驱动“发给谁”、“干什么事”、“带什么数据”。其中offsets数组是关键——它记录数据区里哪些位置嵌有Binder对象,驱动需要根据这些偏移来解析和转换句柄。
数据区编排示例
假设我们要调用一个接口:ping(in String msg, IBinder callback)。那么数据区长这样:
缓冲区地址 内容
0x0000 - 序列化后的字符串 "hello" (6字节)
0x0006 - padding 到 8 字节对齐(2字节填充)
0x0008 - flat_binder_object (type=BINDER_TYPE_HANDLE, handle=某个句柄)
...
offsets[] = { 8 } // 告诉驱动:偏移8的地方有一个Binder对象要处理
注意:offsets数组的每一项必须是8字节对齐的偏移值,且指向的位置必须是一个完整的flat_binder_object。如果偏移没对齐或者指向的数据格式错误,驱动在 binder_translate_binder 内会直接拒绝处理。我在测试时吃过这个亏:少算了padding,结果驱动返回-ENOMEM。
事务流程与协议码的配合
讲完结构,我们看一次完整调用是怎么走的:
- 客户端发送:构造一个
binder_transaction_data,填充code=FOO_TRANSACTION、数据区、偏移数组。然后调用ioctl(BC_TRANSACTION, &data)。 - 驱动接收:解析BC码,根据
target_handle找到服务端节点。把数据区从客户端的虚拟地址映射到内核空间。 - 对象转换:遍历
offsets,把每个flat_binder_object里的handle替换成服务端能识别的句柄(如果是BINDER_TYPE_BINDER则分配新句柄)。 - 唤醒服务端:驱动把处理后的
binder_transaction_data通过共享内存推给服务端,并发送BR_TRANSACTION唤醒服务端的Binder线程。 - 服务端回复:服务端处理完,用
BC_REPLY把结果返回,驱动再用BR_REPLY唤醒客户端。
这个流程里,offsets和对象转换是性能敏感点。我记得在优化高并发场景时,发现驱动在遍历偏移数组时用了for循环逐个翻译,如果offtset数量很大(比如一次传几百个Binder对象),就会卡在binder_translate_binder里。后来我们改成了尽量减少Binder对象传递,全部用基本类型替代。
实战避坑:协议解析中的常见错误
搞了这么多年Binder调试,我总结了几个Protocol级别容易翻车的地方:
- BC与BR不匹配:如果客户端发了BC_TRANSACTION但等来的是BR_ACQUIRE,说明驱动认为需要先处理引用计数。这种情况多出现在服务端刚死或死循环中。我在Mock回放时必须严格按序记录和播放,否则驱动会直接挂起线程。
- data_size和offsets_size不匹配:
offsets_size是byte数,不是个数。记得offsets_size / sizeof(binder_size_t)才是偏移数量。我见过有人误以为offsets_size是偏移个数导致越界。 - 内联buf的使用限制:当数据总大小≤8字节且没有Binder对象时,驱动会把数据直接放进
data.buf[8]而不走映射内存。这在性能上有好处,但Mock捕获时要注意区分data.ptr和data.buf,如果按固定offset解析会读错。
核心思想:BC/BR是协议骨架,binder_transaction_data是数据信封,flat_binder_object是信封里的特制内容。做Mock回放,就是完整录下这个信封的header+body+offsets,然后在回放时原样重建。哪怕只是一字节的偏移偏差,驱动都会拒绝。所以建议大家写工具时,先抓一组最简单的无参数调用,验证信封结构无误后再处理复杂对象。