RV32E + WritebackStage=0: illegal instruction using x16-x31 in rd/rs1 still writes CSRs

未关闭
#2,547 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 2 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃

调研方向

Start with rtl/ibex_decoder.sv, especially the illegal_reg_16 logic and side-effect suppression block, then trace how csr_access_o and data_req_o reach the CSR and LSU paths. Reproduce the WB=0 behavior using the test setup and programs described in the issue, comparing against WB=1. Done means illegal RV32E register references still trap and have no CSR or LSU side effects, while legal instructions continue to work.

由索引模型根据 Issue 内容生成。

描述

Type:Bug

Bug report for lowRISC/ibex. Target HEAD: a896ef22777a40ad3f5a2a3b97fecd5116480c4c (2026-10-23)

Observed Behavior

With RV32E=1 and WritebackStage=0, an instruction that uses a reserved RV32E register number (x16–x31) in rd or rs1 raises the correct illegal-instruction exception (mcause=2, mepc at the instruction, mtval = the instruction word) and still performs the CSR write in the same cycle. WritebackStage=1 is clean.

Test setup
  • DUT: ibex_core, BaseIsa=RV32I, RV32E=1, WritebackStage=0, RV32M=RV32MFast, RV32B=RV32BNone, RV32ZC=RV32ZcaZcbZcmp, PMPEnable=1, PMPNumRegions=16, ICache=0, SecureIbex=0, DummyInstructions=0, external register file, MemDataWidth=32.
  • Harness: external RF, 64 KiB memory, 1-cycle OBI instruction/data slaves, boot_addr_i=0x2000. Ibex's PC_BOOT is {boot_addr_i[31:8], 8'h80} (rtl/ibex_if_stage.sv:244), so execution starts at 0x2080.
  • Memory is zero-filled. The program is poked by the TB; no program file is needed.
  • The program first writes mtvec=0x2100 and mscratch=0x12345678, then executes one CSR instruction at 0x20A0. Illegal variants trap to the handler at 0x2100, which records the architectural state at 0x3000. A store of 0xD00D to 0x3100 ends the run.
  • mtvec is WARL and its MODE field is fixed to 2'b01 (doc/03_reference/cs_registers.rst:219), so writing 0x2100 reads back 0x0000_2101.

Signature layout (written by the trap handler; the legal control writes SIG[0] on the normal path):

Word Content
SIG[0] mscratch read after the instruction
SIG[1] mtvec
SIG[2] mcause
SIG[3] mepc
SIG[4] mtval
Test programs

The only difference between the five runs is the CSR instruction at 0x20A0 (and the value loaded into t2):

+PROG Instruction at 0x20A0 Encoding
0 csrrw x16, mscratch, t2 (t2=0x55AA55AA) 0x34039873
1 csrrw x16, mtvec, t2 (t2=0x2000) 0x30539873
2 csrrw x16, mscratch, x0 0x34001873
3 csrrw x0, mscratch, x16 0x34081073
4 csrrw x6, mscratch, t2 (legal control) 0x34039373
Results
+PROG WB=0 (unmodified RTL) WB=1 (control)
0 SIG[0]=0x55aa55aa — the CSR write landed SIG[0]=0x12345678 — unchanged
1 TIMEOUT pc=0x2000 mcause=2 — mtvec was overwritten SIG[1]=0x00002101, mcause=2, mepc=0x20a0
2 SIG[0]=0x00000000 SIG[0]=0x12345678
3 SIG[0]=0x00000000 SIG[0]=0x12345678
4 SIG[0]=0x55aa55aa, mcause=0 — legal write works same

Full signature for PROG=0, WB=0:

TB WB=0 PROG=0
RESULT done cycles=58
RESULT SIG[0]=0x55aa55aa   # mscratch after "csrrw x16, mscratch, t2"
RESULT SIG[1]=0x00002101   # mtvec (base 0x2100, mode 01)
RESULT SIG[2]=0x00000002   # mcause = illegal instruction
RESULT SIG[3]=0x000020a0   # mepc = the illegal instruction
RESULT SIG[4]=0x34039873   # mtval = faulting instruction
RESULT SIG[5]=0x00000000
RESULT SIG[6]=0x00000000
RESULT SIG[7]=0x00000000
RESULT mstatus=0c mepc=000020a0 mcause=02 mtval=34039873

