Files
calculet-npu-research-archive/reports/Calculet-NPU-CCU-ELF静态逆向初步调查-20260802.md
T
2026-08-02 16:22:50 +08:00

17 KiB
Raw Blame History

Calculet NPU CCU ELF 静态逆向初步调查

日期:2026-08-02
状态:第一轮静态逆向;未执行样本,未修改设备或模型
对象:本仓库 CalRT Python 测试目录中的 YOLOv5s 单芯粒 CCU ELF

1. 结论摘要

本地 YOLOv5s 样本的 ccu0_chip0.elfccu1_chip0.elf 是两个真实的、可直接读取的 NPU 控制程序,不是 Git LFS pointer。它们是 stripped、静态链接的 ELF32 RISC-V 可执行文件,运行于两套不同地址窗口中的 CCU 控制核。

ELF 并非只包含标准 RISC-V 指令,而是至少混合三类编码:

  1. 标准 RV32I/M/F/Zicsr 指令,用于启动、寄存器准备、栈、分支和 MMIO。
  2. 使用 RISC-V 保留 custom opcode 的 32 位 Calculet 指令;已稳定识别 opcode 0x2b
  3. 标准 binutils 无法识别、呈规则 8 字节分组的 Calculet 私有 CCU 指令包。

ccu0_chip0.elf 的顶层调度器顺序调用 149 个编译器生成的执行块。每个执行块末尾都有相同四条 32 位 custom 指令,随后恢复寄存器并返回。执行块内部交替出现标准 RV32 指令和大量私有 64 位包。这更像完整展开的静态 NPU 执行计划,而不是通用固件或包含权重的数据文件。

目前能够确认 ELF 的加载布局、入口初始化、顶层调用图、私有包边界、部分 MMIO 地址和外围 CCU CSR;尚不能在缺少私有 ISA、compiler/oplib 和算子 YAML 的情况下,把每个私有 opcode 精确命名为 GEMM、DMA、Conv 或同步操作。

2. 证据等级

本文使用以下标记,避免把猜测写成事实:

等级 含义
F0 ELF 字节、ELF header 或当前源码直接证明
F1 两个 ELF 或 ELF 与 HostRT/CSR 头文件交叉一致
H1 结构性推断,证据较强但缺厂商定义
H2 工作假设,只用于指导后续验证

3. 样本与相关文件

样本根目录:

source/repositories-complete/calrt-exact/calrt/hostrt/python/test/yolo-0915/calbin/

3.1 实体 ELF

文件 字节数 SHA-256
yolov5s.pt--W8A8-W4AF16/ccu0_chip0.elf 790,824 22c5cfd5a58f25dcaed55dc0a553c7ee8f351b48dfa171220dd5bf5955566ff2
yolov5s.pt--W8A8-W4AF16/ccu1_chip0.elf 788,464 51ea1cb733d5f5d664bb3e08024b81ae2c06287ae98a153c1f1a78d334a1f060

3.2 可读元数据

文件 内容 状态
model_memory_reserved_info.txt 模型、设备、DRAM/SRAM reservation、参数映射 实体文本
yolov5s.pt--W8A8-W4AF16/submodel_memory_reserved_info.txt I/O、CCU ELF、芯粒和计算单元声明 实体文本
yolov5s.pt--W8A8-W4AF16/cpu_chip0_runtime_cmds.txt POLLING_CCU_DONEEND 实体文本

3.3 当前不是实体内容的文件

以下文件当前只有约 126-133 字节,是 Git LFS pointer,不能按其工作树内容分析:

param_blk0.bin
yolov5s.pt--W8A8-W4AF16_inputs[0].bin
yolov5s.pt--W8A8-W4AF16_outputs[0].bin
yolo_Compiler_golden.bin
cpu_chip0_ping.bin
cpu_chip0_pong.bin

因此本文没有把这些 pointer 的字节当成参数、golden 或运行命令。

4. 模型与内存合同

元数据确认该样本为:

calcc_ver: v0.1
device: ks1
model_type: cnn
model_arch: yolov5s.pt--W8A8-W4AF16
n_batch: 1
calc_mchip: schip
calc_utype: [ccu]

I/O 合同:

角色 shape dtype 虚拟地址区间 字节数
Input [1,640,640,3] BF16 0xc0000000-0xc0258000 2,457,600
Output [1,1575,6,16,16] BF16 0xc051f000-0xc09bd000 4,838,400

reservation

空间 起始地址 大小
DRAM 0xc0000000 15,049,728
SRAM 0x08000000 16,784,768
Sync 0x0a080000 2

这说明 ELF 代码、参数映射和 tensor 地址是不同资产:ELF 主要是 CCU 控制程序,权重本应由 param_blk0.bin 提供。

5. ELF 基本属性

两份文件共同属性:

