From 2a20b1747a64e116a62ed7a48a6221ac57eac636 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Thu, 30 Jul 2026 14:16:02 +0200 Subject: [PATCH 1/2] mm/pgalloc: support 32 KB and 64 KB page sizes Add CONFIG_MM_PGSIZE == 32768 and 65536 to the page-size switch (and the Kconfig help text). The 64 KB size matches the ESP32-S3 cache-MMU page granularity, so an address-environment port there can use one mm_pgalloc() page per cache-MMU page (naturally 64 KB-aligned by the granule allocator) instead of coalescing several smaller pages. Inert for existing configs: MM_PGSIZE is only used when CONFIG_MM_PGALLOC is enabled (BUILD_KERNEL). Assisted-by: Claude Opus 4.8 (1M context) Signed-off-by: Marco Casaroli --- include/nuttx/pgalloc.h | 6 ++++++ mm/Kconfig | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/include/nuttx/pgalloc.h b/include/nuttx/pgalloc.h index 33f6a937b4593..bec03d9b4e719 100644 --- a/include/nuttx/pgalloc.h +++ b/include/nuttx/pgalloc.h @@ -69,6 +69,12 @@ #elif CONFIG_MM_PGSIZE == 16384 # define MM_PGSIZE 16384 # define MM_PGSHIFT 14 +#elif CONFIG_MM_PGSIZE == 32768 +# define MM_PGSIZE 32768 +# define MM_PGSHIFT 15 +#elif CONFIG_MM_PGSIZE == 65536 +# define MM_PGSIZE 65536 +# define MM_PGSHIFT 16 #else # error CONFIG_MM_PGSIZE not supported #endif diff --git a/mm/Kconfig b/mm/Kconfig index aebba1d0130fe..e0b8441a6ae71 100644 --- a/mm/Kconfig +++ b/mm/Kconfig @@ -301,9 +301,10 @@ config MM_PGSIZE int "Page Size" default 4096 ---help--- - The MMU page size. Must be one of {1024, 2048, 4096, 8192, or - 16384}. This is easily extensible, but only those values are - currently support. + The MMU page size. Must be one of {1024, 2048, 4096, 8192, 16384, + 32768, or 65536}. This is easily extensible, but only those values + are currently support. 64 KB (65536) matches the ESP32-S3 cache-MMU + page size. config DEBUG_PGALLOC bool "Page Allocator Debug" From f67dbec2adb716abe299af17f74a133b93bb48c1 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 10 Aug 2026 15:17:24 +0200 Subject: [PATCH 2/2] xtensa: Support BUILD_KERNEL. Add what a kernel build needs on Xtensa: a crt0 for a user process, the kernel stack allocation that a system call switches to, the syscall entry and return path for an unprivileged caller, and the initial register state that starts a user task at EL0 with its save area on the kernel stack. On the ESP32-S3 the arch code that runs while the flash mapping is in flux moves to IRAM, and the kernel heap is placed above the user .bss so that up_allocate_kheap() and the user address environment do not overlap. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/xtensa/include/irq.h | 17 ++ arch/xtensa/src/Makefile | 14 +- arch/xtensa/src/common/Make.defs | 6 +- arch/xtensa/src/common/crt0.c | 48 ++++++ .../xtensa/src/common/xtensa_addrenv_kstack.c | 129 ++++++++++++++++ arch/xtensa/src/common/xtensa_initialstate.c | 13 ++ arch/xtensa/src/common/xtensa_swint.c | 145 ++++++++++++++++++ arch/xtensa/src/lx7/Toolchain.defs | 11 +- 8 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 arch/xtensa/src/common/xtensa_addrenv_kstack.c diff --git a/arch/xtensa/include/irq.h b/arch/xtensa/include/irq.h index 537262ebae3ae..507f97fc7b8ad 100644 --- a/arch/xtensa/include/irq.h +++ b/arch/xtensa/include/irq.h @@ -216,6 +216,23 @@ struct xcptcontext uint32_t *regs; +#ifdef CONFIG_ARCH_KERNEL_STACK + /* In a kernel build the kernel cannot run on the stack of the process it + * is working for. That stack lives in a cache-MMU window which + * up_addrenv_select() reprograms, so it would move out from under the + * kernel the moment it touched another process's address environment -- + * taking the exception frame and every spilled register window with it. + * Each thread therefore gets a small stack of its own in kernel memory, + * which no address environment change can disturb. + */ + + uint32_t *kstack; /* Allocated base of the kernel stack */ + uint32_t *ktopstk; /* Top of the kernel stack (initial stack pointer) */ + uint32_t *ustkptr; /* Saved user stack pointer, while in a system call */ + uint32_t *kstkptr; /* Saved kernel stack pointer, while a user signal + * handler runs on the user stack */ +#endif + #ifdef CONFIG_LIB_SYSCALL /* The following array holds the return address and the exc_return value * needed to return from each nested system call. diff --git a/arch/xtensa/src/Makefile b/arch/xtensa/src/Makefile index 31425e38fa3bc..522e622be0cf3 100644 --- a/arch/xtensa/src/Makefile +++ b/arch/xtensa/src/Makefile @@ -199,10 +199,20 @@ ifneq ($(CONFIG_WINDOWS_NATIVE),y) endif # This is part of the top-level export target +# +# A kernel build links its user programs outside this tree, against the +# export package, so crt0 has to travel with it: apps/import expects to +# find it as startup/crt0.o. + +ifeq ($(CONFIG_BUILD_KERNEL),y) +EXPORT_STARTUP_OBJS = $(STARTUP_OBJS) $(STARTUP_ELF_OBJS) +else +EXPORT_STARTUP_OBJS = $(STARTUP_OBJS) +endif -export_startup: $(STARTUP_OBJS) +export_startup: $(EXPORT_STARTUP_OBJS) $(Q) if [ -d "$(EXPORT_DIR)/startup" ]; then \ - cp -f $(STARTUP_OBJS) "$(EXPORT_DIR)/startup"; \ + cp -f $(EXPORT_STARTUP_OBJS) "$(EXPORT_DIR)/startup"; \ else \ echo "$(EXPORT_DIR)/startup does not exist"; \ exit 1; \ diff --git a/arch/xtensa/src/common/Make.defs b/arch/xtensa/src/common/Make.defs index 3e1ea13a78582..5aee976930081 100644 --- a/arch/xtensa/src/common/Make.defs +++ b/arch/xtensa/src/common/Make.defs @@ -81,7 +81,11 @@ ifeq ($(CONFIG_XTENSA_SEMIHOSTING_HOSTFS),y) CMN_CSRCS += xtensa_hostfs.c endif -ifeq ($(CONFIG_BUILD_PROTECTED),y) +ifeq ($(CONFIG_ARCH_KERNEL_STACK),y) + CMN_CSRCS += xtensa_addrenv_kstack.c +endif + +ifneq ($(CONFIG_BUILD_FLAT),y) CMN_UASRCS += xtensa_signal_handler.S CMN_ASRCS += xtensa_dispatch_syscall.S CMN_CSRCS += xtensa_task_start.c xtensa_pthread_start.c diff --git a/arch/xtensa/src/common/crt0.c b/arch/xtensa/src/common/crt0.c index 89b21b78db07e..5dd1458e9822a 100644 --- a/arch/xtensa/src/common/crt0.c +++ b/arch/xtensa/src/common/crt0.c @@ -25,6 +25,7 @@ ****************************************************************************/ #include +#include #include #include @@ -55,6 +56,53 @@ int main(int argc, char *argv[]); * Private Functions ****************************************************************************/ +#ifdef CONFIG_BUILD_KERNEL + +/**************************************************************************** + * Name: sig_trampoline + * + * Description: + * The user-space signal handler trampoline. A kernel build cannot reach + * the one in xtensa_signal_handler.S -- that lives in libarch, which user + * programs do not link -- so it is carried here in crt0 instead, and + * _start() publishes it to the kernel through ARCH_DATA_RESERVE. The + * kernel enters it from the SYS_signal_handler case of xtensa_swint(). + * + * Written as file-scope assembly rather than as a naked function because + * GCC does not implement the naked attribute on Xtensa: it would emit a + * window-rotating prologue and quietly invalidate the register assignments + * below. + * + * Input Parameters: + * a2 = sighand, the user-space signal handling function + * a3, a4, a5 = signo, info and ucontext, its arguments + * + * Returned Value: + * None. This function does not return in the normal sense; it returns + * via the SYS_signal_handler_return syscall. + * + ****************************************************************************/ + +__asm__ +( + " .text\n" + " .global sig_trampoline\n" + " .type sig_trampoline, @function\n" + " .align 4\n" + "sig_trampoline:\n" + " mov a6, a3\n" /* Move signo into the callee's a2 */ + " mov a7, a4\n" /* Move info into the callee's a3 */ + " mov a8, a5\n" /* Move ucontext into the callee's a4 */ + " callx4 a2\n" /* Call the signal handler */ + " movi a2, " STRINGIFY(SYS_signal_handler_return) "\n" + " syscall\n" /* Will not return */ + " .size sig_trampoline, .-sig_trampoline\n" +); + +void sig_trampoline(void); + +#endif /* CONFIG_BUILD_KERNEL */ + #ifdef CONFIG_HAVE_CXXINITIALIZE /**************************************************************************** diff --git a/arch/xtensa/src/common/xtensa_addrenv_kstack.c b/arch/xtensa/src/common/xtensa_addrenv_kstack.c new file mode 100644 index 0000000000000..57bb749588062 --- /dev/null +++ b/arch/xtensa/src/common/xtensa_addrenv_kstack.c @@ -0,0 +1,129 @@ +/**************************************************************************** + * arch/xtensa/src/common/xtensa_addrenv_kstack.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include + +#include +#include +#include + +#include "xtensa.h" + +#if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_KERNEL_STACK) + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* The Xtensa windowed ABI requires 16-byte stack alignment */ + +#define KSTACK_ALIGNMENT 16 +#define KSTACK_ALIGN_DOWN(a) ((a) & ~(KSTACK_ALIGNMENT - 1)) + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: up_addrenv_kstackalloc + * + * Description: + * This function is called when a new thread is created to allocate the + * new thread's kernel stack. This function may be called for certain + * terminating threads which have no kernel stack. It must be tolerant of + * that case. + * + * The stack comes from the kernel heap, which lives in internal SRAM and + * is mapped identically no matter which address environment is selected. + * That is the whole point of it: the kernel needs somewhere to keep the + * exception frame and its spilled register windows that does not move when + * up_addrenv_select() reprograms the user cache-MMU windows. + * + * Input Parameters: + * tcb - The TCB of the thread that requires the kernel stack. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int up_addrenv_kstackalloc(struct tcb_s *tcb) +{ + DEBUGASSERT(tcb && tcb->xcp.kstack == NULL); + + tcb->xcp.kstack = kmm_memalign(KSTACK_ALIGNMENT, ARCH_KERNEL_STACKSIZE); + if (tcb->xcp.kstack == NULL) + { + berr("ERROR: Failed to allocate the kernel stack\n"); + return -ENOMEM; + } + + /* Xtensa stacks grow down and must stay aligned, so the usable top is the + * far end of the allocation. + */ + + tcb->xcp.ktopstk = (uint32_t *) + KSTACK_ALIGN_DOWN((uintptr_t)tcb->xcp.kstack + ARCH_KERNEL_STACKSIZE); + + return OK; +} + +/**************************************************************************** + * Name: up_addrenv_kstackfree + * + * Description: + * This function is called when any thread exits. This function frees + * the kernel stack. + * + * Input Parameters: + * tcb - The TCB of the thread that no longer requires the kernel stack. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int up_addrenv_kstackfree(struct tcb_s *tcb) +{ + DEBUGASSERT(tcb); + + /* Does the exiting thread have a kernel stack? */ + + if (tcb->xcp.kstack != NULL) + { + kmm_free(tcb->xcp.kstack); + tcb->xcp.kstack = NULL; + tcb->xcp.ktopstk = NULL; + } + + return OK; +} + +#endif /* CONFIG_ARCH_ADDRENV && CONFIG_ARCH_KERNEL_STACK */ diff --git a/arch/xtensa/src/common/xtensa_initialstate.c b/arch/xtensa/src/common/xtensa_initialstate.c index 1a0a14579c2e6..9a158281e4c49 100644 --- a/arch/xtensa/src/common/xtensa_initialstate.c +++ b/arch/xtensa/src/common/xtensa_initialstate.c @@ -81,11 +81,24 @@ void up_initial_state(struct tcb_s *tcb) const uint32_t base = ALIGN_UP((uint32_t)&_rodata_reserved_align, TCB_SIZE); #endif +#ifdef CONFIG_ARCH_KERNEL_STACK + /* The kernel stack is allocated before the thread's initial state is set + * up, so hold on to it across the wipe below. + */ + + uint32_t *kstack = xcp->kstack; + uint32_t *ktopstk = xcp->ktopstk; +#endif /* Initialize the initial exception register context structure */ memset(xcp, 0, sizeof(struct xcptcontext)); +#ifdef CONFIG_ARCH_KERNEL_STACK + xcp->kstack = kstack; + xcp->ktopstk = ktopstk; +#endif + /* Initialize the idle thread stack */ if (tcb->pid == IDLE_PROCESS_ID) diff --git a/arch/xtensa/src/common/xtensa_swint.c b/arch/xtensa/src/common/xtensa_swint.c index 4b38987e17e38..b89d1978fd68c 100644 --- a/arch/xtensa/src/common/xtensa_swint.c +++ b/arch/xtensa/src/common/xtensa_swint.c @@ -32,6 +32,7 @@ #include #include +#include #include #include "sched/sched.h" @@ -39,6 +40,19 @@ #include "signal/signal.h" #include "xtensa.h" +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_ARCH_KERNEL_STACK +/* A stack pointer is 16-byte aligned, and the windowed ABI reserves the + * 16 bytes below one as the base save area of the frame that owns it. + */ + +# define SIGTRAMP_STACK_ALIGN 16 +# define SIGTRAMP_SAVE_AREA 16 +#endif + /**************************************************************************** * Private Functions ****************************************************************************/ @@ -102,6 +116,24 @@ int xtensa_swint(int irq, void *context, void *arg) case SYS_restore_context: case SYS_switch_context: { +#ifdef CONFIG_ARCH_ADDRENV + /* Close down the outgoing task's address environment and + * instantiate the incoming one. up_switch_context() is a + * SYS_switch_context call on this architecture, so this is the + * path every *voluntary* context switch takes -- without it a task + * resumed here keeps running against whatever address environment + * happened to be resident, which on the ESP32-S3 means the + * cache-MMU windows still point at another process's pages. + * + * addrenv_switch() may change this_task(), because dropping an + * address environment can post to the high-priority work queue, so + * re-read the TCB afterwards -- as arm_syscall.c does. + */ + + addrenv_switch(tcb); + tcb = this_task(); +#endif + restore_critical_section(tcb, this_cpu()); #ifdef CONFIG_DEBUG_SYSCALL_INFO svcinfo("SYSCALL Return: Context switch!\n"); @@ -151,6 +183,19 @@ int xtensa_swint(int irq, void *context, void *arg) rtcb->xcp.nsyscalls = index; +#ifdef CONFIG_ARCH_KERNEL_STACK + /* Leaving the outermost system call: hand the thread back its own + * stack, which it has not touched while the kernel borrowed its + * context. + */ + + if (index == 0 && rtcb->xcp.ustkptr != NULL) + { + regs[REG_A1] = (uintptr_t)rtcb->xcp.ustkptr; + rtcb->xcp.ustkptr = NULL; + } +#endif + /* Handle any signal actions that were deferred while processing * the system call. */ @@ -275,7 +320,11 @@ int xtensa_swint(int irq, void *context, void *arg) * unprivileged mode. */ +#if defined(CONFIG_BUILD_PROTECTED) regs[REG_PC] = (uintptr_t)USERSPACE->signal_handler; +#else + regs[REG_PC] = (uintptr_t)ARCH_DATA_RESERVE->ar_sigtramp; +#endif xtensa_lowerprivilege(regs); /* User mode */ @@ -287,6 +336,55 @@ int xtensa_swint(int irq, void *context, void *arg) regs[REG_A3] = regs[REG_A4]; /* signal */ regs[REG_A4] = regs[REG_A5]; /* info */ regs[REG_A5] = regs[REG_A6]; /* ucontext */ + +#ifdef CONFIG_ARCH_KERNEL_STACK + /* The handler runs in user mode, so it has to run on the user + * stack. Signal dispatch always reaches here on the thread's + * kernel stack -- up_schedule_sigaction() builds the dispatch + * context below the interrupted one -- so put that stack pointer + * aside and hand the thread its own stack back for the duration. + * + * Having a kernel stack at all is what says this is a user + * process. Testing xcp.ustkptr instead would be wrong: that + * holds the user stack pointer only while a system call is in + * progress, so a signal caught in user code would leave the + * handler running on the kernel stack. + */ + + if (rtcb->xcp.kstack != NULL) + { + uintptr_t usp; + + rtcb->xcp.kstkptr = (uint32_t *)regs[REG_A1]; + + /* The thread's own stack pointer is the one the system call + * saved if it was in one, and otherwise the one it was + * interrupted with, which up_schedule_sigaction() kept. + */ + + usp = rtcb->xcp.ustkptr != NULL ? + (uintptr_t)rtcb->xcp.ustkptr : + (uintptr_t)rtcb->xcp.saved_regs[REG_A1]; + + /* The siginfo passed in lives on the kernel stack, which the + * handler must not reach -- and cannot, once the permission + * control is programmed. Copy it onto the user stack and + * hand the handler that copy. + * + * Skip the base save area the windowed ABI keeps in the + * 16 bytes below a stack pointer: it belongs to the frame + * that was interrupted. + */ + + usp = (usp - SIGTRAMP_SAVE_AREA - sizeof(siginfo_t)) & + ~(SIGTRAMP_STACK_ALIGN - 1); + + memcpy((void *)usp, (void *)regs[REG_A4], sizeof(siginfo_t)); + + regs[REG_A4] = usp; /* info */ + regs[REG_A1] = usp; + } +#endif } break; #endif @@ -313,6 +411,20 @@ int xtensa_swint(int irq, void *context, void *arg) xtensa_raiseprivilege(regs); /* Privileged mode */ rtcb->xcp.sigreturn = 0; + +#ifdef CONFIG_ARCH_KERNEL_STACK + /* The handler is done: return to the kernel stack the signal + * dispatch was running on. + */ + + if (rtcb->xcp.kstack != NULL) + { + DEBUGASSERT(rtcb->xcp.kstkptr != NULL); + + regs[REG_A1] = (uintptr_t)rtcb->xcp.kstkptr; + rtcb->xcp.kstkptr = NULL; + } +#endif } break; #endif @@ -353,6 +465,39 @@ int xtensa_swint(int irq, void *context, void *arg) xtensa_raiseprivilege(regs); /* Privileged mode */ #endif +#ifdef CONFIG_ARCH_KERNEL_STACK + /* The system call itself runs in this task's own context, so + * without help it would run the kernel on the *user* stack. That + * cannot be allowed in a kernel build: the user stack lives in a + * cache-MMU window, and any system call that selects a different + * address environment -- exec() loading a program, for one -- + * reprograms that window and the kernel's stack disappears from + * under it, taking the frames it is standing on. + * + * So the outermost system call moves to the thread's kernel + * stack, which lives in kernel memory and is unaffected by + * address environment changes. Nested calls are already on it. + */ + + if (index == 0 && rtcb->xcp.ktopstk != NULL) + { + rtcb->xcp.ustkptr = (uint32_t *)regs[REG_A1]; + + /* Start at the top of the kernel stack -- unless a signal + * handler is running, in which case the kernel stack is in + * use down to the point the dispatch left it at, and this + * call has to continue below that. Restarting at the top + * would overwrite both the suspended signal dispatch and the + * context it saved to resume the thread with, which sits in + * the topmost frame. + */ + + regs[REG_A1] = rtcb->xcp.kstkptr != NULL ? + (uintptr_t)rtcb->xcp.kstkptr : + (uintptr_t)rtcb->xcp.ktopstk; + } +#endif + /* Offset A2 to account for the reserved values */ regs[REG_A2] -= CONFIG_SYS_RESERVED; diff --git a/arch/xtensa/src/lx7/Toolchain.defs b/arch/xtensa/src/lx7/Toolchain.defs index cd4e593230cff..26dcb1f053591 100644 --- a/arch/xtensa/src/lx7/Toolchain.defs +++ b/arch/xtensa/src/lx7/Toolchain.defs @@ -240,7 +240,16 @@ LDMODULEFLAGS = -r -T $(call CONVERT_PATH,$(TOPDIR)/libs/libc/elf/gnu-elf.ld) CELFFLAGS = $(CFLAGS) -fvisibility=hidden -mtext-section-literals CXXELFFLAGS = $(CXXFLAGS) -fvisibility=hidden -mtext-section-literals -LDELFFLAGS = -r -e _start +LDELFFLAGS = -e _start + +# A relocatable object is the default for loadable modules. A kernel build +# instead needs each user program fully linked at the addresses of its +# address environment, so the partial link is dropped there. + +ifeq ($(CONFIG_BINFMT_ELF_RELOCATABLE),y) + LDELFFLAGS += -r +endif + LDELFFLAGS += -T $(call CONVERT_PATH,$(TOPDIR)$(DELIM)libs$(DELIM)libc$(DELIM)elf$(DELIM)gnu-elf.ld) ifneq ($(CONFIG_BUILD_KERNEL),y) # Flat build and protected elf entry point use crt0,