Same image with WB=1:

TB WB=1 PROG=0
RESULT done cycles=58
RESULT SIG[0]=0x12345678   # no write; correct
RESULT SIG[1]=0x00002101
RESULT SIG[2]=0x00000002
RESULT SIG[3]=0x000020a0
RESULT SIG[4]=0x34039873

PROG=1, WB=0 (the trapping instruction changes the trap vector):

TB WB=0 PROG=1
TIMEOUT cycles=20001 pc=00002000 mepc=00002000 mcause=02

PROG=1, WB=1:

TB WB=1 PROG=1
RESULT SIG[0]=0x12345678   # mscratch unchanged
RESULT SIG[1]=0x00002101   # mtvec unchanged
RESULT SIG[2]=0x00000002   # mcause
RESULT SIG[3]=0x000020a0   # mepc
RESULT SIG[4]=0x30539873   # mtval

For PROG=2 and PROG=3 the illegal instruction leaves mscratch at 0x00000000 at WB=0 (writing x0 / rs1=x16 = 0), while WB=1 keeps 0x12345678. The legal control (PROG=4) writes 0x55aa55aa and does not trap (mcause=0) in either build.

Root cause

At a896ef2:

  1. rtl/ibex_decoder.sv:202-205 computes the reserved-register condition:

    assign illegal_reg_16 = (RV32E || (cheriot_enable_i == IbexMuBiOn)) &&
                            ((raddr_a[4]   && rf_ren_a_o) ||
                             (raddr_b[4]   && rf_ren_b_o) ||
                             (instr_rd[4]  && rf_we_or_load));
    
  2. The decoder's side-effect suppression block is guarded by illegal_insn only (rtl/ibex_decoder.sv:844-857):

    // make sure illegal instructions detected in the decoder do not propagate from decoder
    // into register file, LSU, EX, WB, CSRs, PC
    if (illegal_insn) begin
      rf_we           = 1'b0;
      data_req_o      = 1'b0;
      data_we_o       = 1'b0;
      jump_in_dec_o   = 1'b0;
      jump_set_o      = 1'b0;
      branch_in_dec_o = 1'b0;
      csr_access_o    = 1'b0;
    end
    

    illegal_reg_16 is merged into illegal_insn_o at rtl/ibex_decoder.sv:1257, but the only effect on the datapath is the RF write mask at :1260:

    assign illegal_insn_o = illegal_insn | illegal_reg_16;
    assign rf_we_o = rf_we & ~illegal_reg_16;
    

    Therefore csr_access_o, data_req_o, data_we_o and the jump/branch requests survive an instruction whose only problem is a reserved register number.

  3. With WritebackStage=0, the ID-stage execution qualifier does not include an exception kill (rtl/ibex_id_stage.sv:1149-1150):

    assign instr_executing_spec = instr_valid_i & ~instr_fetch_err_i & controller_run;
    assign instr_executing      = instr_executing_spec;
    

    The WB=1 path (:1050-1053) does include ~instr_kill, and instr_kill (:1024-1027) includes id_exception_nc, i.e. the illegal-instruction exception:

    assign instr_kill = instr_fetch_err_i |
                        wb_exception      |
                        id_exception_nc   |
                        ~controller_run;
    

    Hence, at WB=0, csr_op_en_o = csr_access_o & instr_executing & ... (:744-746) — and lsu_req (:729) for the load/store form of the same bug — are still asserted in the cycle the trap is recognized. The controller flushes the instruction in the following cycle, but the CSR write has already been committed.

  4. The CSR write itself is not blocked later either. csr_we_int (rtl/ibex_cs_registers.sv:1018-1021) excludes only illegal_csr_insn_o:

    assign csr_we_int = csr_wr & csr_op_en_i
      & (~(BaseIsa == BaseIsaRV32IorCHERIoT) | (cheriot_enable_i != IbexMuBiOn)
         | debug_mode_i | pcc_cap_q.perms.SR)
      & ~illegal_csr_insn_o;
    

    illegal_csr_insn_o covers a non-existent CSR, a write to a read-only CSR, insufficient privilege and debug-only CSRs. It does not cover illegal_reg_16: the addressed CSR (e.g. mscratch, mtvec) exists, is writable, and M-mode is allowed to write it. The write therefore proceeds whenever csr_op_en_i is high.

