Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第九周 Bug 发现:SpacemiT K3 A100 偶发产生非法 stval

2026 年 9 月 18 日确认与复盘:此前将该现象归因为 K3 板端硬件错误的结论不成立,根因在软件。软件使执行地址跳出 Sv39 的可表示范围;sepc(EPC)被截断,而 stval 依照规范记录了完整的 faulting virtual address,才造成了两者高位不一致的表象。

这是一次低级的排查错误。发现异常地址后,没有在 QEMU 与 K3 板上对同一地址和同一场景做真实对比,而是一味假定是板端错误;还误以为 QEMU 的实现正确,因此没有参考价值、不能帮助反映 K3 的问题。实际上,QEMU 的正确行为正应作为对照基线。此前的排查思路有误,以下原始复现记录和采样证据保留,末尾结论已据此更正。

完整原始日志见:exception.md

相关源码快捷跳转:

复现步骤

EFI:target/starry-grub/spacemitk3-com260kit/starryos.efi。

对应的调试符号表压缩包为 target/riscv64gc-unknown-none-elf/release/starryos.xz。使用 JTAG/GDB 前解压:

xz -dk target/riscv64gc-unknown-none-elf/release/starryos.xz

解压得到的 target/riscv64gc-unknown-none-elf/release/starryos 即为这份 EFI 对应的 ELF 符号表。

上传时直接指定该 EFI:

python3 board_script/k3com260kit/sync_kernel.py \
  --kernel target/starry-grub/spacemitk3-com260kit/starryos.efi \
  <板子 IP>

对应 release ELF 的函数断点地址如下:

符号地址
riscv64_raw_entry0xffffffff80001000
someboot::arch::entry::kernel_entry0xffffffff8035d700
someboot::arch::entry::enter_from_efi0xffffffff8035d748
someboot::arch::entry::mmu_entry0xffffffff8035d810
someboot::arch::entry::mmu_entry_rust0xffffffff80360078
someboot::arch::entry::primary_entry_from_efi0xffffffff8036015a
trap_vector_base0xffffffff8036a09c
riscv_trap_handler0xffffffff8036ac84
ax_cpu::riscv::trap::handle_page_fault0xffffffff8036a5d8
starry_kernel::mm::access::handle_page_fault0xffffffff8013713e

trap_vector_base 是 trap 汇编入口。trap.S/trap_tls.S 会先读取 stval、scause、sepc 和 sstatus,再跳转到 riscv_trap_handler;调试时使用上面这份 ELF 执行 file 或设置符号断点即可。

复现教程

前置条件

  • K3 开发板能够启动一个同时具备 GRUB、SSH 登录和正常网络连接的系统;
  • 已知开发板的 IP 地址及 root 用户密码。

部署 StarryOS 内核

在主机上克隆 integer-k3-board-support 分支并进入仓库:

git clone --branch integer-k3-board-support --single-branch \
  https://github.com/Dirinkbottle/tgoskits.git
cd tgoskits

确保开发板已启动到上述可通过 SSH 访问的系统后,运行同步脚本(将 <板子 IP> 替换为实际地址):

python3 board_script/k3com260kit/sync_kernel.py \
  --kernel target/starry-grub/spacemitk3-com260kit/starryos.efi \
  <板子 IP>

需要确保脚本中的BOARD_ROOT_DEVICE = “/dev/sda3“和BOARD_ESP_DEVICE = “/dev/sda1“对应的分区准确无误,如有不对请手动调整 脚本执行过程中会要求输入开发板 root 用户的密码。同步完成后,StarryOS 内核会被加入开发板的 GRUB 菜单。

触发异常

  1. 重启开发板,并在 GRUB 菜单中选择 StarryOS 启动。

  2. 进入 StarryOS shell 后执行:

    echo 1 > /proc/set_ai_thread
    

    此操作会将当前进程切换到 A100 核心 8(CPU8);在本次复现环境中,首次执行可确认运行在 core 8。

  3. 随后执行一个会触发指令缺页的命令,例如:

    ls或者sync
    # 或其他能触发指令缺页的命令
    

预期结果

StarryOS 会通过 error! 输出观察到的异常 stval != sepc 情况。典型日志中,stval 的低位与 sepc 一致,但 stval 带有不符合 Sv39 canonical address 规则的高位,例如:

Instruction page fault: stval=0xffffff0000c68dfc, sepc=0xc68dfc

取指跨页时 stval 与 sepc 不同本身可以是正常现象;本复现关注的是上例这种 stval 高位异常、且不是合法 Sv39 canonical 地址的情况。

原始日志(exception.md 节选)

以下内容直接截取自完整日志,异常发生在 SpacemiT K3 的 A100 核心 CPU8 上:

root@starry:/root # echo 1 > /proc/set_ai_thread
root@starry:/root # ls
[ 58.494400 8:122 starry_kernel::task::user:70] Enter user space: ip=0xf481c, sp=0x3fffff970
[ 58.939963 8:43 starry_kernel::syscall::task::wait:245] sys_waitpid <= pid: -1, options: WaitPidOptions(WUNTRACED)
[ 59.040106 8:122 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0xffffff0000c68dfc, sepc=0xc68dfc
[ 59.048726 8:122 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0xffffff0000c13400, sepc=0xc13400
[ 59.059760 8:122 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0xffffff0000c0a8aa, sepc=0xc0a8aa
[ 59.071088 8:122 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0xffffff0000bdd5e6, sepc=0xbdd5e6
spacemit-toolchain-linux-glibc-x86_6[ 59.104138 8:122 starry_kernel::task::ops:398] Task(122, "ls") exit with code: 0
4-v1.2.4.tar.xz

同一份日志中,ort_learn 还捕获到了正常的跨页取指异常:

[ 81.622547 8:124 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0x29000, sepc=0x28ffe
[ 81.660874 8:124 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0xffffff0000171ffc, sepc=0x171ffc
[ 81.679713 8:124 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0x5c3000, sepc=0x5c2ffe
[ 81.704560 8:124 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0x5b2000, sepc=0x5b1ffe
[ 81.728771 8:124 ax_cpu::riscv::uspace:144] Instruction page fault: stval=0x5c6000, sepc=0x5c5ffe

其中第一条是合理的跨页场景;第二条仍然是非 canonical stval,但它的低位地址与 sepc 相同。

现象

在 SpacemiT K3 的 A100 核心(CPU8)上运行 StarryOS 时,用户态程序发生 Instruction Page Fault 后,硬件 stval CSR 偶发返回非 Sv39 canonical 地址。

异常值具有以下特征:

  • stval 的低 39 位与实际 faulting virtual address 一致;
  • stval 的高位被填充为 0xffffff...,但低地址的 bit38 为 0,因此不满足 Sv39 canonical address 规则;
  • sepc 仍保持正常的低地址形式;
  • 同一 CPU8 上既能复现异常值,也能捕获符合规范的跨页取指异常。

典型异常值为:

stval = 0xffffff0000c68dfc
sepc  = 0x0000000000c68dfc
scause = 0xc

同一程序在 CPU8 上可反复出现类似结果:

stval=0xffffff0000c68dfc, sepc=0xc68dfc
stval=0xffffff0000c13400, sepc=0xc13400
stval=0xffffff0000c0a8aa, sepc=0xc0a8aa
stval=0xffffff0000bdd5e6, sepc=0xbdd5e6

影响

StarryOS 原本直接使用 stval 查找用户 VMA。因此,同一异常可能出现以下结果:

stval = 0xffffff0000bcb26a
sepc  = 0x0000000000bcb26a

find_area(stval) = false
find_area(sepc)  = true

本来应该由 demand paging 处理的缺页被误判为非法地址,最终产生 SIGSEGV。将异常 stval 按 Sv39 规则重新 canonicalize 后,程序可以继续执行并正常退出。

规范判断

Sv39 使用 39 位虚拟地址。对于 RV64,合法 Sv39 虚拟地址必须满足:

VA[63:39] = {25{VA[38]}}

也就是:

bit38 = 0  => bits[63:39] 必须全部为 0
bit38 = 1  => bits[63:39] 必须全部为 1

因此:

0x0000000000c68dfc

是合法的 Sv39 低地址,而:

0xffffff0000c68dfc

不是合法的 Sv39 canonical address:它的 bit38 为 0,但 bits[63:39] 并非全 0。

对于 Instruction Page Fault,sepc 保存发生异常的指令起始地址;在实现支持有效 trap value 时,stval 应保存导致异常的 faulting virtual address。

变长指令跨页时,二者可以不同。例如:

sepc  = 0x28ffe
stval = 0x29000

这表示指令从 0x28ffe 开始,指令后半部分位于下一页,并在 0x29000 处发生取指异常。这种差异符合规范。

但以下情况不存在跨页关系:

sepc  = 0x0000000000c68dfc
stval = 0xffffff0000c68dfc

stval 本身还是非法 Sv39 地址,因此不能用正常的 instruction page fault 语义解释。

证据

1. trap.S 在 trap entry 直接读取硬件 CSR

源码:查看 trap.S 第 50–73 行

以下是 trap.S 中的原始代码:

.Ltrap_saved:
    csrr    t3, stval
    csrr    t4, scause
    csrr    t5, sepc
    csrr    t2, sstatus
    STR     t5, sp, 32          // tf.sepc
    STR     t2, sp, 33          // tf.sstatus
    STR     t3, sp, 34          // tf.stval
    STR     t4, sp, 35          // tf.scause

    // Kernel Rust always sees the psABI global pointer, trusted kernel tp, and
    // canonical sscratch=0.
    csrw    sscratch, zero
    .option push
    .option norelax
    lla     gp, __global_pointer$
    .option pop

    andi    t2, t2, 1 << 8      // sstatus.SPP == 1
    beqz    t2, .Lexit_user

    mv      a0, sp
    la      ra, .Ltrap_return
    j       riscv_trap_handler

这里 csrr t3, stval、csrr t5, sepc 在进入 Rust handler 之前执行,并立即写入 trap frame。该路径没有对 stval 做高位拼接或地址转换,因此可以作为硬件 CSR 值的第一手采样点。

2. trap_tls.S 的另一条 trap entry 路径采用相同的 CSR 保存方式

源码:查看 trap_tls.S 第 39–58 行

以下是 trap_tls.S 中的原始代码:

.Ltrap_saved:
    csrr    t3, stval
    csrr    t4, scause
    csrr    t5, sepc
    csrr    t2, sstatus
    STR     t5, sp, 32
    STR     t2, sp, 33
    STR     t3, sp, 34
    STR     t4, sp, 35
    csrw    sscratch, t0
    .option push
    .option norelax
    lla     gp, __global_pointer$
    .option pop

    andi    t2, t2, 1 << 8
    beqz    t2, .Lexit_user
    mv      a0, sp
    la      ra, .Ltrap_return
    j       riscv_trap_handler

这个配置下仍然是先读取 stval/scause/sepc,再将它们分别保存到 trap frame 的固定位置。两种配置的 CSR 采样和保存逻辑一致,因此至少可以排除“某一份汇编在保存 stval 时额外拼接了高位”这一类原因;当前日志中的异常是在该保存路径上观察到的。

3. context.rs 中的 trap frame 字段顺序与保存位置对应

源码:查看 context.rs 第 15–52 行 和 第 327–341 行

以下是 context.rs 中的原始代码:

#![allow(unused)]
fn main() {
/// General registers of RISC-V.
#[allow(missing_docs)]
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
pub struct GeneralRegisters {
    pub zero: usize,
    pub ra: usize,
    pub sp: usize,
    pub gp: usize,
    pub tp: usize,
    pub t0: usize,
    pub t1: usize,
    pub t2: usize,
    pub s0: usize,
    pub s1: usize,
    pub a0: usize,
    pub a1: usize,
    pub a2: usize,
    pub a3: usize,
    pub a4: usize,
    pub a5: usize,
    pub a6: usize,
    pub a7: usize,
    pub s2: usize,
    pub s3: usize,
    pub s4: usize,
    pub s5: usize,
    pub s6: usize,
    pub s7: usize,
    pub s8: usize,
    pub s9: usize,
    pub s10: usize,
    pub s11: usize,
    pub t3: usize,
    pub t4: usize,
    pub t5: usize,
    pub t6: usize,
}
}
#![allow(unused)]
fn main() {
/// Saved registers when a trap (interrupt or exception) occurs.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct TrapFrame {
    /// All general registers.
    pub regs: GeneralRegisters,
    /// Supervisor Exception Program Counter.
    pub sepc: usize,
    /// Supervisor Status Register.
    pub sstatus: sstatus::Sstatus,
    /// Supervisor Trap Value captured at trap entry.
    pub stval: usize,
    /// Supervisor Cause captured at trap entry.
    pub scause: usize,
}
}

GeneralRegisters 恰好包含 32 个机器字,因此在 RV64 下:

trap frame index 32 -> sepc
trap frame index 33 -> sstatus
trap frame index 34 -> stval
trap frame index 35 -> scause

这与两个 trap entry 文件中的 STR 位置完全对应,说明当前观察到的高位异常不是由于 TrapFrame 字段顺序或 stval/sepc 槽位错位造成的。

排查结论

经 9 月 18 日重新确认,根因是软件而非 A100 硬件 erratum:软件使地址跳出 Sv39 的表示范围,sepc(EPC)又被截断;相对地,stval 按规范保留了完整的 faulting virtual address。因此日志中看到的高位不一致并非 K3 错误写回 stval。

此前虽然确认了 trap entry 会直接采样 CSR、trap frame 字段未错位,也观察到了跨页取指场景,但没有把异常地址在 QEMU 与 K3 板上进行同条件的真实对比,过早地将问题归因为板端。QEMU 的正确实现应当作为行为基线,而不是被排除在参考之外。后续排查此类异常时,应先对比两端的完整地址、sepc/stval 语义和地址截断路径,再判断是否存在硬件问题。