Skip to content

Fixed the RISC-V64 trap frame size mismatch in the regression test BSP - #708

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/riscv-shared-bsp-frame-contract
Sep 9, 2026
Merged

fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:fix/riscv-shared-bsp-frame-contract

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

The RISC-V64 port moved its interrupt frame to 528 bytes in #698 — 65 slots plus 8 bytes of padding, so sp stays 16-byte aligned at a call — and published the size as TX_RISCV_TRAP_FRAME_SIZE. The port sources were converted to consume it. The shared regression test BSP was not: trap_entry in test/tx/cmake/riscv/bsp/tx_initialize_low_level.S still allocates a hardcoded 65 * REGBYTES.

Disassembled from the current dev:

trap_entry (test BSP):          addi  sp,sp,-520
_tx_thread_context_restore:     addi  sp,sp,528     (x2)

Every interrupt unwinds 8 bytes more than it allocated.

The example BSP under the port directory was converted along with the port, which is why the QEMU functional target kept passing and this went unnoticed. The RISC-V regression jobs are commented out in regression_test.yml, so CI did not catch it either.

Measured effect

RISC-V64, default_build, 95 tests, QEMU. Each tree built with ninja -k 0 so that every linkable target is actually present — without that, missing binaries are counted as failures and the numbers are meaningless:

Tree Failures
Before #698 (515ab8ab) 2
Current dev 24
With this change 2

This restores the suite exactly to its pre-#698 state. The failure signature was an illegal instruction (mcause=0x2) once execution reached a corrupted frame.

The two residual failures predate all of this and are unrelated: newlib pulls _impure_ptr out of R_RISCV_HI20 range for time(), so threadx_thread_delayed_suspension_test and threadx_thread_wait_abort_and_isr_test do not link. #691 addresses that with a local time() implementation.

The change

The test BSP now derives both frame sizes from the port it is linked against, so the two cannot drift apart again. A port that publishes no contract keeps the historical layout, so RISC-V32 is byte-identical until its own port publishes one.

TX_RISCV_TRAP_CALL_FRAME_SIZE is restored to the RISC-V64 tx_port.h. It was removed as unused when the frame sizes were introduced, but it belongs to the same contract — the space a trap entry reserves around a call into C, which the psABI requires to be 16 bytes rather than one register slot.

Verification

  • RISC-V64 default_build: 24 → 2 failures, matching the pre-riscv64: spec compliance and regression test fix #698 baseline.
  • RISC-V32, all five configurations: 2 failures before and after, the same two tests each time. No change.
  • Emitted frame allocation confirmed by disassembly: RV64 trap_entry now addi sp,sp,-528; RV32 unchanged at -260.

Note on ordering

#691 was intended to land before #698. It is what teaches the shared BSP to consume the port contract, and it fixes the newlib time() link error. Merging #698 first left the shared BSP on the old hardcoded size against a port that had moved. This change supplies that missing groundwork for RISC-V64; #691 can then drop its now-redundant __riscv_xlen == 32 guard on the same include.

The RISC-V64 port moved its interrupt frame to 528 bytes (65 slots plus 8
bytes of padding, so sp stays 16-byte aligned at a call) and published the
size as TX_RISCV_TRAP_FRAME_SIZE. The port sources were converted to use
it, but the shared regression test BSP was not: its trap_entry still
allocated a hardcoded 65 * REGBYTES, or 520 bytes.

Every interrupt therefore unwound 8 bytes more than it allocated:

    trap_entry:                   addi  sp,sp,-520
    _tx_thread_context_restore:   addi  sp,sp,528

On the RISC-V64 regression suite that left 24 of 95 tests failing in the
default configuration, typically as an illegal instruction once execution
reached a corrupted frame. The example BSP under the port directory was
converted with the port and was unaffected, which is why the functional
QEMU test kept passing.

The test BSP now takes both frame sizes from the port it is linked
against, so the two cannot drift apart again. A port that publishes no
contract keeps the historical layout, so the RISC-V32 side is unchanged
until its own port publishes one.