Why WB=1 is clean: instr_kill includes id_exception_nc, so instr_executing (and therefore csr_op_en_o / lsu_req) is low in the trap cycle. WB=0 has no equivalent term; it relies only on the controller's later flush to discard the instruction, which does not prevent the CSR (or LSU) operation from committing first.

Impact
  • The demonstrated writes are from M-mode (the only mode that can write mtvec). U-mode CSR writes are still rejected by the existing privilege check, and U-mode memory accesses are still bounded by PMP, so this is not an unconditional privilege escalation.
  • In M-mode, a trapping instruction can rewrite mtvec (demonstrated), mepc, mstatus, pmpcfg, pmpaddr, mseccfg and other writable CSRs before the trap is taken. A monitor that treats the illegal-instruction trap as "the instruction had no effect" is unsound.
  • With mtvec as the target the hart can be redirected into non-code memory and never reaches its handler (TIMEOUT at pc=0x2000 above).
  • The same illegal_reg_16 predicate also applies to the runtime CHERIoT 16-register mode, whose production small configuration is WritebackStage=0. The CSR half is narrower there (csr_we_int requires pcc_cap_q.perms.SR or debug mode), but the load/store half uses the normal LSU path. The CHERIoT configuration was not built for this report.
Related upstream issues

Closest issues found (checked against the open/closed list on 2026-10-26):

  • #2192 (open): jalr x2,0(x16) on two consecutive multicycle invalid instructions updates x2. That is a jalr/rd-update path; no CSR write.
  • #2278 (open): a store fetched from PMP non-executable memory drives data_we_o. Same "invalid instruction still drives a bus" symptom class, different trigger and RTL path.
  • #2537 (open): RV32E + Zcmp reserved moves partially update s0/s1.
  • #1132 (open): minstret counts illegal instructions.

None describes a CSR write from an illegal instruction, the mtvec corruption, or the writeback-stage dependence. Issue #2473/#2474 (MPRV with DRET / Debug Mode) are unrelated.

Expected Behavior

RV32E defines only x0–x15; an instruction that references x16–x31 is reserved and must raise an illegal-instruction exception. Ibex documents the 16-register file in doc/03_reference/register_file.rst:7 and doc/03_reference/cheriot.rst:53.

The exception must be precise: the trapping instruction must have no architecturally visible effect, including no CSR write. This is also what the decoder comment at rtl/ibex_decoder.sv:844-848 states as the intent ("do not propagate ... into register file, LSU, EX, WB, CSRs, PC"), and the design already implements the RF half of the rule (rf_we_o = rf_we & ~illegal_reg_16); the CSR/LSU escape is an omission.

A minimal fix is a separate suppression block in the decoder that does not clear rf_we:

--- a/rtl/ibex_decoder.sv
+++ b/rtl/ibex_decoder.sv
@@ -855,6 +855,19 @@
       branch_in_dec_o = 1'b0;
       csr_access_o    = 1'b0;
     end
+
+    // RV32E/CHERIoT reserved register numbers must not have any side effect.
+    // rf_we is deliberately not cleared here: rf_we_o already masks it, and
+    // illegal_reg_16 is derived from rf_we (via rf_we_or_load), so clearing it
+    // in this block would create a combinational loop for CSR instructions.
+    if (illegal_reg_16) begin
+      data_req_o      = 1'b0;
+      data_we_o       = 1'b0;
+      jump_in_dec_o   = 1'b0;
+      jump_set_o      = 1'b0;
+      branch_in_dec_o = 1'b0;
+      csr_access_o    = 1'b0;
+    end
   end
 
   /////////////////////////////

rf_we must not be cleared in this block: illegal_reg_16 is derived from rf_we through rf_we_or_load (rtl/ibex_decoder.sv:195-205), so clearing it inside the same condition creates a combinational loop for CSR instructions. The naive if (illegal_insn || illegal_reg_16) form including rf_we = 1'b0 makes Verilator abort with DIDNOTCONVERGE (reproduced during this investigation). An alternative fix is to add ~instr_kill to the WB=0 instr_executing assignment, matching the WB=1 path; the decoder change is narrower because it preserves the "illegal instruction has no side effect" intent even if another controller path asserts instr_executing.

