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

490 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 指令,而是至少混合三类编码:
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. 样本与相关文件
样本根目录:
```text
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,不能按其工作树内容分析:
```text
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. 模型与内存合同
元数据确认该样本为:
```text
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 基本属性
两份文件共同属性:
```text
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
```
没有发现:
```text
符号表
调试信息
动态依赖
函数名
源文件路径
可用的算子名称字符串
```
## 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 |
地址换算:
```text
CCU0 VA = 0x0a210000 + (file_offset - 0x1000)
file_offset = 0x1000 + (CCU0 VA - 0x0a210000)
```
## 7. CCU1 文件和地址布局
`ccu1_chip0.elf`
```text
入口 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 和机器状态
入口前几条指令:
```text
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`
```text
0x0a002000
0x0a00200c
```
仓库 `mem_map.h` 给出的 CCU 视角寄存器窗口从 `0x0a000000` 开始,因此这两个地址落在 CCU 本地寄存器空间。(F1)
具体寄存器名未出现在当前头文件中,可能是执行单元启用、状态清理或同步触发。(H2)
## 9. 顶层 149 段调用图
调度器从 `0x0a210110``0x0a210360` 连续发出 149 条直接 `jal`
```text
0x0a210378
0x0a210990
0x0a2149d0
0x0a218ec4
...
0x0a2cfa08
0x0a2cfc2c
```
顶层没有发现复杂的请求级动态调度结构。它是一个顺序展开的调用表:
```text
初始化 -> 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. 第一执行块字节轨迹
范围:
```text
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 在上述区域输出大量:
```text
.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 |
计数:
```text
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 的估算:
```text
标准 32 位字:约 83,854
明确 custom32596
候选 custom64:约 55,968
```
### 13.3 初步字段分解
暂以 64 位小端值表示:
```text
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 地址:
```text
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` 只有:
```text
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_gen``calrt_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. 第一轮最终判断
当前最符合证据的模型是:
```text
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 验证的假设。