Binder跨进程Mock回放从入门到实战 - 第4节

Binder协议与数据格式:从BC到事务结构

Binder通信的本质,就是客户端和驱动之间用一堆固定编号的协议码在对话。我刚开始啃源码时,被BC_XXX和BR_XXX搞得眼花缭乱,后来发现其实就两套:一套是用户态发给驱动的(BC),一套是驱动回给用户态的(BR)。打个比方,BC就像你下订单,BR就是快递员送上门。

BC协议:谁在发号施令?

客户端(或者服务端)要干活,得先通过ioctl()往Binder驱动里塞一个命令。这个命令就叫做BC码。所有的BC码都是整数常量,定义在linux/binder.h里。

BC码 值 含义
BC_TRANSACTION0x00发起一次事务(调用或返回)
BC_REPLY0x01回复上一次调用
BC_FREE_BUFFER0x02通知驱动释放一块映射缓冲区
BC_INCREFS0x03增加弱引用计数
BC_ACQUIRE0x04增加强引用计数
BC_RELEASE0x05减少强引用计数
BC_DECREFS0x06减少弱引用计数
BC_REGISTER_LOOPER0x08注册一个线程到Binder线程池

我个人习惯在分析Binder日志时,先过滤BC_TRANSACTION和BC_REPLY,因为90%的性能问题都跟事务有关。其他BC码多半是生命周期管理,一般不会踩坑。

BR协议:驱动如何回应?

驱动处理完BC后,会把结果塞到一块共享内存里,然后唤醒用户态的线程去读。读到的就是BR码。BR码是驱动发给用户态的“通知”。

BR码值含义
BR_TRANSACTION0x00驱动通知:有事务需要处理
BR_REPLY0x01驱动返回:之前发的事务的回复
BR_ACQUIRE0x02驱动要求:增加强引用
BR_RELEASE0x03驱动要求:减少强引用
BR_INCREFS0x04驱动要求:增加弱引用
BR_DECREFS0x05驱动要求:减少弱引用
BR_SPAWN_LOOPER0x07驱动建议:再创建一个线程
BR_DEAD_BINDER0x08通知:某个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。

事务流程与协议码的配合

讲完结构,我们看一次完整调用是怎么走的:

  1. 客户端发送:构造一个binder_transaction_data,填充code=FOO_TRANSACTION、数据区、偏移数组。然后调用ioctl(BC_TRANSACTION, &data)。
  2. 驱动接收:解析BC码,根据target_handle找到服务端节点。把数据区从客户端的虚拟地址映射到内核空间。
  3. 对象转换:遍历offsets,把每个flat_binder_object里的handle替换成服务端能识别的句柄(如果是BINDER_TYPE_BINDER则分配新句柄)。
  4. 唤醒服务端:驱动把处理后的binder_transaction_data通过共享内存推给服务端,并发送BR_TRANSACTION唤醒服务端的Binder线程。
  5. 服务端回复:服务端处理完,用BC_REPLY把结果返回,驱动再用BR_REPLY唤醒客户端。

这个流程里,offsets和对象转换是性能敏感点。我记得在优化高并发场景时,发现驱动在遍历偏移数组时用了for循环逐个翻译,如果offtset数量很大(比如一次传几百个Binder对象),就会卡在binder_translate_binder里。后来我们改成了尽量减少Binder对象传递,全部用基本类型替代。

Binder协议与事务结构 用户态 客户端线程 发起BC_TRANSACTION 服务端线程 接收BR_TRANSACTION Binder驱动层 BC协议解析 binder_ioctl -> BC码分发 BR协议生成 binder_send_reply -> BR码 binder_transaction 核心处理 解析binder_transaction_data → 偏移遍历 → 扁平对象转换 数据缓冲区 (buffer) 序列化参数 + flat_binder_object 按偏移索引 偏移数组 (offsets) 每个偏移指向一个对象 必须8字节对齐 flat_binder_object type, flags, binder/handle 驱动翻译句柄 🔍 Mock回放关键点 捕获BC_TRANSACTION与BR_TRANSACTION中的binder_transaction_data 并按偏移结构解析对象,实现透明回放

实战避坑:协议解析中的常见错误

搞了这么多年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,然后在回放时原样重建。哪怕只是一字节的偏移偏差,驱动都会拒绝。所以建议大家写工具时,先抓一组最简单的无参数调用,验证信封结构无误后再处理复杂对象。