Skip to content

Phase 4: NuttX 12 stack bounds and Thumb-2 JIT code alignment - #4

Merged
NevynUK merged 2 commits into
MergeManagedCodefrom
Phase4
Oct 14, 2026
Merged

NevynUK merged 2 commits into
MergeManagedCodefrom
Phase4

Conversation

@NevynUK

@NevynUK NevynUK commented Oct 14, 2026

Copy link
Copy Markdown

These two runtime fixes let Mono run a managed Hello World on a Meadow F7 (CCM) under Meadow.OS (NuttX 12), with both the JIT and the interpreter.

Stack bounds (mono-threads-nuttx.c)

Since NuttX 10.2.0 (upstream commit 2335b69120a), pthread_get_stackaddr_np returns stack_base_ptr, the lowest usable address, and pthread_get_stacksize_np returns the usable size. The NuttX port still assumed the NuttX 7 behaviour, where the address is the initial stack pointer at the top of the stack, so it subtracted the size. The resulting bounds lay below the real stack, and mono_thread_info_get_stack_bounds asserted in sgen_client_init:
(current > *staddr) && (current < *staddr + *stsize).

Thumb-2 code buffer alignment (thumb-codegen.h, mini-arm.c, tramp-arm.c)

Thumb-2 code is only halfword aligned, but the JIT wrote instruction and literal words through 4-byte-aligned types. GCC 15 merged two of these stores in get_throw_trampoline into an strd r1, r2, [r0, #4] with r0=0xc0339bb2. This raised a UsageFault with CFSR=0x01000000 (UNALIGNED). STRD, LDRD, STM and LDM always need word alignment, even where a single LDR or STR does not.

The fix:

  • arminstr_t and armword_t are now aligned(2).
  • The 36 32-bit instruction structs are now packed, aligned(2). I checked all 69 type sizes before and after with the cross-compiler, and none changed.
  • The code-buffer accesses in mini-arm.c and tramp-arm.c now use arminstr_t.
  • The A32 arm-codegen.h is unchanged.

get_throw_trampoline now uses only SP-relative paired accesses.

Testing

On a CCM with Meadow.OS Phase4 (WildernessLabs/Meadow.OS#697):

  • A net11.0 console app runs to exit_code=42 under the JIT, the JIT with --debug, and --interp.
  • meadow listen shows its stdout and stderr lines.

🤖 Generated with Claude Code

NevynUK and others added 2 commits October 14, 2026 20:23
Since NuttX 10.2.0 (upstream commit 2335b69120a) pthread_get_stackaddr_np
returns stack_base_ptr, the lowest usable stack address, and
pthread_get_stacksize_np the usable size.  The NuttX port still assumed the
NuttX 7 behaviour (the initial stack pointer at the top) and subtracted the
size, so the bounds lay below the real stack and the sanity check in
mono_thread_info_get_stack_bounds asserted during GC initialisation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thumb-2 code is only halfword aligned, but the JIT wrote 32-bit
instructions and literal words through uint32_t/guint32 pointers and 32-bit
bit-field structures, which tell the compiler the address is word aligned.
GCC 15 combined two such stores in get_throw_trampoline into an STRD, which
takes a UsageFault (UNALIGNED) on a halfword aligned address.

In thumb-codegen.h arminstr_t and armword_t are now 2-byte aligned, the 36
32-bit instruction structures are packed with 2-byte alignment (sizes are
unchanged), and the code-buffer accesses in thumb-codegen.h, mini-arm.c and
tramp-arm.c use arminstr_t.  arm-codegen.h (A32, always word aligned) is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NevynUK
NevynUK merged commit ff60710 into MergeManagedCode Oct 14, 2026
NevynUK added a commit to WildernessLabs/Meadow.OS that referenced this pull request Oct 14, 2026
WildernessLabs/runtime#4 is merged; the tree is the same as the Phase4
commit pinned before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NevynUK added a commit that referenced this pull request Oct 22, 2026
arm_branch_long() added 2 to the offset when the B.W was at an address
that is 2 mod 4.  Every caller emits LDR.W rd, [pc, #4]; B.W 8; .word
with the literal unpadded straight after the branch, and a branch is
relative to the unaligned PC, so the extra 2 landed in the second
halfword of the following instruction.  (arm_ldr_lit does need the
adjustment, as its PC is Align(PC, 4).)

For a call from a dynamic method (emit_call_seq) the following
instruction is ORR.W ip, ip, #1, whose second halfword 0x0C01 runs as
LSRS r1, r0, dotnet#16, so about half of those call sites passed r0 >> 16 as
the second argument register.  A compiled Regex calls its generated
methods with (RegexRunner, ReadOnlySpan<char>), so the span's reference
became the top half of the runner's address, and the first match took a
MemManage fault (MMFAR 0xc03a) and reset the board.  The other callers
(GOT and AOT constant loads, sequence point and trigger page loads) had
the same exposure.

Measured on a Meadow F7: a dynamic method forwarding (object,
ReadOnlySpan<char>) to an ordinary method used to deliver the object's
address >> 16 as the span reference, and now delivers the right one.
Compiled regexes match, YamlDotNet parses, and the
ProjLabV3SoakTestRunner app completes a 1000 cycle socket soak test
under the JIT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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