当我们使用 Rockchip RK3588 上的 Mali-G610 GPU 时,必须加载名为 mali_csffw.bin 的固件才能让 GPU 正常工作。这篇博客不会重复静态逆向工程的内容,而是聚焦于运行时行为的动态分析 —— 固件在 GPU 内部如何执行、内存如何被管理、以及开发者如何通过 GDB 和追踪缓冲区进行实时调试。
固件运行时的核心架构
CSF(Command Stream Firmware)固件运行在 GPU 内部的一个 Cortex-M7 微控制器上。这颗 MCU 是 ARM v10 架构 Valhall GPU 的核心组件,负责处理过去由内核完成的大量任务。根据实测,这颗 Cortex-M7 r1p2 版本运行在与 GPU 相同的时钟域上,在 RK3588 上最高可达 990 MHz—— 这个频率因芯片个体差异略有不同,但已足以处理复杂的命令流解析。
理解固件运行时的行为,关键在于掌握它与系统其余部分的交互方式。固件不仅执行命令流指令,还管理 GPU 电源状态、处理中断、协调内核与用户空间的数据交换。这些职责以往由内核空间驱动程序承担,如今被下沉到 MCU 中执行,这种设计变化深刻影响了调试方法。
三层内存架构的运行时解析
固件访问内存并非简单的线性映射,而是通过三个层次协同工作。理解这一架构对于调试内存访问问题至关重要。
第一层是 MCU 集成的 MPU(内存保护单元)。这个单元配置权限和 L1 缓存行为,提供 16 个几乎任意大小的区域(32 字节的倍数,最大为二次幂的八倍)。对齐要求是区域大小向上取整到二次幂。当发生访问违规时,MCU 自身会捕获异常并处理。值得注意的是,固件可以自由配置这 16 个区域来实施精细的内存保护策略。
第二层是 MCU 内存映射。这里只有映射功能,不支持权限控制。八个 128 MiB 区域各自需要 64 MiB 对齐。这种设计使得固件可以灵活地将不同的虚拟地址空间映射到固定的物理位置。需要特别指出的是,这一层不会产生故障 —— 未映射的访问会静默失败或返回不可预测的数据,这也是固件设计中需要特别小心的地方。
第三层是 GPU MMU。这是由内核控制的层次,负责配置权限、映射、 L2 缓存行为和缓存一致性。它支持十六个 48 位虚拟地址空间,每个空间可容纳任意数量的 4 KB 页面的页表。页面必须 4 KB 对齐。与前两层不同,MMU 产生的故障会触发内核处理,MCU 地址空间中的故障会导致 GPU 复位。
在实际调试中,访问特定虚拟地址的典型代码流程是先通过 MMIO 寄存器配置映射寄存器 —— 基地址为 0x40022100,控制从 0x08000000 开始的 128 MiB 区域;然后设置地址空间选择和目标虚拟地址;接着配置 MPU 区域设置权限和缓存属性;最后通过内存屏障指令确保所有写入生效后访问映射后的数据。
中断系统的运行时行为
中断机制是固件运行时与外部世界交互的核心通道。理解每种中断类型的触发条件和处理逻辑,是调试固件行为的前提。
内核到 MCU 的中断最为常用,用于唤醒 MCU 执行固件初始化、配置命令流和命令流组、进行电源管理任务,以及进入保护模式等。作者还提到他曾修改内核,使追踪缓冲区写入时产生中断 —— 通常从 CPU 角度看来追踪缓冲区是只读的 —— 从而实现了 gdb 的双向通信。
MCU 到内核的中断在固件处理完内核请求后发送,也用于命令流中的 event store 或 event add 指令。这些指令允许内核唤醒正在 poll GPU 完成工作的用户空间线程,同时用于错误信号传递。
用户空间到 MCU 的中断仅用于一件事:告知 GPU 开始处理一组命令流指令。这是最直接的 GPU 工作提交方式。
GPU 到 MCU 的中断在命令流处理完成时触发,以便固件向内核报告状态或关闭未使用的 GPU 组件。当命令流指令需要模拟(如前述的 event add)时也会产生这类中断。
MCU 定时器中断使用 SysTick 定时器,可用于在设定时间后运行代码 —— 例如实现片段作业的软停止,让其他上下文获得渲染机会。
动态调试实践:GDB 集成
使用 GDB 调试运行在 GPU 内部 MCU 上的固件,需要克服没有外部调试硬件的困难。作者使用了名为 MRI 的调试服务器,这是 Adam Green 为 Armv7-M 架构开发的开源工具,利用自托管调试功能实现软件调试。
调试环境的搭建需要以下步骤。首先是内核补丁 —— 官方内核不提供调试接口,需要打补丁以在 debugfs 中创建 /sys/kernel/debug/mali0/fw_io 文件,该文件使用固件中的 fwin 和 fwout 追踪缓冲区进行双向通信。其次是追踪缓冲区配置 —— 固件镜像中包含三个追踪缓冲区:fwlog 用于日志输出,fwin 和 fwout 用于 gdb 通信。
连接调试器时,使用以下命令:gdb -iex 'set osabi none' -ex 'target remote /sys/kernel/debug/mali0/fw_io' /tmp/w/fw。进入后即可设置断点、单步执行、检查变量,体验与普通嵌入式调试几乎相同。唯一显著区别是 NULL 指针现在指向中断向量表,这会导致一些习惯性的调试操作需要调整。
实际的调试会话可能如下:先查看堆栈跟踪确定当前执行位置,然后中断在标准输出函数上查看输出内容,最后继续执行等待下一个断点。硬件断点工作正常,但硬件观察点存在限制 —— 由于异常是异步的,可能在触发指令之后几条指令才发生,因此几乎无用。替代方案包括使用 MPU 区域禁止所有访问,或单步执行并在调试处理程序中检查每个寄存器值。
追踪缓冲区的运行时监控
追踪缓冲区是观察固件运行时行为的另一重要手段。固件镜像中定义的追踪缓冲区结构包含几个关键字段:size 字段指定缓冲区大小,insert 和 extract 指针分别指向写入位置和读取位置,data 指向实际数据存储区域,enable 位控制是否启用。
标准的 fwlog 追踪缓冲区可用于获取固件运行时的日志输出。通过读取 /sys/kernel/debug/mali0/ 下的相应文件,可以实时查看固件输出的调试信息。更进一步,通过修改内核和固件,可以利用 fwin 和 fwout 缓冲区实现前面提到的双向通信,从而支持完整的调试功能。
实用调试参数与配置清单
以下是运行和调试 RK3588 CSF 固件时的关键参数和配置项,供实际使用参考。
固件加载方面,mali_csffw.bin 必须放置在 /lib/firmware 目录中,这是 kbase 驱动寻找固件的标准位置。固件镜像格式包含头部信息(版本号、条目大小)和多个条目类型:Interface 条目设置内存段、Tracebuffer 条目建立通信缓冲区、Configuration 条目允许用户空间通过 sysfs 修改固件配置、Timeline metadata 条目提供追踪事件信息。
中断配置方面,doorbell 机制的寄存器地址需要在地址映射中正确配置,以便用户空间的命令流提交能够正确触发 MCU 处理。电源管理相关的配置需要通过内核接口与固件协同完成。
调试环境配置需要:确保 debugfs 已挂载(mount -t debugfs none /sys/kernel/debug),加载修改后的内核模块以提供 fw_io 接口,准备支持 Armv7-M 架构的 gdb 交叉编译工具链。
运行时行为分析的工程价值
理解 CSF 固件的运行时行为并进行动态调试,对于从事 RK3588 GPU 驱动开发和优化的工作者具有直接的工程价值。首先是问题定位 —— 当 GPU 出现异常时,能够通过 GDB 逐步分析固件执行流程,确定问题发生在内核侧还是固件侧。其次是性能优化 —— 追踪缓冲区和中断分析可以帮助识别命令流处理中的瓶颈。第三是功能扩展 —— 作者展示了在固件上运行 MicroPython 的可能性,这为固件快速迭代提供了实验基础。
固件本质上是运行在 GPU 内部 MCU 上的一个特殊操作系统。它有自己的内存管理、中断处理、任务调度机制。掌握这些运行时特性的分析方法,是深入理解现代 GPU 架构的必经之路。
资料来源:本文技术细节主要来自 icecream95 的博客文章《Fun with CSF firmware》(https://icecream95.gitlab.io/fun-with-csf-firmware.html),该文详细记录了作者对 RK3588 Mali-G610 CSF 固件的逆向工程与动态调试实践。