17 KiB
Calculet NPU CCU ELF 静态逆向初步调查
日期:2026-08-02
状态:第一轮静态逆向;未执行样本,未修改设备或模型
对象:本仓库 CalRT Python 测试目录中的 YOLOv5s 单芯粒 CCU ELF
1. 结论摘要
本地 YOLOv5s 样本的 ccu0_chip0.elf 和 ccu1_chip0.elf 是两个真实的、可直接读取的 NPU 控制程序,不是 Git LFS pointer。它们是 stripped、静态链接的 ELF32 RISC-V 可执行文件,运行于两套不同地址窗口中的 CCU 控制核。
ELF 并非只包含标准 RISC-V 指令,而是至少混合三类编码:
- 标准 RV32I/M/F/Zicsr 指令,用于启动、寄存器准备、栈、分支和 MMIO。
- 使用 RISC-V 保留 custom opcode 的 32 位 Calculet 指令;已稳定识别 opcode
0x2b。 - 标准 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_DONE 和 END |
实体文本 |
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 段调用图
调度器从 0x0a210110 到 0x0a210360 连续发出 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
可见结构:
- 函数序言,栈减 96 字节。
- 保存
ra及多个 callee-saved 寄存器。 - 用
lui/addi构造大量常量、地址和字段值。 - 反复向
0x0a002000/0x0a00200c写值。 - 标准 RV32 指令与规则 8 字节未知包交替。
- 执行四条固定的 32 位 custom 指令。
- 恢复寄存器并返回。
首批 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 使用如下规则:
.text按小端读取。- 如果低 7 位是 RV32I/M/F/Zicsr 已知 opcode,按 4 字节前进。
- 如果低 7 位是 RISC-V custom opcode
0x0b/0x2b/0x5b/0x7b,按 4 字节 custom 记录。 - 其他位置按 8 字节候选私有包记录。
- 使用 149 个直接
jal目标作为块边界,避免错误跨块传播。
这是一种启发式边界恢复,不是私有 ISA 的正式 decoder。私有包首 32 位偶然碰撞标准 opcode 时仍可能被误分类。
13.2 数量
对 CCU0 的估算:
标准 32 位字:约 83,854
明确 custom32:596
候选 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():
- 从 calbin 获取 CCU0/CCU1 ELF program info。
- 计算 CCU scalar DDR offset 和两个程序的 mmap entry。
- 配置 CCU stack。
- 写 CCU0/CCU1
scl_ddr_offset。 - 生成
CONFIG_CCU_CSR,写两个 program address。 - 提交 launcher。
- 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. 风险与误判防护
- 不把 objdump 在私有包中误解码的浮点/CSR 指令当作事实。
- 不把 149 个块直接等同于 149 个 ONNX 节点;一个算子可能拆为多个阶段,也可能融合多个算子。
- 不把 high32 小值直接命名为长度或通道,只记录相关性。
- 不把
ccu*.elf当作完整 NPU kernel 源码;它是控制程序与私有命令流的编译结果。 - 不在缺少签名/校验规则时修改 ELF 并上板执行。
18. 后续验证路线
按信息增益排序:
- 下载该 YOLO 样本的 LFS 参数、input、golden 和 ping/pong,确认完整样本哈希。
- 从完整 CalRT Git bundle 恢复历史中的
device_cmd_gen和calrt_config_ccu_csr.h,寻找已删除的 CCU 编码定义。 - 获取 YOLO calbin 的算子 YAML/profparts,建立 block 地址到算子顺序的候选对应。
- 对 CCU0/CCU1 相同 block 做字段级差分,识别 unit/chip/DM 选择位。
- 用 Runtime trace、CCU instruction counter、DM0/DM1 counters 做无修改动态观测。
- 若厂商提供 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 验证的假设。