末尾 zstd Blob
本页中的所有偏移、地址和节名均适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(包版本0.0.40,build-id89edbbe81c5b328a958fe628a9f2207d,781,691,048 字节)。请固定到 build-id;这是唯一明确的标识符。其他构建会有所不同。
摘要
对 libtpu.so 做一次朴素的 binwalk 风格 magic-byte carve,会找到 zstd 帧魔数 28 b5 2f fd,并容易误报文件偏移 0x20F99BEF 处存在一个约 4.1 MB 的追加在 ELF 节头之后、使用 zstd 字典压缩的 blob。并不存在这样的 blob。没有末尾载荷,没有嵌入式字典,也没有可用于恢复字典的 ZSTD_DCtx_loadDictionary 调用点。carve 锚定的字节序列是位于 .text 深处的一个 x86-64 指令立即数,而清楚地证明这一点正是本页的目的。
直接读取该二进制后,机制很简单。四个字节 28 b5 2f fd 是 ZSTD_MAGICNUMBER(0xFD2FB528,RFC 8878 §3.1.1.1)的小端编码。libtpu.so 静态链接了完整的 libzstd 源码,包括压缩器和解压器、309 个 ZSTD_* 符号,并且完整符号以本地 (t) 条目保留,因此这个常量出现了五次,每次都是某个 libzstd 函数中的 mov/cmp 立即数,用于把帧头写入调用者提供的输出缓冲区,或将其与输入缓冲区进行检查。五处都不在数据节中;没有一处被指针或重定位引用;五处都只能通过程序执行流进入 libzstd 代码而到达。
本页做三件事:证明 ELF 节头表恰好在 EOF 结束(不存在超出节之后的末尾区域),走读产生误报 mov 的 ZSTD_compressEnd_public 空帧收尾逻辑,并记录实际的运行时 zstd 表面,即由 riegeli 驱动的流式(解)压缩;如果使用字典,字典也是应用在运行时通过 ZSTD_DCtx_refDDict 提供,而不是烘焙在镜像中。carve 可能猜测该 blob 会解码出的按 codename 区分的硬件常量包是真实制品,但它位于别处,是未压缩的 protobuf;参见 chip-parts binarypb。
对重新验证本结论的读者来说,契约是:
- ELF 布局计算 —
e_shoff + e_shnum × e_shentsize = file size,因此没有追加内容。 - 五处 magic 出现 — 每一处的文件偏移、所在 libzstd 函数和指令角色。
- 空帧收尾逻辑 —
ZSTD_compressEnd_public中把28 b5 2f fd写入堆缓冲区的确切字节序列。 - 缺失的符号 — 为什么不存在
ZSTD_DCtx_loadDictionary会否定字典恢复方案,以及现有的ZSTD_DCtx_refDDict实际做什么。
| 声称的 blob | 0x20F99BEF 处约 4.1 MB 的 zstd-dict 压缩载荷 — 不存在 |
0x20F99BEF 处实际内容 | movl $0xfd2fb528,(%r14) 立即数(文件偏移 0x20F99BEF,.text) |
| 容器 | 节 [21] .text (PROGBITS, R+X) — 不在节表之后 |
| 节表 | e_shoff 0x2E979BA8 + 52 × 64 = 0x2E97A8A8 = EOF(没有末尾区域) |
| Magic 出现 | 共 5 处,全部在 .text 中,全部是 mov/cmp 立即数 |
| 字典机制 | 没有静态字典;ZSTD_DCtx_loadDictionary 缺失;只有运行时 ZSTD_DCtx_refDDict |
| 解压格式 | n/a — 没有可解压的 blob |
| 置信度 | 高(负面结论,由字节和反汇编锚定) |
**注意:**不存在“位于
0x20F99BEF、约 4.1 MB、zstd 字典编码、解码为按 codename 区分的硬件常量的末尾 zstd blob”。该偏移位于.text内,不在 EOF 之后;这些字节是一个mov立即数,不是帧;没有嵌入式字典;不存在载荷。下文给出字节级和反汇编级证明,用来解答 forensics overview 指向这里的 magic-byte 问题。
“EOF 之后的末尾内容”前提是错误的
ELF 实际在哪里结束
最初的假设是该 blob “位于最后一个 ELF 节头之后”,意味着有一个不属于任何节的追加载荷。ELF 头部计算直接否定了这一点。
e_shoff (Start of section headers) = 781,687,720 = 0x2E979BA8
e_shentsize = 64 = 0x40
e_shnum = 52 = 0x34
section-header table end = 0x2E979BA8 + 52 × 0x40
= 0x2E979BA8 + 0xD00
= 0x2E97A8A8
file size = 781,691,048 = 0x2E97A8A8 ← identical
```text
节头表一直延伸到文件的**最后**一个字节。它之后没有任何区域。最后一个承载数据的节是 `[51] .strtab`,结束于 `0x23DF76C3 + 0xAB824DE = 0x2E979BA1`;一个 7 字节对齐间隙位于 `0x2E979BA8` 的表之前。这个 745 MB 镜像中的每个字节都可归属于某个节,或归属于节头表本身。
> **陷阱 —** 节头表在 EOF 结束,是对“追加载荷”说法最干净的反证,只需一次 `readelf -h`。今后对这个二进制提出任何“节之后的末尾 blob”主张,都应当先检查 `e_shoff + e_shnum × e_shentsize`;在此构建中,该和逐字节等于文件大小。
### 锚点偏移位于 `.text` 结束前约 2.6 MB
carve 锚点 `0x20F99BEF`(文件偏移 553,229,295)**不在** EOF 处或之后。它落在节 `[21] .text` 内:
| 区域 | 文件范围 |
|---|---|
| `[21] .text` (PROGBITS, R+X) | `0xE63C000 .. 0x21217484` |
| `[11] .rodata` (PROGBITS, R) | `0x84A0000 .. 0xBE8AF28` |
| `[51] .strtab`(最后一个数据节) | `0x23DF76C3 .. 0x2E979BA1` |
| 节头表 | `0x2E979BA8 .. 0x2E97A8A8` (= EOF) |
| 锚点 `0x20F99BEF` | 位于 `.text` 内,距其结束约 2.6 MB |
`.text` 结束于 `0xE63C000 + 0x12BDB484 = 0x21217484`。锚点位于代码节结束前 `0x21217484 − 0x20F99BEF = 0x27D895 ≈ 2.6 MB`,正处在可执行代码中,不在任何数据区域,也没有追加到任何地方。
> **说明 —** 因为在这个镜像中 `.text` 的文件偏移与虚拟地址相同(段在 `0xE63C000..0x21217484` 范围内按 1:1 布局),所以下面的每个偏移同时也是文件偏移和运行时 vaddr。这就是为什么 `objdump --start-address=0x20F99BEC` 可以在无需 offset/vaddr 转换的情况下正确反汇编磁盘上的字节。
### grep 返回零的伪象
对该文件运行普通的 `grep -c -a -P '\x28\xb5\x2f\xfd'` 会返回 **0** 个命中,也就是 forensics overview 标出的结果。这是工具伪象,不是缺失证据:GNU `grep` 以行为缓冲,并会误处理一个布满 NUL 字节和超长“行”的 745 MB 二进制。分块的精确字节扫描(读取 16 MB 窗口,保留 3 字节重叠,使用 `bytes.find`)会返回真实计数:
```text
total occurrences of 28 b5 2f fd: 5
0x20F99BEF (553,229,295)
0x20F9B2FE (553,235,198)
0x2100C714 (553,699,092)
0x2100C72A (553,699,114)
0x2100C77D (553,699,197)陷阱 — “magic byte scan returned zero hits” 可能意味着扫描器错了,而不是字节不存在。对这个二进制的诚实答案是五处出现,全部在
.text内。先使用二进制安全扫描器(分块bytes.find、xxd | rg,或理解十六进制的工具),再判断某个 magic 是否缺失。
五处 Magic 出现
每一处是什么
每一处都是 libzstd 指令的立即操作数。没有一处是数据。下表是完整清单。
| 文件偏移 | 所在函数 | 函数基址 | 指令 | 角色 |
|---|---|---|---|---|
0x20F99BEF | ZSTD_compressEnd_public | 0x20F99AC0 | movl $0xfd2fb528,(%r14)(位于 0x20F99BEC,+0x12C) | 将 magic 写入输出缓冲区(空帧收尾逻辑) |
0x20F9B2FE | ZSTD_writeFrameHeader | 0x20F9B200 | movl $0xfd2fb528,(%rdi)(位于 0x20F9B2FC,+0xFC) | 在每个写出帧的开头写入 magic |
0x2100C714 | ZSTD_getFrameHeader_advanced | 0x2100C6C0 | movl $0xfd2fb528,-0x1c(%rbp)(位于 0x2100C711,+0x51) | 在 header memcpy 前写入 stack-local 哨兵 |
0x2100C72A | ZSTD_getFrameHeader_advanced | 0x2100C6C0 | cmpl $0xfd2fb528,-0x1c(%rbp)(位于 0x2100C727,+0x67) | 将复制后的 stack-local 与 magic 比较 |
0x2100C77D | ZSTD_getFrameHeader_advanced | 0x2100C6C0 | cmp $0xfd2fb528,%ecx(位于 0x2100C77B,+0xBB) | 验证输入帧 magic;不匹配则 jne 到错误 |
第三和第四行在同一个 ZSTD_getFrameHeader_advanced 基本块内相距 22 字节(0x2100C711/0x2100C727):第一条把 magic 写入一个 stack-local 作为哨兵,然后 memcpy 把候选 header 覆盖到它上面,第二条再把结果与 magic 比较。它们属于同一套 header 验证逻辑,不是相互独立的发现。
怪癖 — 该常量在 codec 的两侧都会出现。
ZSTD_writeFrameHeader和ZSTD_compressEnd_public会发出它;ZSTD_getFrameHeader_advanced会检查它。朴素的“搜索 magic 来寻找载荷”会把写 header 的编码器和验证 header 的解码器都当作已存储帧。两者都不是。
0x2100C77B 处的解码器侧检查
这些内容是代码而不是数据的最清楚单点证明,是解码器中的 magic 验证 cmp。反汇编会读取输入缓冲区的前四个字节,并将其与常量比较:
2100c779: 8b 0e mov (%rsi),%ecx ; load 4 bytes of input frame
2100c77b: 81 f9 28 b5 2f fd cmp $0xfd2fb528,%ecx ; compare to ZSTD_MAGICNUMBER
; jne -> "not a zstd frame" error path
```text
`%rsi` 是调用者提供的源指针。该常量是解码器要求**外部**输入具备的*期望* magic,正好与已存储帧相反。4 字节 `cmp` 立即数 `28 b5 2f fd` 从文件偏移 `0x2100C77D` 开始(opcode `81` 位于 `0x2100C77B`,ModR/M `f9` 位于 `0x2100C77C`,随后是立即数);carve 锚定的就是这个立即数。
---
## 空帧收尾逻辑 — `ZSTD_compressEnd_public`
### 为什么该函数会写入 magic
`ZSTD_compressEnd_public`(`0x20F99AC0`,大小 `0x27F` = 639 字节;下一个符号 `ZSTD_createCDict_advanced` 位于 `0x20F99D40`)是上游 zstd 的流终结器。它会冲刷最后一个块,并在退化的“压缩 0 字节 / 流从未开始”情况下,直接在调用者的输出缓冲区中合成一个完整的**空 zstd 帧**。`movl $0xfd2fb528,(%r14)` 就位于这个合成逻辑中。`%r14` 保存 `dst` 指针,即调用者拥有的堆缓冲区,从来不是 ELF 镜像中的某个区域。
### 算法
```c
function ZSTD_compressEnd_public(cctx, dst, dstCap, src, srcSize): // sub @ 0x20F99AC0
// ... normal flush path elided ...
if cctx.stage == init_only: // degenerate "empty frame" case
out = (byte*)dst; // %r14 = dst (heap, caller-owned)
// --- build the 6-byte frame header from cctx.cParams ---
WD = window_descriptor(cParams.windowLog); // movzbl 0xf4(%rbx); shl/add
chk = (cParams.checksumFlag != 0); // setne %dl -> Content_Checksum_flag
did = (cParams.dictID > 0); // setg %dil -> Dictionary_ID present
FHD = assemble_frame_header_descriptor(chk, did); // shl/or into FHD bit positions
// --- emit the empty frame, 10 bytes total ---
*(uint32_t*)out = 0xFD2FB528; // 0x20F99BEC: movl -> magic 28 b5 2f fd
r8 = 4; // 0x20F99BF3: cursor past the 4-byte magic
out[r8 + 0] = FHD; // 0x20F99C01: Frame_Header_Descriptor
out[r8 + 1] = WD; // 0x20F99C05: Window_Descriptor
*(uint16_t*)&out[r8 + 2] = 0x0001; // 0x20F99C10: block header, last=1 type=Raw
out[r8 + 4] = 0x00; // 0x20F99C18: 1-byte raw block content
return 10; // magic(4)+FHD(1)+WD(1)+blkhdr(3)+content(1)原始字节及其含义
如果把 0x20F99BEF 之后的字节按十六进制转储读取,它们看起来像是帧数据,而这正是朴素 carve 被欺骗的原因:
20f99bef: 28 b5 2f fd 41 b8 04 00 00 00 85 f6 0f b6 c9 0f
20f99bff: 44 f9 43 88 14 06 43 88 7c 06 01 c7 03 02 00 00 00
```text
按 x86-64 反汇编时,它们是一段连贯的指令流:`movl $magic,(%r14)`;`mov $0x4,%r8d`;`test %esi,%esi`;`movzbl %cl,%ecx`;`cmove %ecx,%edi`;`mov %dl,(%r14,%r8,1)`;……并流向该函数的 `vzeroupper`/`call ZSTD_trace_compress_end` 出口。若(误)按 zstd 帧反汇编,它们会立即产生“frame parameters too large”错误:伪 header 之后的字节会解码出 33 的 window log(`2^33` = 8 GiB,超过 `ZSTD_WINDOWLOG_MAX = 31`),并且第一个块大小远超 128 KiB 块上限。zstd 会拒绝这样的帧;carve 的“4.1 MB”长度来自签名处理器沿着可执行代码追逐无意义的块大小字段,直到出错为止,结束位置在某个 MLIR pass 中间,仍然位于 `.text` 内。
> **怪癖 —** 没有交叉验证的 4 字节 magic 检测器,会在*任何*通过 32 位 `mov` 立即数构造 zstd 帧的代码路径上误报。可靠的判断点是所在符号:如果命中位于名为 `ZSTD_writeFrameHeader`、`ZSTD_compressEnd_*` 或 `ZSTD_*FrameHeader*` 的函数中,就是代码立即数,不是已存储帧。carve 之前先把锚点解析到符号表。
---
## 字典机制 — 不存在静态字典
### 缺失的符号否定了恢复方案
字典恢复假设需要找到 `ZSTD_DCtx_loadDictionary` 调用点,向后回溯每个调用点到 `.rodata`/`.data` 中的固定字典偏移,并匹配帧头中的 dictionary-ID。该前提在第一步就失败了:这个二进制中不存在公共的**解压侧**字典加载器。
| 恢复方案假定的符号 | 是否存在于 `libtpu.so`? | 地址 |
|---|---|---|
| `ZSTD_DCtx_loadDictionary` | **否** | — |
| `ZSTD_DCtx_loadDictionary_byReference` | **否** | — |
| `ZSTD_DDict_createByReference` | **否** | — |
| `ZSTD_loadDictionaryContent` | 是(压缩器内部) | `0x20FA0020` |
| `ZSTD_dedicatedDictSearch_lazy_loadDictionary` | 是(压缩器 match-finder) | `0x20FBB120` |
| `ZSTD_DDict_dictContent` | 是 | `0x2100C100` |
| `ZSTD_createDDict_advanced` | 是 | `0x2100C200` |
| `ZSTD_DCtx_refDDict` | 是 | `0x2100E300` |
| `ZSTD_decompressDCtx` | 是 | `0x2100D3E0` |
| `ZSTD_decompressStream` | 是 | `0x2100E840` |
实际存在的两个名称包含 “loadDictionary” 的符号是**压缩器**内部符号(`ZSTD_loadDictionaryContent` 把字典摄入 `ZSTD_CCtx`;`ZSTD_dedicatedDictSearch_lazy_loadDictionary` 是 lazy match-finder 辅助函数)。二者都不会把字典加载到解压上下文,也没有接到这个镜像中的静态缓冲区。没有 `ZSTD_DCtx_loadDictionary`,就不存在“把固定嵌入式字典加载到 DCtx 并解压 blob”的可逆向路径。
### 运行时字典路径实际是什么
现有的 `ZSTD_DCtx_refDDict` 和 `ZSTD_createDDict_advanced` 是**引用运行时提供的已消化字典**的 API。字符串 sidecar 显示它们由哪里驱动:riegeli 的 zstd reader/writer 和一个 RPC/program 压缩层,而不是静态 blob:
```text
REQUEST_COMPRESSION_ZSTD ZstdReaderBase ZSTD_DCtx_refDDict
RESPONSE_COMPRESSION_ZSTD ZstdReaderINS_ ZSTD_decompressStream
TPU_CORE_PROGRAM_COMPRESSION_ZSTD ZStdCompressor ZSTD_CCtx_refCDict
zstd_compression_level zstd_reader / zstd_writer这些名称指向用于 RPC 载荷和 TPU-core 程序的流式压缩设施。此类流使用的任何字典都由应用在运行时传入(JAX/TF 制品、RPC 协商),通过 ZSTD_DCtx_refDDict 附加,并且从不从 libtpu.so 的固定区域读取。
唯一嵌入的 zstd 参数数据
镜像中烘焙的唯一 zstd 参数区域是 8 字节,不是字典:
| 符号 | Vaddr / file off | 节 | 字节 | 解码后 |
|---|---|---|---|---|
tpu::(anonymous)::kZstdParams | 0x0B831ACC | [11] .rodata | 01 00 00 00 1B 00 00 00 | { level = 1, window_log = 27 } |
这是在没有 override 时构造 TPU zstd compressor 使用的默认 tpu::ZStdParams 结构(level 1,128 MiB window)。它是配置,不是内容,更不可能是 4 MB 字典。
运行时解压路径 — 替代 blob 的实际存在物
没有什么需要定位或解压
因为不存在静态 blob,也不存在静态字典,所以没有“运行时定位 + 解压末尾 blob”的序列可供记录;本页标题提出的问题,其答案是该操作并未发生。实际存在的运行时 zstd 代码是通用的静态链接 libzstd,由 riegeli 驱动,对来自 .so 外部的数据做流式压缩:
application / RPC layer
└─ supplies a compressed stream (+ optional dictionary buffer) at runtime
└─ riegeli ZstdReaderBase
└─ ZSTD_createDCtx (0x2100C480)
└─ ZSTD_DCtx_refDDict (0x2100E300) ── attach app-supplied digested dict
└─ ZSTD_decompressStream (0x2100E840) ── stream-decompress into output
└─ ZSTD_getFrameHeader_advanced (0x2100C6C0) ── validates magic (the cmp above)
```text
`0x2100C77B` 处的 frame-magic `cmp` 位于这条解码路径上:它验证应用送入的帧。blob 搜寻把这个验证常量以及编码器的发射常量误认为了已存储数据。
### 想象中的载荷实际位于何处
人们可能期望这种 blob 解压出的硬件常量包,在这个二进制中确实是真实制品,但它是一个从数据节解析出的**未压缩 protobuf**,另有文档说明:
- 按 codename 区分的硬件常量和 `chip_parts` binarypb 包在 [`targets/chip-parts-binarypb.md`](../targets/chip-parts-binarypb.md) 和 [`targets/per-codename-hw-constants.md`](../targets/per-codename-hw-constants.md) 中说明。它们没有经过 zstd 压缩,也不依赖任何 blob 恢复。
> **说明 —** 对语料库其余部分可泛化的经验是:任何静态链接 libzstd 的二进制(crucible-notes 中有若干个)都会在 `ZSTD_writeFrameHeader` / `ZSTD_compressEnd_*` / `ZSTD_*FrameHeader*` 函数通过 `mov` 立即数构造帧的位置产生同样的误报 carve。若某个 zstd magic 命中的锚点解析到 `.text` 内,应先把它当作代码立即数,除非能够通过所在*数据*节和有效 header(window-log ≤ 31,block-size ≤ 128 KiB)证明它是已存储帧。
---
## 相关组件
| 名称 | 关系 |
|---|---|
| `ZSTD_compressEnd_public` (`0x20F99AC0`) | `0x20F99BEF` 误报 magic 的来源(空帧收尾逻辑) |
| `ZSTD_writeFrameHeader` (`0x20F9B200`) | `0x20F9B2FE` magic 的来源(每个发射出的帧头) |
| `ZSTD_getFrameHeader_advanced` (`0x2100C6C0`) | `0x2100C714/72A/77D` magic 的来源(输入帧验证) |
| riegeli `ZstdReaderBase` / `ZStdCompressor` | 真正的运行时 zstd 表面 — 流式处理,应用提供字典 |
| `tpu::(anonymous)::kZstdParams` (`0x0B831ACC`) | 唯一嵌入的 zstd 配置:`{level=1, window_log=27}`,8 字节 |
## 交叉引用
- [Forensics Overview](overview.md) — 将 magic-byte 谜题指向这里;其关于节表在 EOF 结束的陷阱在本页解决。
- [ELF Anatomy](elf-anatomy.md) — 结尾与 EOF 重合的节表;完整的 `[0]..[51]` 布局表明没有末尾区域。
- [Embedded Library Atlas](embedded-library-atlas.md) — 静态链接的 libzstd(309 个 `ZSTD_*` 符号),其代码立即数产生了五处 magic 命中。
- [Custom Sections](custom-sections.md) — 确实承载载荷的非标准数据节(`filewrapper_toc`、`.lrodata` 等),但都不是 zstd blob。
- [chip-parts binarypb](../targets/chip-parts-binarypb.md) — 真实的、未压缩的硬件常量包,从数据节解析,而不是从任何压缩 blob 解码。
- [Per-Codename HW Constants](../targets/per-codename-hw-constants.md) — 按 codename 区分的硅片常量,从数据节解析,不来自任何压缩 blob。