Skip to content

Conversation

@erhankur
Copy link
Owner

@erhankur erhankur commented Nov 14, 2025

Summary

This PR fixes thread awareness support for Zephyr RTOS on RISC-V and Xtensa targets.

Xtensa Summary

The implementation reads thread register state from Zephyr's Base Save Area (BSA) structure using dynamic symbol-based offset resolution. The BSA size and register offsets vary based on Zephyr kernel configuration (FPU, loops, threadptr, etc.), so the implementation uses symbol addresses provided by GDB rather than hardcoded offsets.

Scope: This implementation only supports threads suspended via voluntary context switches (arch_switch). Exception frames (involuntary context switches from interrupts/exceptions) are not supported.

Accessing BSA Fields

Zephyr uses double indirection to locate the BSA on the thread's stack:

thread->switch_handle → ptr_to_bsa → BSA structure

How OpenOCD reads registers:

  1. Read thread->switch_handle (points to stack location)
  2. Read *switch_handle to get ptr_to_bsa (points to BSA)
  3. Read BSA structure from ptr_to_bsa address
  4. Extract PC, PS, A0-A3 from BSA using dynamic offsets
  5. Detect and read high registers (A4-A15) from above BSA if spilled

Zephyr Configuration Requirement

To enable thread awareness, Zephyr must expose the switch_handle offset in its thread info structure. Add the following to zephyr/kernel/thread_info.c:

#elif defined(CONFIG_XTENSA)
    #ifdef CONFIG_USE_SWITCH
        [THREAD_INFO_OFFSET_T_STACK_PTR] = offsetof(struct k_thread,
                                                switch_handle),
    #else
        [THREAD_INFO_OFFSET_T_STACK_PTR] = THREAD_INFO_UNIMPLEMENTED,
    #endif

Note: CONFIG_USE_SWITCH is enabled by default for Xtensa targets.
Note: CONFIG_DEBUG_THREAD_INFO must be enabled in prj.conf

Memory Layout

Stack Memory (growing downward):
┌─────────────────────────┐
│ switch_handle           │ ← thread->switch_handle (step 1)
│ (contains ptr_to_bsa)   │
├─────────────────────────┤
│ ptr_to_bsa              │ ← *switch_handle (step 2)
│ (points to BSA)         │
├─────────────────────────┤
│ High registers (A4-A15) │ ← Saved above BSA (if spilled)
│ (A4-A7, A8-A11, A12-A15)│   Read via marker detection
├─────────────────────────┤
│ BSA structure           │ ← Read from ptr_to_bsa (step 3)
│   - PC, PS, A0-A3, etc  │   Offsets from ABS symbols
│   - caller_a0-a3        │ ← At end (BSA_SIZE - 16, -12, -8, -4)
└─────────────────────────┘

Caller Spill Slots:

  • Located at the end of BSA (always BSA_SIZE - 16, -12, -8, -4)
  • caller_a0 (BSA end - 16): Return address of calling function
  • caller_a1 (BSA end - 12): Stack pointer of calling function
  • Created automatically by Xtensa's ENTRY instruction
  • Enable backtrace construction in GDB

Dynamic Symbol Resolution:
The implementation uses symbol addresses provided by GDB to read BSA size and register offsets:

  • ___xtensa_irq_bsa_t_SIZEOF - Total BSA size in bytes (configuration dependent)
  • ___xtensa_irq_bsa_t_ps_OFFSET - PS register offset within BSA

GDB resolves these symbols from the ELF file and provides their addresses to OpenOCD, avoiding hardcoded offsets to maintain compatibility across different Zephyr kernel configurations (with/without FPU, loops, threadptr, etc.).

High Registers (A4-A15):
High registers are saved above the BSA on the stack when spilled during context switches. OpenOCD detects which register quads (A4-A7, A8-A11, A12-A15) were saved by checking marker values at fixed offsets relative to the BSA address. The registers are then read from their known locations and made available to GDB for debugging suspended threads.

RISC-V Summary

The implementation reads thread register state from Zephyr's callee_saved structure, which contains the callee-saved registers (SP, RA, S0-S11) that are preserved across context switches.

Zephyr stores thread context directly in the thread structure:

thread->callee_saved → callee_saved structure (SP, RA, S0-S11)

To read callee_saved from OpenOCD, Zephyr must expose the callee_saved.sp offset in its thread info structure. This is typically provided by Zephyr's thread_info.c:

[THREAD_INFO_OFFSET_T_STACK_PTR] = offsetof(struct k_thread, callee_saved.sp),

Note: CONFIG_DEBUG_THREAD_INFO must be enabled in prj.conf

Memory Layout:

Thread Structure (struct k_thread):
┌─────────────────────────┐
│ base (thread metadata)  │
├─────────────────────────┤
│ callee_saved structure  │ ← thread->callee_saved
│   - SP (offset 0)       │ ← Stack pointer
│   - RA (offset 4)       │ ← Return address (becomes PC)
│   - S0 (offset 8)       │ ← Frame pointer
│   - S1-S11 (offsets 12-52)│ ← Saved registers
└─────────────────────────┘

For a suspended thread, the PC should point to where execution will resume when the thread is switched back in. In RISC-V, the Return Address (RA) register contains exactly this value. Setting PC = RA enables proper backtrace construction in GDB.

@erhankur erhankur force-pushed the fix/zephyr_thread_awareness branch 2 times, most recently from 970d47a to 0ac31f4 Compare November 14, 2025 20:39
@erhankur erhankur force-pushed the fix/zephyr_thread_awareness branch from 0ac31f4 to 0446881 Compare November 16, 2025 23:50
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.

2 participants