Class: ELF32
Endian: little-endian
Machine: RISC-V
Type: EXEC
ABI: single-float ABI
Link: statically linked
Symbols: stripped
RISC-V arch: rv32i2p1_m2p0_f2p2_zicsr2p0_zmmul1p0
Stack alignment: 16 bytes

没有发现:

符号表
调试信息
动态依赖
函数名
源文件路径
可用的算子名称字符串

6. CCU0 文件和地址布局

ccu0_chip0.elf.text 恰好为 0xc0000 字节,即 768 KiB。

文件偏移 CCU 虚拟地址 长度 解释 等级
0x000000-0x000fff 4 KiB ELF header、program header、padding F0
0x001000-0x0010e7 0x0a210000 232 B 启动和运行环境初始化 F0
0x0010e8-0x001377 0x0a2100e8 656 B 顶层直线调度器 F0
0x001378-0x0c0fff 0x0a210378 约 767 KiB 149 个编译器生成执行块 F0/H1
0x0c1000-0x0c1037 56 B .riscv.attributes F0
0x0c1038 以后 余量 section string table/header F0

地址换算:

CCU0 VA = 0x0a210000 + (file_offset - 0x1000)
file_offset = 0x1000 + (CCU0 VA - 0x0a210000)

7. CCU1 文件和地址布局

ccu1_chip0.elf

入口 VA:       0x4a210000
.text offset:  0x1000
.text size:    0x0bf6c8
.text end VA:  0x4a2cf6c8

其物理加载地址在 Program Header 中为 0x40010000,虚拟地址为 0x4a210000。CCU0 与 CCU1 的启动结构和 149 段顶层调用框架高度相似,但代码体、目标地址和常量并不完全相同。

推断:两份 ELF 由同一图执行计划生成,分别控制不同 CCU/执行资源;不是同一文件的简单重定位副本。(F1/H1)

8. 启动代码逐段解释

8.1 私有 CSR 和机器状态

入口前几条指令:

0a210000: lui   x5,0x400
0a210004: csrrs x0,0x7c0,x5
0a210008: lui   x5,0x2
0a21000c: csrrs x0,mstatus,x5

随后将 x1-x31 清零,并继续配置:

CSR 写入值 证据 当前解释
0x7c0 0x00400000 F0 厂商自定义状态/扩展开关,具体位未知
mstatus 0x00002000,后追加 0x8 F0 标准 RISC-V machine status
0x7c2 0x00030013 F0 厂商自定义配置,字段未知
0x7c1 0x0000007f F0 厂商自定义配置,字段未知
0x7c5 0x0000610c F0 厂商自定义配置,字段未知

0x7c0-0x7ff 属于厂商自定义 CSR 范围。当前 HostRT 源码没有这些 CPU-local CSR 的位字段定义,不能仅按数值命名。

8.2 栈和 BSS

CCU0 设置栈指针到约 0x0a208000,然后按 ELF 的 BSS 边界执行清零循环。该样本 .bss 实际为空或接近为空。

CCU1 使用其独立地址窗口,栈和 .text 地址整体移到 0x4a... 区域。

8.3 跳入顶层调度器

启动代码通过 jalr 跳到 0x0a2100e8。顶层调度器先向以下 MMIO 地址写 1

0x0a002000
0x0a00200c

仓库 mem_map.h 给出的 CCU 视角寄存器窗口从 0x0a000000 开始,因此这两个地址落在 CCU 本地寄存器空间。(F1)

具体寄存器名未出现在当前头文件中,可能是执行单元启用、状态清理或同步触发。(H2)

9. 顶层 149 段调用图

调度器从 0x0a2101100x0a210360 连续发出 149 条直接 jal

0x0a210378
0x0a210990
0x0a2149d0
0x0a218ec4
...
0x0a2cfa08
0x0a2cfc2c

顶层没有发现复杂的请求级动态调度结构。它是一个顺序展开的调用表:

初始化 -> block[0] -> block[1] -> ... -> block[148] -> 完成 -> return

块尺寸统计:

指标 字节数
最小 480
中位数 3,164
平均 5,272.1
最大 107,692

最大的执行块:

block VA 字节 标准 RV32(启发式) custom32 custom64(启发式)
14 0x0a22cacc 107,692 7,719 4 9,600
33 0x0a25db00 25,692 1,619 4 2,400
109 0x0a2aac28 25,668 1,613 4 2,400
100 0x0a29a5b0 24,256 1,260 4 2,400
99 0x0a295dac 18,436 1,961 4 1,322

块大小与私有包数量大体相关。大块可能对应计算或数据搬运量较大的图阶段,但没有算子清单时不能把 block 14 直接命名为某个 Conv。(H1/H2)

10. 第一执行块字节轨迹

范围:

block index: 0
file offset: 0x1378-0x198f
CCU0 VA:    0x0a210378-0x0a21098f
size:       1,560 bytes