TX_RISCV_TRAP_CALL_FRAME_SIZE is restored to the RISC-V64 tx_port.h. It
was removed as unused when the frame sizes were introduced, but it is
part of the same contract: it is the space a trap entry reserves around a
call into C, and the psABI requires 16 bytes there rather than one
register slot.

Verified on QEMU with every linkable test built, comparing against the
commit before the port change:

    before the port change   2 failures out of 95 (both unlinkable)
    current dev              24 failures out of 95
    with this change          2 failures out of 95 (both unlinkable)

The two remaining failures predate all of this: newlib pulls _impure_ptr
out of R_RISCV_HI20 range for time(), so those two binaries do not link.
RISC-V32 is unchanged at 2 failures across all five configurations.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 146d57b into eclipse-threadx:dev Sep 9, 2026
12 checks passed
@fdesbiens
fdesbiens deleted the fix/riscv-shared-bsp-frame-contract branch September 9, 2026 14:57
fdesbiens added a commit to akifejaz/threadx that referenced this pull request Sep 9, 2026
tx_port.h published TX_RISCV_TRAP_FRAME_SIZE for the GNU BSP assembly, but
nothing in the port consumed it. Six .S files each rebuilt the same numbers
from their own #if, so the interrupt frame size was written out in seven
places and the solicited frame size in three.

That is the shape that produced the RISC-V64 fault fixed in eclipse-threadx#708, where the
port moved to a padded frame and one copy of the constant did not. The
sources now include tx_port.h and take both sizes from it, and no literal
frame size remains in the port. TX_RISCV_SOL_FRAME_SIZE joins the contract,
since the solicited frame was never published at all.

The emitted code is unchanged: 400 and 176 bytes for ILP32D, 128 for
soft-float, confirmed by disassembly before and after.

Two further corrections:

_tx_initialize_low_level carried .global immediately followed by .weak, so
the symbol stayed weak and the .global did nothing. Weak is what the port
wants, because the example and regression BSPs both provide their own
definition, so the stray .global is removed rather than the .weak. Verified
with nm that the symbol is still W.

The QEMU runner seeded fpu_verified from skip_fpu, so a soft-float run
satisfied the FPU gate whether or not the script ever reported the skip. It
now starts false and is set only when the skip marker is present, so a run
that dies before reaching that point fails instead of passing.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
fdesbiens added a commit that referenced this pull request Sep 9, 2026
* riscv32: spec compliance and regression test fix

Signed-off-by: Akif Ejaz <akifejaz40@gmail.com>

* Derived the RISC-V32 frame sizes from the port contract in one place

tx_port.h published TX_RISCV_TRAP_FRAME_SIZE for the GNU BSP assembly, but
nothing in the port consumed it. Six .S files each rebuilt the same numbers
from their own #if, so the interrupt frame size was written out in seven
places and the solicited frame size in three.

That is the shape that produced the RISC-V64 fault fixed in #708, where the
port moved to a padded frame and one copy of the constant did not. The
sources now include tx_port.h and take both sizes from it, and no literal
frame size remains in the port. TX_RISCV_SOL_FRAME_SIZE joins the contract,
since the solicited frame was never published at all.

The emitted code is unchanged: 400 and 176 bytes for ILP32D, 128 for
soft-float, confirmed by disassembly before and after.

Two further corrections:

_tx_initialize_low_level carried .global immediately followed by .weak, so
the symbol stayed weak and the .global did nothing. Weak is what the port
wants, because the example and regression BSPs both provide their own
definition, so the stray .global is removed rather than the .weak. Verified
with nm that the symbol is still W.

The QEMU runner seeded fpu_verified from skip_fpu, so a soft-float run
satisfied the FPU gate whether or not the script ever reported the skip. It
now starts false and is set only when the skip marker is present, so a run
that dies before reaching that point fails instead of passing.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Signed-off-by: Akif Ejaz <akifejaz40@gmail.com>
Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant