hitHuang opened a new pull request, #19439:
URL: https://github.com/apache/nuttx/pull/19439

   
   ## Summary
   
   The RISC-V implementation of `up_backtrace()` 
(`arch/risc-v/src/common/riscv_backtrace.c`) has several correctness issues 
under `CONFIG_BUILD_KERNEL`. This PR fixes three independent, compounding 
problems found while debugging `dumpstack` on `rv-virt:knsh_romfs`.
   
   ### Problem 1: `xcp.ustkptr` is stale and no longer maintained
   
   `ustkptr` was introduced in `77e90d9c875` ("RISC-V: Include support for 
kernel stack"). The intent was: on syscall entry, update `ustkptr` to the 
user-mode SP; on syscall exit, reset it back to `NULL`.
   
   `76e5204a806` ("risc-v/backtrace: correct stack pointer if enable 
ARCH_KERNEL_STACK backtrace") then started consuming `ustkptr` in 
`up_backtrace()`, using `*(ustkptr + 1)` as the starting frame pointer for a 
task that is currently inside a syscall (running on the kernel stack instead of 
the user stack).
   
   However, `e6973c764cd` ("riscv/syscall: Optimize user service call 
performance") appears to have dropped the maintenance of `ustkptr` on the 
syscall entry/exit path. As a result, `ustkptr` is only ever set once, at task 
creation time, and never updated afterwards — the `!= NULL` check in 
`up_backtrace()` is effectively dead, and the value it reads is always the 
stale "initial" one.
   
   This alone causes `dumpstack` to return zero frames for any user-mode task, 
because `ustkptr + 1` no longer points anywhere near a valid frame — it's out 
of range of the user stack entirely.
   
   Separately, I have to admit I don't fully understand what `*(ustkptr + 1)` 
was supposed to compute in the first place — it doesn't correspond to any 
ABI-defined frame pointer slot I can find. If I'm missing something about how 
`76e5204a806` was verified at the time, I'd appreciate clarification; otherwise 
this looks like it was never exercised against a real out-of-range case.
   
   **Fix**: instead of relying on `ustkptr`, use the existing 
`TCB_FLAG_SYSCALL` flag together with `xcp.sregs[REG_FP]` / 
`xcp.sregs[REG_EPC]` (the syscall entry's saved register context, already 
maintained elsewhere) to detect "this task is currently inside a syscall" and 
get its correct frame pointer/PC.
   
   ### Problem 2: cross-tcb backtrace reads across the wrong address environment
   
   Independently of problem 1, when `dumpstack` targets a different task's TCB 
(`CONFIG_ARCH_ADDRENV`, e.g. `knsh_romfs`), `backtrace()`'s `fp`/`ra` chain 
lives in the *target* task's address space, while `buffer` (where results are 
written) belongs to the *caller*. The RISC-V `backtrace()` never switched 
address environments at all, so the fp-chain walk dereferenced the target 
task's stack addresses using the *caller's* page tables — reading whatever 
physical page happens to be mapped at that virtual address in the caller's own 
environment, not the target's actual stack contents.
   
   **Fix**: `backtrace()` now takes an optional `addrenv` parameter and wraps 
only the two frame-chain reads (`ra = *(fp - 1)`, `next_fp = *(fp - 2)`) in 
`addrenv_select()`/`addrenv_restore()`. The write into `buffer[i++] = ra` stays 
outside that window, since `buffer` is the caller's own memory and must be 
written using the caller's own address environment — switching addrenv around 
that write as well would corrupt/lose the result (confirmed by hardware 
testing; an earlier, wider-scoped attempt that included the buffer write inside 
the switch produced all-zero backtraces).
   
   ### Problem 3: leaf syscall wrapper functions don't save `ra` on the stack
   
   Even with problems 1 and 2 fixed, backtraces crossing a syscall boundary can 
still be broken. `sys_call0()`..`sys_call6()` in 
`arch/risc-v/include/syscall.h` are leaf functions as far as the compiler can 
tell (they never call another C function themselves), so even with 
`CONFIG_FRAME_POINTER` (`-fno-omit-frame-pointer`), the compiler only bothers 
to save `s0` (the frame pointer) around the `ecall` — it never allocates or 
writes a stack slot for `ra`, because as far as the compiler's leaf-function 
analysis is concerned, `ra` is never clobbered inside the function body and 
`ret` (which is `jalr x0, 0(x1)`) works correctly straight out of the register.
   
   The problem is that `backtrace()`'s fp-chain walk assumes the standard 
prologue layout (`ra` at `fp - 1`, saved `s0` at `fp - 2`). In these leaf 
wrappers, `s0` ends up at `fp - 1` instead (since there's no `ra` slot to begin 
with), and `fp - 2` is simply never written — uninitialized stack garbage. 
Disassembly of `waitpid()` before the fix:
   
   ```
   c000ba08 <waitpid>:
   c000ba08:       1141                    addi    sp,sp,-16
   c000ba0a:       c622                    sw      s0,12(sp)
   c000ba0c:       872a                    mv      a4,a0
   c000ba0e:       0800                    addi    s0,sp,16
   c000ba10:       87ae                    mv      a5,a1
   c000ba12:       86b2                    mv      a3,a2
   c000ba14:       02300513                li      a0,35
   c000ba18:       85ba                    mv      a1,a4
   c000ba1a:       863e                    mv      a2,a5
   c000ba1c:       00000073                ecall
   c000ba20:       0001                    nop
   c000ba22:       4432                    lw      s0,12(sp)
   c000ba24:       0141                    addi    sp,sp,16
   c000ba26:       8082                    ret
   ```
   
   Note `s0` is saved at `sp+12` (i.e. `fp - 1`), and `ra` is never spilled at 
all. `backtrace()` reading `*(fp - 1)` here gets the caller's old `s0`, not a 
return address; `*(fp - 2)` gets whatever garbage was left on the stack from a 
previous call.
   
   **Fix**: force `ra` to be spilled/reloaded around the `ecall` by adding it 
to the clobber list, gated behind a new `RISCV_ECALL_CLOBBERS` macro:
   
   ```c
   #if defined(CONFIG_FRAME_POINTER) && defined(CONFIG_SCHED_BACKTRACE)
   #  define RISCV_ECALL_CLOBBERS "memory", "ra"
   #else
   #  define RISCV_ECALL_CLOBBERS "memory"
   #endif
   ```
   
   This only takes effect when both `CONFIG_FRAME_POINTER` (the fp-chain is 
structurally meaningful at all) and `CONFIG_SCHED_BACKTRACE` (the only consumer 
of `up_backtrace()`'s fp-chain walk) are enabled — otherwise there is no 
fp-chain consumer to fix up for, and the extra spill/reload would be pure 
overhead. Disassembly of `waitpid()` after the fix:
   
   ```
   c000ba9c <waitpid>:
   c000ba9c:       1141                    addi    sp,sp,-16
   c000ba9e:       c422                    sw      s0,8(sp)
   c000baa0:       c606                    sw      ra,12(sp)
   c000baa2:       0800                    addi    s0,sp,16
   c000baa4:       872a                    mv      a4,a0
   c000baa6:       87ae                    mv      a5,a1
   c000baa8:       86b2                    mv      a3,a2
   c000baaa:       02300513                li      a0,35
   c000baae:       85ba                    mv      a1,a4
   c000bab0:       863e                    mv      a2,a5
   c000bab2:       00000073                ecall
   c000bab6:       0001                    nop
   c000bab8:       40b2                    lw      ra,12(sp)
   c000baba:       4422                    lw      s0,8(sp)
   c000babc:       0141                    addi    sp,sp,16
   c000babe:       8082                    ret
   ```
   
   `ra` and `s0` are now both correctly saved at `fp - 1`/`fp - 2`, matching 
what `backtrace()` expects. This adds two instructions and one stack word of 
overhead to each `sys_callN()` wrapper, but only in configurations where both 
gating options above are enabled.
   
   ### Known follow-up (not part of this PR)
   
   While debugging this, I found that AArch64's `up_backtrace()` under 
`CONFIG_BUILD_KERNEL` appears to have a similar class of problem. I'd like to 
hold off on that until this PR is reviewed/confirmed, and will submit it as a 
separate follow-up PR.
   
   ## Impact
   
   - Affects RISC-V only, and only when `CONFIG_ARCH_KERNEL_STACK` and/or 
`CONFIG_ARCH_ADDRENV` are enabled (i.e. `CONFIG_BUILD_KERNEL`-style 
configurations such as `knsh_romfs`/`knsh64_romfs`). No change in behavior for 
FLAT/PROTECTED builds that don't use these options.
   - The `syscall.h` clobber change only adds instructions when both 
`CONFIG_FRAME_POINTER` and `CONFIG_SCHED_BACKTRACE` are enabled; otherwise it's 
a no-op (`RISCV_ECALL_CLOBBERS` expands to the original `"memory"`).
   - No public API/ABI changes. `backtrace()` (static, file-local) gained an 
extra parameter, but `up_backtrace()`'s external signature is unchanged.
   - Purely a correctness fix for `dumpstack`/`sched_backtrace()` output; does 
not change scheduling, syscall semantics, or any other runtime behavior.
   
   ## Testing
   
   Tested on `rv-virt:knsh_romfs` (QEMU), with `CONFIG_SCHED_BACKTRACE`, 
`CONFIG_FRAME_POINTER` and `CONFIG_SYSTEM_DUMPSTACK` manually enabled. Also 
verified okay on `nsh` and `knsh64_romfs`.
   
   (Not focusing here on why the printed timestamp is an obviously bogus value 
— that looks like a separate, pre-existing issue unrelated to this fix.)
   
   ### Before fix 1
   
   For kernel threads, `dumpstack` works fine. For a user-mode application, 
`dumpstack` produces no output at all:
   
   ```
   NuttShell (NSH) NuttX-13.0.0
   nsh> uname -a
   NuttX 13.0.0 20579ab531-dirty Jul 15 2026 11:05:29 risc-v rv-virt
   nsh> ps
     TID   PID  PPID PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK        
    STACK COMMAND
       0     0     0   0 FIFO     Kthread   - Ready              
0000000000000000 0003040 Idle_Task
       1     0     0 100 RR       Kthread   - Waiting  Semaphore 
0000000000000000 0001968 lpwork 0x80600010 0x80600060
       3     3     0 100 RR       Task      - Running            
0000000000000000 0003008 /system/bin/init
   nsh> dumpstack 0
   [17179869184000.001000] sched_dumpstack: backtrace| 0: 0x8020bd02 0x806086f0 
0x8020119a 0x8020004a
   nsh> dumpstack 1
   [2357937045504000.001000] sched_dumpstack: backtrace| 1: 0x8020b8c4 
0x802170c6 0x8020844e 0x80208472 0x80202e16 0x80202798
   nsh> dumpstack 3
   nsh> dumpstack 3
   ```
   
   `dumpstack`ing itself also produces no output:
   
   ```
   NuttShell (NSH) NuttX-13.0.0
   nsh> ps
     TID   PID  PPID PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK        
    STACK COMMAND
       0     0     0   0 FIFO     Kthread   - Ready              
0000000000000000 0003040 Idle_Task
       1     0     0 100 RR       Kthread   - Waiting  Semaphore 
0000000000000000 0001968 lpwork 0x80600010 0x80600060
       3     3     0 100 RR       Task      - Running            
0000000000000000 0003008 /system/bin/init
   nsh> dumpstack 0 10
   [1473173782528000.001000] sched_dumpstack: backtrace| 0: 0x8020bd02 
0x806086f0 0x8020119a 0x8020004a
   [1511828488192000.001000] sched_dumpstack: backtrace| 1: 0x8020b8c4 
0x802170c6 0x8020844e 0x80208472 0x80202e16 0x80202798
   ```
   
   ### After fix 1
   
   User-mode `init` (TID 3) now produces *some* output, but it's still 
incomplete — this is expected, since problem 2 (addrenv) hasn't been fixed yet:
   
   ```
   NuttShell (NSH) NuttX-13.0.0
   nsh> uname -a
   NuttX 13.0.0 7c92d4fb7b-dirty Jul 15 2026 11:22:33 risc-v rv-virt
   nsh> ps
     TID   PID  PPID PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK        
    STACK COMMAND
       0     0     0   0 FIFO     Kthread   - Ready              
0000000000000000 0003040 Idle_Task
       1     0     0 100 RR       Kthread   - Waiting  Semaphore 
0000000000000000 0001968 lpwork 0x80600010 0x80600060
       3     3     0 100 RR       Task      - Running            
0000000000000000 0003008 /system/bin/init
   nsh> dumpstack 0 10
   [3143916060672000.001000] sched_dumpstack: backtrace| 0: 0x8020bd02 
0x806086f0 0x8020119a 0x8020004a
   [3156800962560000.001000] sched_dumpstack: backtrace| 1: 0x8020b8c4 
0x802170c6 0x8020844e 0x80208472 0x80202e16 0x80202798
   [3156800962560000.001000] sched_dumpstack: backtrace| 3: 0xc000ba20
   [3173980831744000.001000] sched_dumpstack: backtrace| 4: 0xc000271a 
0xc0803fd0
   ```
   
   ### After fix 2
   
   ```
   NuttShell (NSH) NuttX-13.0.0
   nsh> uname -a
   NuttX 13.0.0 ac32077475-dirty Jul 15 2026 11:26:42 risc-v rv-virt
   nsh> ps
     TID   PID  PPID PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK        
    STACK COMMAND
       0     0     0   0 FIFO     Kthread   - Ready              
0000000000000000 0003040 Idle_Task
       1     0     0 100 RR       Kthread   - Waiting  Semaphore 
0000000000000000 0001968 lpwork 0x80600010 0x80600060
       3     3     0 100 RR       Task      - Running            
0000000000000000 0003008 /system/bin/init
   nsh> dumpstack 0 10
   [1387274436608000.001000] sched_dumpstack: backtrace| 0: 0x8020bd02 
0x806086f0 0x8020119a 0x8020004a
   [1511828488192000.001000] sched_dumpstack: backtrace| 1: 0x8020b8c4 
0x802170c6 0x8020844e 0x80208472 0x80202e16 0x80202798
   [1550483193856000.001000] sched_dumpstack: backtrace| 3: 0xc000ba20 
0xc08029f0 0xc0002b52 0xc0003aa0 0xc0003b40 0xc00025a2 0xc00023ca 0xc0000b4a
   [1580547964928000.001000] sched_dumpstack: backtrace| 3: 0xc0000b08
   [1597727834112000.001000] sched_dumpstack: backtrace| 4: 0xc000271a 
0xc0803fd0
   ```
   
   `addr2line` on the TID 3 (`init`) addresses — noticeably better than before, 
but still broken partway through because of problem 3:
   
   ```
   $ riscv64-unknown-elf-addr2line -f -e init 0xc000ba20 0xc08029f0 0xc0002b52 
0xc0003aa0 0xc0003b40 0xc00025a2 0xc00023ca 0xc0000b4a 0xc0000b08
   sys_call3
   /home/huang/workspace/nuttx-fork/nuttx/include/arch/syscall.h:241
   ??
   ??:0
   nsh_execute
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_parse.c:599
   nsh_parse_command
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_parse.c:2911
   nsh_parse
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_parse.c:3116
   nsh_session
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_session.c:249
   nsh_consolemain
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_consolemain.c:81
   main
   /home/huang/workspace/nuttx-fork/apps/system/nsh/nsh_main.c:82
   _start
   /home/huang/workspace/nuttx-fork/nuttx/arch/risc-v/src/common/crt0.c:201
   ```
   
   And on TID 4 (`dumpstack` itself):
   
   ```
   $ riscv64-unknown-elf-addr2line -f -e dumpstack 0xc000271a 0xc0803fd0
   sys_call4
   /home/huang/workspace/nuttx-fork/nuttx/include/arch/syscall.h:271
   ??
   ??:0
   ```
   
   ### After fix 3 (all three fixes applied)
   
   ```
   NuttShell (NSH) NuttX-13.0.0
   nsh> uname -a
   NuttX 13.0.0 358a66b7c1-dirty Jul 15 2026 11:49:15 risc-v rv-virt
   nsh> ps
     TID   PID  PPID PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK        
    STACK COMMAND
       0     0     0   0 FIFO     Kthread   - Ready              
0000000000000000 0003040 Idle_Task
       1     0     0 100 RR       Kthread   - Waiting  Semaphore 
0000000000000000 0001968 lpwork 0x80600010 0x80600060
       3     3     0 100 RR       Task      - Running            
0000000000000000 0003008 /system/bin/init
   nsh> dumpstack 0 10
   [4247722655744000.001000] sched_dumpstack: backtrace| 0: 0x8020bd02 
0x806086f0 0x8020119a 0x8020004a
   [4290672328704000.001000] sched_dumpstack: backtrace| 1: 0x8020b8c4 
0x802170c6 0x8020844e 0x80208472 0x80202e16 0x80202798
   [25769803776000.001000] sched_dumpstack: backtrace| 3: 0xc000bab6 0xc0005312 
0xc0002b76 0xc0003ac4 0xc0003b64 0xc00025c6 0xc00023ee 0xc0000b4a
   [60129542144000.001000] sched_dumpstack: backtrace| 3: 0xc0000b08
   [73014444032000.001000] sched_dumpstack: backtrace| 4: 0xc0002738 0xc0000b44 
0xc0000aca 0xc0000a8c
   ```
   
   This time we finally get a full, correct backtrace. `addr2line` on TID 3 
(`init`):
   
   ```
   $ riscv64-unknown-elf-addr2line -f -e init 0xc000bab6 0xc0005312 0xc0002b76 
0xc0003ac4 0xc0003b64 0xc00025c6 0xc00023ee 0xc0000b4a 0xc0000b08
   sys_call3
   /home/huang/workspace/nuttx-fork/nuttx/include/arch/syscall.h:258
   nsh_fileapp
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_fileapps.c:307
   nsh_execute
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_parse.c:599
   nsh_parse_command
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_parse.c:2911
   nsh_parse
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_parse.c:3116
   nsh_session
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_session.c:249
   nsh_consolemain
   /home/huang/workspace/nuttx-fork/apps/nshlib/nsh_consolemain.c:81
   main
   /home/huang/workspace/nuttx-fork/apps/system/nsh/nsh_main.c:82
   _start
   /home/huang/workspace/nuttx-fork/nuttx/arch/risc-v/src/common/crt0.c:201
   ```
   
   And on TID 4 (`dumpstack` itself), also complete now:
   
   ```
   $ riscv64-unknown-elf-addr2line -f -e dumpstack 0xc0002738 0xc0000b44 
0xc0000aca 0xc0000a8c
   sys_call4
   /home/huang/workspace/nuttx-fork/nuttx/include/arch/syscall.h:288
   sched_dumpstack
   /home/huang/workspace/nuttx-fork/nuttx/libs/libc/sched/sched_dumpstack.c:71
   main
   /home/huang/workspace/nuttx-fork/apps/system/dumpstack/dumpstack.c:77 
(discriminator 1)
   _start
   /home/huang/workspace/nuttx-fork/nuttx/arch/risc-v/src/common/crt0.c:201
   ```
   
   Also re-verified okay on `nsh` and `knsh64_romfs`.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to