可见结构:

  1. 函数序言,栈减 96 字节。
  2. 保存 ra 及多个 callee-saved 寄存器。
  3. lui/addi 构造大量常量、地址和字段值。
  4. 反复向 0x0a002000/0x0a00200c 写值。
  5. 标准 RV32 指令与规则 8 字节未知包交替。
  6. 执行四条固定的 32 位 custom 指令。
  7. 恢复寄存器并返回。

首批 8 字节包示例(按小端两个 32 位字展示):

VA low32 high32
0x0a2103c8 0xa72d8008 0x00000354
0x0a2103e4 0xa775a014 0x00000000
0x0a2103f4 0xf7adc014 0x00000000
0x0a21040c 0x632dc014 0x00000000
0x0a21044c 0x18e60920 0x00000039
0x0a2104cc 0xa55e6030 0x00000422

11. 标准反汇编器为何失步

ELF 属性明确不含 RISC-V C 压缩扩展,但 GNU objdump 在上述区域输出大量:

.insn 2
.insn 4

并偶尔把私有包的中间字节误解成浮点、CSR 或分支指令。原因不是 ELF 损坏,而是标准解码器不知道 Calculet 私有编码格式。

因此:

  • 启动代码、标准 opcode 和明确对齐的控制流可以相信。
  • 私有包区域中 objdump 显示的 fmadd.s、随机 CSR 等不能直接当作真实语义。
  • 对整个 ELF 做普通 mnemonic 频率统计会混入误解码结果。

12. 32 位 custom 指令

每个执行块末尾稳定出现以下四个 32 位字:

顺序 编码 opcode[6:0] funct3 rd rs1 rs2 funct7
1 0x0001f82b 0x2b 7 16 3 0 0
2 0x0000092b 0x2b 0 18 0 0 0
3 0x000008ab 0x2b 0 17 0 0 0
4 0x1400102b 0x2b 1 0 0 0 10

计数:

149 blocks * 4 = 596 custom32 instructions

