3. Binder内存映射

任务目标

编写一个用户空间C程序,通过open()打开Binder设备节点/dev/binder,使用mmap()建立内核缓冲区映射,并打印映射前后的虚拟地址布局与映射属性,以验证Binder IPC中内存映射机制的基本行为。

核心实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/ioctl.h>
#include <errno.h>

#define BINDER_DEV_PATH "/dev/binder"
#define MAP_SIZE (128 * 1024)  // 128KB映射区

// Binder驱动版本信息结构(与内核include/uapi/linux/android/binder.h对应)
struct binder_version {
    long protocol_version;
};
#define BINDER_VERSION _IOWR('b', 9, struct binder_version)

int main(void) {
    int fd;
    void *map_addr;
    struct binder_version ver;

    // 1. 打开Binder设备
    fd = open(BINDER_DEV_PATH, O_RDWR | O_CLOEXEC);
    if (fd < 0) {
        perror("open /dev/binder");
        return EXIT_FAILURE;
    }
    printf("[1] 打开 %s 成功, fd = %d\n", BINDER_DEV_PATH, fd);

    // 2. 查询Binder驱动协议版本(验证设备可用性)
    memset(&ver, 0, sizeof(ver));
    if (ioctl(fd, BINDER_VERSION, &ver) == 0) {
        printf("[2] Binder驱动协议版本: %ld\n", ver.protocol_version);
    } else {
        perror("ioctl BINDER_VERSION");
        close(fd);
        return EXIT_FAILURE;
    }

    // 3. 映射前进程内存布局关键信息
    printf("\n[3] mmap前:\n");
    printf("    当前程序堆起始(brk): %p\n", sbrk(0));
    printf("    准备映射大小: %zu 字节 (%zu KB)\n", MAP_SIZE, MAP_SIZE / 1024);

    // 4. 执行mmap映射Binder内核缓冲区
    //    MAP_PRIVATE: 写时复制,不修改内核缓冲区内容
    //    PROT_READ: 只读映射(Binder驱动要求)
    map_addr = mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map_addr == MAP_FAILED) {
        perror("mmap /dev/binder");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("\n[4] mmap成功:\n");
    printf("    映射起始地址: %p\n", map_addr);
    printf("    映射结束地址: %p\n", (char *)map_addr + MAP_SIZE);
    printf("    映射长度: %zu 字节\n", MAP_SIZE);

    // 5. 读取映射区首个字节(验证可访问性)
    volatile char first_byte = *((volatile char *)map_addr);
    printf("\n[5] 读取映射区首字节: 0x%02x (可正常访问)\n", (unsigned char)first_byte);

    // 6. 映射后进程内存布局变化
    printf("\n[6] mmap后:\n");
    printf("    当前程序堆起始(brk): %p (未变化)\n", sbrk(0));
    printf("    映射区域: %p - %p\n", map_addr, (char *)map_addr + MAP_SIZE);

    // 7. 清理
    if (munmap(map_addr, MAP_SIZE) == 0) {
        printf("\n[7] munmap解除映射成功\n");
    } else {
        perror("munmap");
    }
    close(fd);
    printf("    关闭fd, 程序退出\n");

    return EXIT_SUCCESS;
}

编译与运行

# 编译(需Linux内核头文件支持Binder uapi)
gcc -o binder_mmap binder_mmap.c

# 运行(需要root权限或所属binder组)
sudo ./binder_mmap

验证方法与判定标准

程序执行后应依次观察到以下现象,全部满足即验证通过:

  • 步骤[1]:成功打开/dev/binder,fd值大于等于3;若提示Permission denied,说明当前用户无权限,需sudo或加入binder组。
  • 步骤[2]:打印出Binder驱动协议版本号(通常为1),证明设备节点可正常通信。
  • 步骤[4]:mmap返回非MAP_FAILED地址,且映射起始地址按页对齐(低12位为0)。
  • 步骤[5]:能成功读取映射区首字节而不触发段错误,说明内核已建立物理页映射。
  • 步骤[6]:brk堆地址在mmap前后保持不变,而映射区域位于进程虚拟地址空间的独立区间(通常在堆与栈之间),验证mmap不会干扰堆内存布局。
  • 步骤[7]:munmap返回0,程序正常退出,退出码为0。

关键说明

  • Binder驱动要求映射使用PROT_READ(只读),不可使用PROT_WRITE,否则mmap返回EACCES错误。
  • 映射大小MAP_SIZE取128KB仅为演示值,实际Binder驱动会按需扩展映射区域,但用户空间一次mmap的映射大小即为进程可访问的Binder缓冲区上限。
  • 若系统未加载Binder驱动(如纯容器环境),open()会返回ENOENT或ENODEV,此时需在宿主机或具有Binder驱动的环境中运行。
  • 映射区首字节读取值通常为0,这是Binder内核缓冲区的初始状态,不表示数据传输内容。
Binder mmap 前后进程虚拟地址空间变化 mmap 前 内核空间 (高地址) 栈 (向下增长) 局部变量、返回地址 地址空间空洞 (无映射) 堆 (brk, 向上增长) malloc分配区 .bss / .data / .text 低地址 mmap mmap 后 内核空间 (高地址) 栈 (向下增长) Binder映射区 (128KB) PROT_READ | MAP_PRIVATE 堆 (brk, 未变化) malloc分配区 .bss / .data / .text 低地址 Binder映射区 堆(brk) 栈 内核空间