With the patch above applied to a896ef2 (no other changes), this harness reports at WB=0:

+PROG Patched WB=0
0 SIG[0]=0x12345678 (write suppressed)
1 SIG[1]=0x00002101, mcause=2, mepc=0x20a0 (handler reached)
2 SIG[0]=0x12345678
3 SIG[0]=0x12345678

The patch is a suggestion, not applied to the tree used for this report.

Steps to reproduce the issue

Everything needed is in this report: one SystemVerilog file, one Verilator command per configuration, and the run commands.

1. Requirements
  • Verilator 5.x (this report used 5.051 devel).

  • A clean Ibex checkout at a896ef2; the normal upstream tree already contains vendor/lowrisc_ip:

    git clone https://github.com/lowRISC/ibex
    git -C ibex checkout a896ef22777a40ad3f5a2a3b97fecd5116480c4c
    
  • No RTL changes; the bug reproduces on the unmodified tree.

2. Save the testbench as tb.sv
tb.sv (self-contained: the program is poked into memory, no hex file)
// Minimal, self-contained repro for:
//   RV32E + WritebackStage=0: an illegal instruction using x16-x31 in rd/rs1
//   still writes the CSR.
//
// ibex_core + external RF + 64 KiB memory + 1-cycle OBI slaves.  No external
// program file: the test program is poked into memory below.  No RTL changes.
//
//   -GWB=0|1      WritebackStage (default 0 = bug, 1 = control)
//   +PROG=0..4    test program (default 0)
//                 0: csrrw x16, mscratch, t2   (t2=0x55AA55AA)
//                 1: csrrw x16, mtvec,    t2   (t2=0x2000)
//                 2: csrrw x16, mscratch, x0
//                 3: csrrw x0,  mscratch, x16
//                 4: csrrw x6,  mscratch, t2   (legal control)
//
// The program preloads mscratch=0x12345678, mtvec=0x2100.  Illegal variants
// trap to 0x2100, which records mscratch/mtvec/mcause/mepc/mtval at 0x3000;
// the legal variant records mscratch on the normal path.  A store of 0xD00D
// to 0x3100 ends the run; the TB prints the signature.
module tb #(
  parameter bit WB = 1'b0
);
  import ibex_pkg::*;

  localparam [31:0] SIG_BASE  = 32'h0000_3000;
  localparam [31:0] DONE_ADDR = 32'h0000_3100;
  localparam int    SIG_WORDS = 8;

  logic clk = 0;
  logic rst_n = 0;
  always #5 clk = ~clk;

  // OBI instruction
  logic        instr_req, instr_gnt, instr_rvalid, instr_err;
  logic [31:0] instr_addr, instr_rdata;

  // OBI data
  logic        data_req, data_gnt, data_rvalid, data_we, data_tag, data_err;
  logic [3:0]  data_be;
  logic [31:0] data_addr, data_wdata, data_rdata;

  // Register file
  logic [31:0] rf [0:31];
  logic [4:0]  rf_raddr_a, rf_raddr_b, rf_waddr;
  logic        rf_we;
  logic [31:0] rf_wdata;

  // 64 KiB memory
  logic [31:0] mem [0:16383];
  logic [IC_TAG_SIZE-1:0]  ic_tag_rdata [IC_NUM_WAYS];
  logic [IC_LINE_SIZE-1:0] ic_data_rdata [IC_NUM_WAYS];

  int unsigned cycle = 0;
  integer      prog  = 0;

  // Fill memory with NOPs, then build the test program at 0x2080.
  task automatic poke(input int unsigned a, input logic [31:0] d);
    mem[a[15:2]] = d;
  endtask

  initial begin
    if (!$value$plusargs("PROG=%d", prog)) prog = 0;
    $display("TB WB=%0d PROG=%0d", WB, prog);

    for (int i = 0; i < 16384; i++) mem[i] = 32'h0000_0000;  // zeros
    for (int i = 0; i < 32; i++) rf[i] = 32'b0;
    for (int i = 0; i < IC_NUM_WAYS; i++) begin
      ic_tag_rdata[i]  = '0;
      ic_data_rdata[i] = '0;
    end

    // ---- program at 0x2080 ------------------------------------------------
    poke(32'h2080, 32'h0000_22b7);  // lui  t0, 2
    poke(32'h2084, 32'h1002_8293);  // addi t0, t0, 0x100      -> t0 = 0x2100
    poke(32'h2088, 32'h3052_9073);  // csrw mtvec, t0
    poke(32'h208c, 32'h1234_52b7);  // lui  t0, 0x12345
    poke(32'h2090, 32'h6782_8293);  // addi t0, t0, 0x678      -> t0 = 0x12345678
    poke(32'h2094, 32'h3402_9073);  // csrw mscratch, t0

    case (prog)
      1: begin
        poke(32'h2098, 32'h0000_23b7);  // lui  t2, 2
        poke(32'h209c, 32'h0000_0013);  // nop                     -> t2 = 0x2000
        poke(32'h20a0, 32'h3053_9873);  // csrrw x16, mtvec, t2    (illegal rd)
      end
      2: begin
        poke(32'h2098, 32'h55aa_53b7);  // lui  t2, 0x55AA5
        poke(32'h209c, 32'h5aa3_8393);  // addi t2, t2, 0x5AA      -> t2 = 0x55AA55AA
        poke(32'h20a0, 32'h3400_1873);  // csrrw x16, mscratch, x0 (illegal rd)
      end
      3: begin
        poke(32'h2098, 32'h55aa_53b7);
        poke(32'h209c, 32'h5aa3_8393);
        poke(32'h20a0, 32'h3408_1073);  // csrrw x0, mscratch, x16 (illegal rs1)
      end
      4: begin
        poke(32'h2098, 32'h55aa_53b7);
        poke(32'h209c, 32'h5aa3_8393);
        poke(32'h20a0, 32'h3403_9373);  // csrrw x6, mscratch, t2  (legal)
      end
      default: begin
        poke(32'h2098, 32'h55aa_53b7);
        poke(32'h209c, 32'h5aa3_8393);
        poke(32'h20a0, 32'h3403_9873);  // csrrw x16, mscratch, t2 (illegal rd)
      end
    endcase

    // Normal path (executes only for the legal variant; illegal ones trap).
    poke(32'h20a4, 32'h0000_34b7);  // lui  s1, 3                 -> s1 = 0x3000
    poke(32'h20a8, 32'h3400_2373);  // csrr t1, mscratch
    poke(32'h20ac, 32'h0064_a023);  // sw   t1, 0(s1)
    poke(32'h20b0, 32'h0000_d337);  // lui  t1, 0xD
    poke(32'h20b4, 32'h00d3_0313);  // addi t1, t1, 0xD           -> t1 = 0xD00D
    poke(32'h20b8, 32'h0000_32b7);  // lui  t0, 3
    poke(32'h20bc, 32'h1002_8293);  // addi t0, t0, 0x100         -> t0 = 0x3100
    poke(32'h20c0, 32'h0062_a023);  // sw   t1, 0(t0)             -> done marker
    poke(32'h20c4, 32'h0000_006f);  // j    .

    // ---- trap handler at 0x2100 ------------------------------------------
    poke(32'h2100, 32'h0000_34b7);  // lui  s1, 3                 -> s1 = 0x3000
    poke(32'h2104, 32'h3400_2373);  // csrr t1, mscratch
    poke(32'h2108, 32'h0064_a023);  // sw   t1, 0(s1)             -> SIG[0]
    poke(32'h210c, 32'h3050_2373);  // csrr t1, mtvec
    poke(32'h2110, 32'h0064_a223);  // sw   t1, 4(s1)             -> SIG[1]
    poke(32'h2114, 32'h3420_2373);  // csrr t1, mcause
    poke(32'h2118, 32'h0064_a423);  // sw   t1, 8(s1)             -> SIG[2]
    poke(32'h211c, 32'h3410_2373);  // csrr t1, mepc
    poke(32'h2120, 32'h0064_a623);  // sw   t1, 12(s1)            -> SIG[3]
    poke(32'h2124, 32'h3430_2373);  // csrr t1, mtval
    poke(32'h2128, 32'h0064_a823);  // sw   t1, 16(s1)            -> SIG[4]
    poke(32'h212c, 32'h0000_d337);  // lui  t1, 0xD
    poke(32'h2130, 32'h00d3_0313);  // addi t1, t1, 0xD
    poke(32'h2134, 32'h0000_32b7);  // lui  t0, 3
    poke(32'h2138, 32'h1002_8293);  // addi t0, t0, 0x100
    poke(32'h213c, 32'h0062_a023);  // sw   t1, 0(t0)             -> done marker
    poke(32'h2140, 32'h0000_006f);  // j    .
  end

  ibex_core #(
    .BaseIsa          (BaseIsaRV32I),
    .PMPEnable        (1'b1),
    .PMPGranularity   (0),
    .PMPNumRegions    (16),
    .MHPMCounterNum   (0),
    .RV32E            (1'b1),
    .RV32M            (RV32MFast),
    .RV32B            (RV32BNone),
    .RV32ZC           (RV32ZcaZcbZcmp),
    .BranchTargetALU  (1'b0),
    .WritebackStage   (WB),
    .ICache           (1'b0),
    .ICacheECC        (1'b0),
    .BranchPredictor  (1'b0),
    .DbgTriggerEn     (1'b1),
    .DbgHwBreakNum    (2),
    .DmHaltAddr       (32'h0000_2200),
    .DmExceptionAddr  (32'h0000_2204),
    .SecureIbex       (1'b0),
    .DummyInstructions(1'b0),
    .RegFileECC       (1'b0),
    .RegFileDataWidth (32),
    .MemECC           (1'b0),
    .MemDataWidth     (32)
  ) dut (
    .clk_i                  (clk),
    .rst_ni                 (rst_n),
    .hart_id_i              (32'b0),
    .boot_addr_i            (32'h0000_2000),
    .cheriot_enable_i       (IbexMuBiOff),

    .instr_req_o            (instr_req),
    .instr_gnt_i            (instr_gnt),
    .instr_rvalid_i         (instr_rvalid),
    .instr_addr_o           (instr_addr),
    .instr_rdata_i          (instr_rdata),
    .instr_err_i            (instr_err),

    .data_req_o             (data_req),
    .data_gnt_i             (data_gnt),
    .data_rvalid_i          (data_rvalid),
    .data_we_o              (data_we),
    .data_be_o              (data_be),
    .data_addr_o            (data_addr),
    .data_wdata_o           (data_wdata),
    .data_tag_o             (data_tag),
    .data_rdata_i           (data_rdata),
    .data_tag_i             (1'b0),
    .data_err_i             (data_err),

    .dummy_instr_id_o       (),
    .dummy_instr_wb_o       (),
    .rf_raddr_a_o           (rf_raddr_a),
    .rf_raddr_b_o           (rf_raddr_b),
    .rf_waddr_wb_o          (rf_waddr),
    .rf_we_wb_o             (rf_we),
    .rf_wdata_wb_ecc_o      (rf_wdata),
    .rf_rdata_a_ecc_i       (rf[rf_raddr_a]),
    .rf_rdata_b_ecc_i       (rf[rf_raddr_b]),
    .rf_wcap_ecc_wb_o       (),
    .rf_rcap_a_ecc_i        ('0),
    .rf_rcap_b_ecc_i        ('0),

    .ic_tag_req_o           (),
    .ic_tag_write_o         (),
    .ic_tag_addr_o          (),
    .ic_tag_wdata_o         (),
    .ic_tag_rdata_i         (ic_tag_rdata),
    .ic_data_req_o          (),
    .ic_data_write_o        (),
    .ic_data_addr_o         (),
    .ic_data_wdata_o        (),
    .ic_data_rdata_i        (ic_data_rdata),
    .ic_scr_key_valid_i     (1'b0),
    .ic_scr_key_req_o       (),

    .irq_software_i         (1'b0),
    .irq_timer_i            (1'b0),
    .irq_external_i         (1'b0),
    .irq_fast_i             (15'b0),
    .irq_nm_i               (1'b0),
    .irq_pending_o          (),

    .debug_req_i            (1'b0),
    .crash_dump_o           (),
    .double_fault_seen_o    (),

    .fetch_enable_i         (IbexMuBiOn),
    .mcounteren_writable_i  (IbexMuBiOn),
    .alert_minor_o          (),
    .alert_major_internal_o (),
    .alert_major_bus_o      (),
    .core_busy_o            ()
  );

  // 1-cycle OBI slaves: gnt combinational, rvalid one cycle later.
  assign instr_gnt = instr_req & ~instr_rvalid;
  assign data_gnt  = data_req  & ~data_rvalid;
  assign instr_err = 1'b0;
  assign data_err  = 1'b0;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      instr_rvalid <= 1'b0;
      data_rvalid  <= 1'b0;
      instr_rdata  <= 32'b0;
      data_rdata   <= 32'b0;
    end else begin
      instr_rvalid <= instr_req & ~instr_rvalid;
      if (instr_req) instr_rdata <= mem[instr_addr[15:2]];

      data_rvalid  <= data_req & ~data_rvalid;
      if (data_req) data_rdata <= mem[data_addr[15:2]];

      if (data_req & data_gnt & data_we) begin
        if (data_be == 4'b1111) begin
          mem[data_addr[15:2]] <= data_wdata;
        end else begin
          for (int k = 0; k < 4; k++) begin
            if (data_be[k]) mem[data_addr[15:2]][8*k +: 8] <= data_wdata[8*k +: 8];
          end
        end
      end
    end
  end

  always_ff @(posedge clk) begin
    if (rf_we && (rf_waddr != 5'b0)) rf[rf_waddr] <= rf_wdata;
  end

  always @(posedge clk) begin
    cycle <= cycle + 1;
    if (data_req && data_gnt && data_we && (data_addr == DONE_ADDR) &&
        (data_wdata == 32'h0000_D00D)) begin
      $display("RESULT done cycles=%0d", cycle);
      for (int j = 0; j < SIG_WORDS; j++) begin
        $display("RESULT SIG[%0d]=0x%08x", j, mem[(SIG_BASE >> 2) + j]);
      end
      $display("RESULT mstatus=%h mepc=%h mcause=%h mtval=%h",
               dut.cs_registers_i.mstatus_q, dut.cs_registers_i.mepc_q,
               dut.cs_registers_i.mcause_q, dut.cs_registers_i.mtval_q);
      $finish;
    end
    if (cycle > 20000) begin
      $display("TIMEOUT cycles=%0d pc=%h mepc=%h mcause=%h",
               cycle, dut.pc_id, dut.cs_registers_i.mepc_q,
               dut.cs_registers_i.mcause_q);
      $finish;
    end
  end

  initial begin
    repeat (10) @(posedge clk);
    rst_n = 1;
  end
endmodule
3. Build both configurations
IBEX=/path/to/ibex

verilator --binary --timing -Wno-fatal -Wno-PINMISSING --top-module tb \
  -GWB=0 -j 4 \
  -I"$IBEX/vendor/lowrisc_ip/ip/prim/rtl" \
  -I"$IBEX/vendor/lowrisc_ip/ip/prim_generic/rtl" \
  -I"$IBEX/vendor/lowrisc_ip/dv/sv/dv_utils" \
  "$IBEX/rtl/ibex_pkg.sv" "$IBEX/rtl/ibex_cheriot_pkg.sv" \
  "$IBEX/vendor/lowrisc_ip/ip/prim/rtl/prim_cipher_pkg.sv" \
  "$IBEX/rtl/ibex_alu.sv" \
  "$IBEX/rtl/ibex_branch_predict.sv" \
  "$IBEX/rtl/ibex_cheriot_ex.sv" \
  "$IBEX/rtl/ibex_compressed_decoder.sv" \
  "$IBEX/rtl/ibex_controller.sv" \
  "$IBEX/rtl/ibex_core.sv" \
  "$IBEX/rtl/ibex_counter.sv" \
  "$IBEX/rtl/ibex_cs_registers.sv" \
  "$IBEX/rtl/ibex_csr.sv" \
  "$IBEX/rtl/ibex_decoder.sv" \
  "$IBEX/rtl/ibex_dummy_instr.sv" \
  "$IBEX/rtl/ibex_ex_block.sv" \
  "$IBEX/rtl/ibex_fetch_fifo.sv" \
  "$IBEX/rtl/ibex_icache.sv" \
  "$IBEX/rtl/ibex_id_stage.sv" \
  "$IBEX/rtl/ibex_if_stage.sv" \
  "$IBEX/rtl/ibex_load_store_unit.sv" \
  "$IBEX/rtl/ibex_wb_stage.sv" \
  "$IBEX/rtl/ibex_multdiv_fast.sv" \
  "$IBEX/rtl/ibex_multdiv_slow.sv" \
  "$IBEX/rtl/ibex_pmp.sv" \
  "$IBEX/rtl/ibex_prefetch_buffer.sv" \
  "$IBEX/rtl/ibex_register_file_ff.sv" \
  "$IBEX/vendor/lowrisc_ip/ip/prim_generic/rtl/prim_buf.sv" \
  tb.sv --Mdir obj_wb0

# same command with -GWB=1 and --Mdir obj_wb1

-GWB=0|1 only overrides the TB's WB parameter; tb.sv is identical for both builds.

4. Run and compare
for p in 0 1 2 3 4; do echo "== PROG=$p WB=0"; ./obj_wb0/Vtb +PROG=$p; done
for p in 0 1 2 3 4; do echo "== PROG=$p WB=1"; ./obj_wb1/Vtb +PROG=$p; done

The binaries have no working-directory or file dependency. The runs terminate on the 0xD00D store to 0x3100 or after 20000 cycles.

Expected (full signature in the Observed Behavior section):

PROG=0 WB=0: SIG[0]=0x55aa55aa, SIG[1]=0x00002101, SIG[2]=2, SIG[3]=0x20a0
PROG=0 WB=1: SIG[0]=0x12345678, SIG[1]=0x00002101, SIG[2]=2, SIG[3]=0x20a0
PROG=1 WB=0: TIMEOUT pc=0x2000 mcause=2
PROG=1 WB=1: SIG[1]=0x00002101, SIG[2]=2, SIG[3]=0x20a0
PROG=2 WB=0: SIG[0]=0x00000000        PROG=2 WB=1: SIG[0]=0x12345678
PROG=3 WB=0: SIG[0]=0x00000000        PROG=3 WB=1: SIG[0]=0x12345678
PROG=4 WB=0/1: SIG[0]=0x55aa55aa, mcause=0
Program layout

The TB pokes the same skeleton for every +PROG; only the instruction at 0x20A0 (and the t2 value) changes:

0x2080  li   t0, 0x2100
0x2088  csrw mtvec, t0
0x208c  li   t0, 0x12345678
0x2094  csrw mscratch, t0
0x2098  li   t2, <value>        # 0x55AA55AA, or 0x2000 for PROG=1
0x20a0  <the CSR instruction under test>
# normal path (taken only by the legal control)
0x20a4  li   s1, 0x3000
0x20a8  csrr t1, mscratch
0x20ac  sw   t1, 0(s1)
0x20b0  li   t1, 0xD00D
0x20b8  li   t0, 0x3100
0x20c0  sw   t1, 0(t0)
0x20c4  j    .
# trap handler
0x2100  li   s1, 0x3000
0x2104  csrr t1, mscratch ; 0x2108 sw t1,  0(s1)
0x210c  csrr t1, mtvec    ; 0x2110 sw t1,  4(s1)
0x2114  csrr t1, mcause   ; 0x2118 sw t1,  8(s1)
0x211c  csrr t1, mepc     ; 0x2120 sw t1, 12(s1)
0x2124  csrr t1, mtval    ; 0x2128 sw t1, 16(s1)
0x212c  li   t1, 0xD00D
0x2134  li   t0, 0x3100
0x213c  sw   t1, 0(t0)
0x2140  j    .

The source code of the TB above contains the exact instruction words and the address of every poke.

My Environment

EDA tool and version: Verilator 5.051 devel (rev vUNKNOWN-built20260806-e413e67), --binary --timing

Operating system: Ubuntu 24.04.5 LTS, x86_64 (WSL2, kernel 6.18.33.2-microsoft-standard-WSL2)

Version of the Ibex source code: a896ef22777a40ad3f5a2a3b97fecd5116480c4c (2026-10-23). No changes to the Ibex source tree (git status --porcelain empty); only the TB parameter WB differs between the two builds.

主要语言
SystemVerilog
星标
2.2k
派生
836
平均合并
5 天 8 小时
30 天内合并 PR
22

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

lowRISC/ibex 的其他 Issue

查看 lowRISC/ibex 的全部 Issue

相似的 Issue

更多 Operating Systems Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。