确定事实:

  • 0x2b 属于 RISC-V custom opcode 空间。(F0
  • 四条指令在 CCU0 每个块中各出现一次。(F0)
  • CCU1 也保持相同四条尾序列。(F1)

工作假设:

  • 前三条可能从专用执行单元读取/同步状态或搬运结果。
  • 最后一条 funct7=10, funct3=1 可能提交、barrier 或完成一个编译块。

没有动态 trace 或 ISA 定义前,不给它们分配正式操作名。(H2)

13. 64 位私有包的启发式字段

13.1 边界恢复方法

第一轮 parser 使用如下规则:

  1. .text 按小端读取。
  2. 如果低 7 位是 RV32I/M/F/Zicsr 已知 opcode,按 4 字节前进。
  3. 如果低 7 位是 RISC-V custom opcode 0x0b/0x2b/0x5b/0x7b,按 4 字节 custom 记录。
  4. 其他位置按 8 字节候选私有包记录。
  5. 使用 149 个直接 jal 目标作为块边界,避免错误跨块传播。

这是一种启发式边界恢复,不是私有 ISA 的正式 decoder。私有包首 32 位偶然碰撞标准 opcode 时仍可能被误分类。

13.2 数量

对 CCU0 的估算:

标准 32 位字:约 83,854
明确 custom32596
候选 custom64:约 55,968

13.3 初步字段分解

暂以 64 位小端值表示:

bits  0..7   family/opcode candidate
bits  8..31  payload0
bits 32..63  payload1

按最低字节统计的主要家族:

family byte CCU0 数量 样例 low32 样例 high32
0x0c 10,906 0xd519600c 0x00000000
0x30 9,156 0xa55e6030 0x00000422
0x08 8,851 0xa72d8008 0x00000354
0x14 8,793 0xa775a014 0x00000000
0x04 4,712 0x451e4004 0x00000015
0x40 2,945 0xb858a440 0x00000011
0x20 2,230 0x18e60920 0x00000039
0xe0 1,740 0xb2c1e1e0 0x00000000

观察:

  • family byte 集中在少数值,适合作为 opcode 或主格式字段。(H1)
  • high32 经常为 0、1、20、21 或较小数值,可能是长度、模式、通道、tile 或同步字段。(H2)
  • CCU0/CCU1 家族分布相近但参数和数量不同,符合相同图在两个控制单元上的不同资源分配。(H1)
  • 不能排除真正 opcode 位于低字节的部分位,而不是完整 8 位。(H2)

14. CCU 外围寄存器对照

当前源码提供了 Host/System 视角 CSR 地址:

CCU0 SCL CSR base: 0x0a230000
CCU1 SCL CSR base: 0x0a234000
Sync unit base:    0x0a080000
D2D base:          0x0a2e0000

已知 offset

offset 名称 作用
0x0000 scl_program_addr ELF/程序入口配置
0x0004 scl_start 启动 CCU
0x0008 scl_done 完成状态
0x000c ddr_boundary DDR 边界
0x0010 scl_ddr_offset scalar DDR offset
0x0014 nscl_ddr_offset non-scalar DDR offset
0x0018 pf_ctrl prefetch 控制
0x001c stk_init_ctrl 栈初始化
0x1014 dm0_csr_fifo DM0 CSR FIFO
0x1018 dm0_csr_fifo_status DM0 FIFO 状态
0x1024 dm0_function_en DM0 功能开关
0x1028 dm0_instr_start_cmd DM0 指令窗口开始
0x1414 dm1_csr_fifo DM1 CSR FIFO
0x1418 dm1_csr_fifo_status DM1 FIFO 状态
0x2800 instr_counter_ctrl 指令计数器控制

这些字段证明硬件至少有两组 data mover、CSR FIFO、load/write/DDR counters、ping/pong model config 和指令计数器。但尚未证明某一个 64 位 family 与哪一个 CSR 一一对应。

15. HostRT 到 ELF 的加载和启动原理

HostRT 的 genInferLauncher()

  1. 从 calbin 获取 CCU0/CCU1 ELF program info。
  2. 计算 CCU scalar DDR offset 和两个程序的 mmap entry。
  3. 配置 CCU stack。
  4. 写 CCU0/CCU1 scl_ddr_offset
  5. 生成 CONFIG_CCU_CSR,写两个 program address。
  6. 提交 launcher。
  7. runtime command 使用 POLLING_CCU_DONE 等待完成。

cpu_chip0_runtime_cmds.txt 只有:

cmdId 0x0c: POLLING_CCU_DONE
cmdId 0x00: END

这与 ELF 内部携带完整执行计划一致:Host 负责加载、启动和等待,细粒度算子/DMA 调度主要在 CCU ELF 内完成。(F1)

16. 当前可以和不能得到的内容

可以恢复

  • ELF 入口、加载地址和 section 边界。
  • 标准 RV32 启动和控制代码。
  • 顶层 149 段顺序调用关系。
  • 每个执行块的地址、大小和私有包数量。
  • 32 位 custom opcode 和固定尾序列。
  • 64 位候选包的边界、频率和字段变化。
  • CCU/DM/同步外围寄存器地图。

当前不能可靠恢复

  • 原始 C/C++/DSL kernel 源码。
  • 每个私有 opcode 的正式名称和精确位字段。
  • block 到 YOLO 算子名称的精确映射。
  • 私有指令的 cycle、hazard 和异常语义。
  • calcc 如何从图、oplib 和 kernel 生成这些编码。

17. 风险与误判防护

  1. 不把 objdump 在私有包中误解码的浮点/CSR 指令当作事实。
  2. 不把 149 个块直接等同于 149 个 ONNX 节点;一个算子可能拆为多个阶段,也可能融合多个算子。
  3. 不把 high32 小值直接命名为长度或通道,只记录相关性。
  4. 不把 ccu*.elf 当作完整 NPU kernel 源码;它是控制程序与私有命令流的编译结果。
  5. 不在缺少签名/校验规则时修改 ELF 并上板执行。

18. 后续验证路线

按信息增益排序:

  1. 下载该 YOLO 样本的 LFS 参数、input、golden 和 ping/pong,确认完整样本哈希。
  2. 从完整 CalRT Git bundle 恢复历史中的 device_cmd_gencalrt_config_ccu_csr.h,寻找已删除的 CCU 编码定义。
  3. 获取 YOLO calbin 的算子 YAML/profparts,建立 block 地址到算子顺序的候选对应。
  4. 对 CCU0/CCU1 相同 block 做字段级差分,识别 unit/chip/DM 选择位。
  5. 用 Runtime trace、CCU instruction counter、DM0/DM1 counters 做无修改动态观测。
  6. 若厂商提供 ISA/oplib,编写正式 decoder,并用同一小算子的多个 shape 编译结果做受控差分。

19. 第一轮最终判断

当前最符合证据的模型是:

Host CalRT launcher
    -> 配置 CCU0/CCU1 ELF 地址、栈和 DDR offset
    -> 启动两个 RV32 CCU
    -> CCU 顶层调度器顺序调用 149 个生成块
    -> 每块用标准 RV32 准备参数和 MMIO
    -> 通过 Calculet 64 位私有包驱动计算/DMA/同步资源
    -> 用四条固定 custom-1 指令收尾
    -> Host 轮询 CCU done

其中“149 个块”“四条固定 custom 指令”“主要 64 位 family 分布”已由字节分析直接得到;“计算/DMA/同步”的具体 opcode 对应仍是待动态 trace 或私有 ISA 验证的假设。