# 构建与检查

> 固定为同一语义的构建 profile、构建产出的 ELF、加载器由它生成的 ProgramImage 及其报告，以及验证者登记的程序身份。

## 构建

在客户程序（guest）的目录下构建，除了目标之外不加任何参数：

```sh
cd guests/my-app
cargo build --release --target riscv32imac-unknown-none-elf     # .../release/my-app
cargo build --target riscv32imac-unknown-none-elf               # .../debug/my-app
```

ELF 生成在客户程序工作空间唯一的 target 目录 `guests/target/riscv32imac-unknown-none-elf/` 中。`guests/.cargo/config.toml` 会加上两个你永远不必手动输入的链接器参数：

- **`-T crates/guest-sdk/link.ld`**，即内存布局，它还定义了启动代码和分配器所用的符号。
- **`--no-relax`**。链接器松弛（relaxation）会改写指令序列，并移动其后的每一个地址，而程序身份绑定了这些地址。

没有 `runner`：远地虚拟机之外没有任何东西能映射客户程序的内存区域，所以 `cargo run` 无从运行它。客户程序要通过模拟器运行（[运行与性能分析](https://apogee.gweb3networks.com/docs/launch/run)）。

## 构建 profile

`guests/Cargo.toml` 把两个 profile 固定为同一语义。它们只在优化级别和依赖的调试断言上有所不同：

| | dev | release |
| --- | --- | --- |
| `opt-level` | 0 | 3 |
| `overflow-checks` | 开启 | **开启** |
| `debug-assertions` | 开启 | 客户程序 crate 中开启，其依赖中关闭 |
| `panic`、`codegen-units`、`debug`、`incremental` | `abort`、1、关闭、关闭 | 相同 |

Cargo 默认的 release profile 会关闭溢出检查，而在客户程序中，这不是一个性能设置。它会改变陈述：`u32::MAX + 1` 会提交 `00000000` 并以 0 退出，而 dev 构建在同一处会 panic，以 101 退出。所以工作空间在两个 profile 中都保持检查开启。依赖的调试断言检查的是该 crate 自身的不变量，正确的依赖在没有断言时计算结果相同，所以 release 关闭它们，省下这部分周期：相当于无状态以太坊客户程序运行周期的 6.8%。

**证明 release 构建。** 每条执行过的指令都是一行被证明的数据；`opt-level = 3` 能去掉客户程序映像的四分之一到一半以上；而且每个电路族的代码都必须装进它的解码表：以太坊客户程序的 debug 映像需要 `2^22` 行的表，release 映像只需 `2^20`。你发布的程序身份，是 release 映像的程序身份。

## 可复现性

同一台机器上的两次干净构建，会产出完全相同的 ELF。两台机器上的构建一般则不会：ELF 在 panic 位置字符串中嵌入了绝对路径，涉及工具链的 `core` 源码、`crates/guest-sdk` 和 cargo registry，而客户程序自己的文件则以相对于 `guests/` 的路径出现。在别处构建，得到的是另一个映像，以及另一个程序身份。

所以你登记并交付的是**一次构建产出的 ELF，而不是构建方法**。保留你证明过的那个 ELF，想核对程序身份的人可以从这个 ELF、参数和仪式文件重新计算它。

## 导出映像

```sh
cargo run -p artifact-dump -- guests/target/riscv32imac-unknown-none-elf/release/my-app --out artifacts
```

该命令写出 `artifacts/my-app.img`，即加载后的 `ProgramImage` 的序列化格式（`postcard`，无文件头），以及 `artifacts/my-app.img.txt`：一份根据经带校验的读取器读回的映像生成的报告。它会打印入口点、段数和指令数，以及制品的大小和 SHA-256。如果读回的结果不一致，或者加载器拒绝了该 ELF，它什么也不写。

`.img` 是程序的静态描述，用于保存和比对差异；下游没有任何东西需要它，因为设置步骤和各种工具接受的都是 ELF。它的 SHA-256 固定的是字节。它**不是**程序身份。

## 阅读报告

| 部分 | 内容 |
| --- | --- |
| `entry and memory` | 入口，即位于 `0x00010000` 的 `_start`；RAM 窗口；`slot_base` 和槽位跨度 |
| `segments` | 每个段的地址、结束位置、`mem_len`、文件字节数、零填充和指令数：`.text`、`.rodata`（如果有），以及一个延伸到 `0x80000000`、用于 `.data`、`.bss`、堆和栈的可写段 |
| `instruction stream` | 四字节和两字节指令、指令中间的槽位以及 `not code` 槽位，合计等于槽位总数 |
| `symbols` | 取自 ELF 符号表、按地址排列的名称，制品本身不包含这些名称 |
| `listing` | 逐条指令列出：地址、长度、内存中的字节、展开后的 32 位字、符号 |

压缩指令保留自己的地址和两个字节；下一个 pc 是 `pc + 2` 还是 `pc + 4`，只由 `len` 表明。要看助记符，使用下面的 `tables` 视图，或者固定版本的反汇编器：

```sh
"$(rustc --print sysroot)"/lib/rustlib/*/bin/llvm-objdump \
    --disassemble --no-print-imm-hex -M no-aliases <elf>
```

### `not code` 半字

报告中可能出现 `---- not code: 0x00010f9a .. 0x00010f9c, 1 halfword ----` 这样的行。这是普通的编译器输出：LLVM 证明了某个 `match` 的默认分支不可达，rustc 为这个不可达块生成了 `unimp`，在 C 扩展下它就是 `c.unimp`，即全零半字，RVC 中明确定义为非法的编码。加载器把它记录为非指令，然后继续。没有任何 pc 会到达它；如果有，运行会以 `NotAnInstruction` 终止。

## 虚拟机将要证明的内容

```sh
cargo run --release -p artifact-dump -- tables <elf> --ptau assets/ptau/ppot_0080_24.ptau
```

该命令在默认参数下打印映像推导出的 `VmConfig`：每个电路族的高度、有效行数和解码列。随后打印每条指令的 pc、`next_pc`、电路族、助记符和各字段。加上 `--ptau` 和仪式文件，它还会打印**程序身份**，即验证者要登记的值。[快速上手](https://apogee.gweb3networks.com/docs/launch/quickstart#inspect)展示了一个真实的例子。

程序身份是一个域元素。它绑定每条指令及其 pc、长度、操作数和类别（kind），映像中每个来自文件的字节（`.text`、`.rodata`、`.data`），入口点，电路族集合，每个高度，代码大小上限和代码版本。它不绑定符号表、`.bss`，也不绑定任何由执行过程决定的东西。同一个 ELF 在两种高度设置下有两个程序身份。

推导会在以下情况下拒绝，并指出出错的 pc 或大小：

| 拒绝 | 原因 |
| --- | --- |
| `Not all opcodes supported: pc=…` | 可执行代码中任何位置出现了 RV32IMA 之外的字，例如汇编中的 CSR 访问 |
| `TableTooShort` | 代码超出了电路族解码表的覆盖范围 `pc ≤ 2h − 4`：高度为 `2^20` 时可覆盖 1.9375 MiB 代码，`2^22` 时为 7.9375 MiB |
| `ProgramTooLarge` | 映像超出了 `bytecode_size_words`，默认为 4 MiB |
| `ImageOutsideWindow` | 在所选的窗口高度下，有来自文件的字节位于 RAM 窗口 0 之外 |
| `UnknownDelegation` | 映像声明了一个没有任何电路族响应的委托编号 |

## 核对构建

构建到一个全新的 target 目录，再次导出，然后比较：

```sh
cd guests/my-app
CARGO_TARGET_DIR=/tmp/fresh cargo build --release --target riscv32imac-unknown-none-elf
cd ../..
cargo run -p artifact-dump -- /tmp/fresh/riscv32imac-unknown-none-elf/release/my-app --out /tmp/again
cmp artifacts/my-app.img /tmp/again/my-app.img
diff artifacts/my-app.img.txt /tmp/again/my-app.img.txt     # differs only in the `source ELF` line
```
