From d2677d2205d50aab55fdbd1cbe559a6584989433 Mon Sep 17 00:00:00 2001 From: jw238 Date: Sun, 2 Aug 2026 16:22:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eelf=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...et-NPU-CCU-ELF静态逆向初步调查-20260802.md | 489 ++++++++++++++++++ reports/Calculet-NPU-研究导航-20260802.md | 1 + 2 files changed, 490 insertions(+) create mode 100644 reports/Calculet-NPU-CCU-ELF静态逆向初步调查-20260802.md diff --git a/reports/Calculet-NPU-CCU-ELF静态逆向初步调查-20260802.md b/reports/Calculet-NPU-CCU-ELF静态逆向初步调查-20260802.md new file mode 100644 index 0000000..3b91a37 --- /dev/null +++ b/reports/Calculet-NPU-CCU-ELF静态逆向初步调查-20260802.md @@ -0,0 +1,489 @@ +# 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 +明确 custom32:596 +候选 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 验证的假设。 diff --git a/reports/Calculet-NPU-研究导航-20260802.md b/reports/Calculet-NPU-研究导航-20260802.md index d22aa68..5b058e9 100644 --- a/reports/Calculet-NPU-研究导航-20260802.md +++ b/reports/Calculet-NPU-研究导航-20260802.md @@ -47,6 +47,7 @@ | [性能压测故障注入与验收规范](./Calculet-NPU-性能压测故障注入与验收规范-20260802.md) | 正确 TTFT/TPOT、JSONL schema、open-loop 负载、soak/fault/pass-fail | | [Batch、KV 与多模型调度实现规格](./Calculet-NPU-Batch-KV与多模型调度实现规格-20260802.md) | lane/profile、continuous batching、KV 状态、load/unload、公平性和配置 | | [双芯粒双模型独立服务可行性与实施规格](./Calculet-NPU-双芯粒双模型独立服务可行性与实施规格-20260802.md) | CalCore/芯粒边界、组合 calbin、chip session、双 worker、容量/并发/故障验收 | +| [CCU ELF 静态逆向初步调查](./Calculet-NPU-CCU-ELF静态逆向初步调查-20260802.md) | 本地 YOLO ELF、RV32 启动、149 执行块、32/64 位私有编码、字段假设和验证路线 | 这些规格故意把“设计完成”和“硬件已支持”分开。凡涉及新 calbin、kernel、partition、单芯粒、多模型常驻或 batch>1 的部分仍标 C3,直到厂商产物通过 runner。