From b902f6395d80192470d3873cd0fb18af3bb5cb83 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Thu, 30 Jul 2026 14:16:04 +0200 Subject: [PATCH] arch/x86: Provide vfork(). x86 selected neither fork primitive, so vfork() was not available on this architecture at all. fork.S takes the register snapshot and hands it to x86_fork(), which allocates the child, copies the used part of the caller's stack, and starts it. There is one entry point for both primitives, because the snapshot is the same for either. Unlike the register-passing architectures, cdecl puts the flag on the stack, so up_fork() loads it from 4(%esp). That slot is also the stack pointer the caller had: it pushed the argument, then `call' pushed the return address. So the low end of the region that x86_fork() copies is unchanged. POSIX fork() is not provided. It needs an address environment that can be duplicated and this architecture has none, so CONFIG_ARCH_HAVE_FORK is never set here. x86_fork.c makes that a build error rather than a silent omission. Verified under QEMU with qemu-i486:nsh. ostest runs to the end and reports "Child 5 ran and exited before the parent resumed", with fork() correctly absent. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/Kconfig | 1 + arch/x86/src/common/fork.S | 150 +++++++++++++++ arch/x86/src/common/x86_fork.c | 321 +++++++++++++++++++++++++++++++++ arch/x86/src/common/x86_fork.h | 92 ++++++++++ arch/x86/src/qemu/Make.defs | 5 + 5 files changed, 569 insertions(+) create mode 100644 arch/x86/src/common/fork.S create mode 100644 arch/x86/src/common/x86_fork.c create mode 100644 arch/x86/src/common/x86_fork.h diff --git a/arch/Kconfig b/arch/Kconfig index 60039f1991e4e..22ed70d78af12 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -130,6 +130,7 @@ config ARCH_SIM config ARCH_X86 bool "x86" select ARCH_HAVE_TCBINFO + select ARCH_HAVE_VFORK ---help--- Intel x86 architectures. diff --git a/arch/x86/src/common/fork.S b/arch/x86/src/common/fork.S new file mode 100644 index 0000000000000..e090afea27bef --- /dev/null +++ b/arch/x86/src/common/fork.S @@ -0,0 +1,150 @@ +/**************************************************************************** + * arch/x86/src/common/fork.S + * + * 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 "x86_fork.h" + + .file "fork.S" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: up_fork + * + * Description: + * The architecture-specific entry point of fork() and vfork(). It records + * what the caller was doing and hands that to x86_fork(). See x86_fork.c + * for what happens from there. + * + * This architecture has no address environment that can be duplicated, so + * CONFIG_ARCH_HAVE_FORK is never set for it and only vfork() reaches here. + * The flag is passed on all the same, because the entry point is shared. + * + * Input Parameters: + * vfork - true for vfork(), false for fork(). cdecl puts it on the stack, + * so unlike the register-passing architectures it is loaded here. + * + * Returned Value: + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the caller. Otherwise, -1 is returned to + * the caller, no child is created, and errno is set to indicate the error. + * + ****************************************************************************/ + +/* The snapshot, as it sits on the caller's stack once it is built. The entry + * point `jmp's here rather than calling, so the return address at the top on + * entry is the original caller's and %esp + 4 is the stack pointer it had + * before it called us. + * + * | ......... | + * | ret addr | <- %esp on entry here; caller's %esp is this + 4 + * | eip | \ + * | eflags | | + * | ss | | + * | cs | | + * | ds | | struct fork_s + * | esp | | + * | ebp | | + * | ebx | | + * | esi | | + * | edi | / <- %esp, and the `context' argument to x86_fork() + */ + + .globl up_fork + .type up_fork, @function + +up_fork: + /* The flag is the first and only argument. At entry (%esp) holds the + * return address and 4(%esp) holds the argument, which is also the stack + * pointer the caller had: it pushed the argument, then `call' pushed the + * return address. So the LEA below still names the caller's own stack. + */ + + movl 4(%esp), %edx + jmp x86_fork_common + .size up_fork, . - up_fork + + .type x86_fork_common, @function + +x86_fork_common: + /* Take the two things that are read off this frame before any of it is + * disturbed: where the caller would have returned to, and the stack + * pointer it had -- one slot above the return address that the `call' + * reaching the entry point left behind. That stack pointer is the low + * end of the region x86_fork() copies. + * + * Neither MOV nor LEA writes EFLAGS, so the PUSHFL below still records + * the caller's own flags. + */ + + movl (%esp), %ecx + leal 4(%esp), %eax + + /* Build struct fork_s, highest-addressed field first. Segment + * selectors go through a zeroed register: `pushl %' leaves the + * upper half of the stack slot unmodified on most implementations + * rather than zero-extending it, and the C side reads all 32 bits. + */ + + pushl %ecx /* eip -- the caller's return address */ + pushfl /* eflags */ + xorl %ecx, %ecx + movw %ss, %cx + pushl %ecx /* ss */ + movw %cs, %cx + pushl %ecx /* cs */ + movw %ds, %cx + pushl %ecx /* ds */ + pushl %eax /* esp */ + pushl %ebp /* ebp */ + pushl %ebx /* ebx */ + pushl %esi /* esi */ + pushl %edi /* edi */ + + /* x86_fork(context, type) */ + + movl %esp, %eax + pushl %edx + pushl %eax + call x86_fork + addl $8, %esp + + /* The return value is in %eax and stays there. Restore the + * callee-saved registers from the snapshot and discard the rest of it; + * the caller's return address is then back on top for the `ret'. + */ + + popl %edi + popl %esi + popl %ebx + popl %ebp + addl $24, %esp /* esp, ds, cs, ss, eflags, eip */ + ret + .size x86_fork_common, . - x86_fork_common + .end diff --git a/arch/x86/src/common/x86_fork.c b/arch/x86/src/common/x86_fork.c new file mode 100644 index 0000000000000..d8783ef3520e9 --- /dev/null +++ b/arch/x86/src/common/x86_fork.c @@ -0,0 +1,321 @@ +/**************************************************************************** + * arch/x86/src/common/x86_fork.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 +#include +#include +#include + +#include "x86_fork.h" +#include "x86_internal.h" +#include "sched/sched.h" + +/* This architecture gives the child a relocated copy of the parent's stack; + * a borrowed stack would need that relocation to be skipped. + */ + +#ifdef CONFIG_ARCH_VFORK_STACK_BORROW +# error "x86 relocates the vfork() child stack; borrowing is not supported" +#endif + +/* And it has no address environment, so there is no POSIX fork() here. The + * assembly half provides no up_fork() entry point either. + */ + +#ifdef CONFIG_ARCH_HAVE_FORK +# error "x86 has no address environment; fork() cannot be provided" +#endif + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: x86_fork_stacktop + * + * Description: + * The high end of a task's stack, and the point the copy made by a fork is + * aligned to. + * + * Unlike x86_64, this architecture keeps the register save area inside the + * TCB -- xcp.regs is an array, not a pointer into the stack -- so all the + * stack down from the top belongs to the task and nothing has to be held + * back. It is also the value up_initial_state() puts in REG_SP, and + * up_stack_frame() only ever takes from the bottom, so the top does not + * move for the rest of the task's life. + * + * Input Parameters: + * tcb - The task whose stack is in question + * + * Returned Value: + * The address one past the last byte of the task's stack. + * + ****************************************************************************/ + +static uint32_t x86_fork_stacktop(struct tcb_s *tcb) +{ + return (uint32_t)tcb->stack_base_ptr + tcb->adj_stack_size; +} + +/**************************************************************************** + * Name: x86_fork_reloc + * + * Description: + * Carry one address from the parent's stack over to the child's copy of + * it. Addresses outside the copied region are returned unchanged: they + * point somewhere the child shares with the parent, or somewhere that has + * no counterpart at all. + * + * Input Parameters: + * addr - The address to relocate + * esp - The parent's stack pointer where the primitive was called, + * which is the low end of the region that was copied + * stacktop - The high end of the region that was copied + * offset - The distance from the parent's stack to the child's copy + * + * Returned Value: + * The relocated address. + * + ****************************************************************************/ + +static uint32_t x86_fork_reloc(uint32_t addr, uint32_t esp, + uint32_t stacktop, uint32_t offset) +{ + if (addr >= esp && addr < stacktop) + { + return addr + offset; + } + + return addr; +} + +/**************************************************************************** + * Name: x86_fork_relocfp + * + * Description: + * Relocate the saved frame-pointer chain inside the child's copy of the + * parent's stack. + * + * This is not optional on either x86. A function returns here with + * `leave', which is `mov %ebp,%esp' followed by `pop %ebp': the frame + * pointer feeds the *stack* pointer. Relocating only the EBP the child + * resumes with therefore gets it exactly one frame; the moment it returns + * through the next one it loads a saved EBP that still points into the + * parent's stack, and from then on the child runs on the parent's stack. + * It looks like the child is working -- it is even at the right offset -- + * until something returns through a slot the parent has since reused. + * + * The architectures that return through a link register do not need this: + * there a stale frame pointer spoils a backtrace and nothing else. + * + * The walk stops at the first link that leaves the copied region -- the + * outermost frame's saved EBP does -- and refuses to move backwards, so a + * corrupt chain terminates it rather than looping. + * + * Input Parameters: + * ebp - The parent's frame pointer where the primitive was called + * esp - The parent's stack pointer, the low end of the copied region + * stacktop - The high end of the copied region + * offset - The distance from the parent's stack to the child's copy + * + ****************************************************************************/ + +static void x86_fork_relocfp(uint32_t ebp, uint32_t esp, + uint32_t stacktop, uint32_t offset) +{ + while (ebp >= esp && ebp < stacktop) + { + uint32_t *slot = (uint32_t *)(ebp + offset); + uint32_t next = *slot; + + if (next <= ebp || next >= stacktop) + { + break; + } + + *slot = next + offset; + ebp = next; + } +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: x86_fork + * + * Description: + * The architecture-specific half of NuttX's cloning primitives. Which one + * is being performed is given by `vfork'. This architecture provides only + * vfork() -- POSIX fork() needs an address environment to duplicate and + * there is none here -- but the flag is carried all the same, because the + * generic half takes it. + * + * The child is given a stack of its own holding a relocated copy of the + * part of the parent's that is in use. The caller is always cloned as it + * stood at the call, because there is only one way to get here: an + * ordinary function call into fork.S. A flat build has no system-call + * boundary for the caller to be on the far side of, so there is no second + * path of the kind x86_64 needs. + * + * The overall sequence is: + * + * 1) User code calls vfork(). up_fork() in fork.S collects context + * information and transfers control to x86_fork(). + * 2) x86_fork() calls nxtask_setup_fork(). + * 3) nxtask_setup_fork() allocates and configures the child task's TCB. + * This consists of: + * - Allocation of the child task's TCB. + * - Initialization of file descriptors and streams + * - Configuration of environment variables + * - Allocate and initialize the stack + * - Setup the input parameters for the task. + * - Initialization of the TCB (including call to up_initial_state()) + * 4) x86_fork() provides any additional operating context. It must: + * - Initialize special values in any CPU registers that were not + * already configured by up_initial_state() + * 5) x86_fork() then calls nxtask_start_fork() + * 6) nxtask_start_fork() then executes the child thread. + * + * nxtask_abort_fork() may be called if an error occurs between steps 3 + * and 6. + * + * Input Parameters: + * context - Caller context information saved by fork.S + * vfork - true for vfork(), false for fork() + * + * Returned Value: + * Upon successful completion, 0 is returned to the child and the process + * ID of the child is returned to the caller. Otherwise, -1 is returned to + * the caller, no child is created, and errno is set to indicate the error. + * + ****************************************************************************/ + +pid_t x86_fork(const struct fork_s *context, bool vfork) +{ + struct tcb_s *parent = this_task(); + struct tcb_s *child; + uint32_t newsp; + uint32_t newtop; + uint32_t offset; + uint32_t stacktop; + uint32_t stackutil; + + sinfo("fork context [%p]:\n", context); + sinfo(" ebx:%08" PRIx32 " esi:%08" PRIx32 " edi:%08" PRIx32 "\n", + context->ebx, context->esi, context->edi); + sinfo(" esp:%08" PRIx32 " ebp:%08" PRIx32 " eip:%08" PRIx32 "\n", + context->esp, context->ebp, context->eip); + + /* Allocate and initialize a TCB for the child task. The child resumes at + * the instruction the caller would have returned to. + */ + + child = nxtask_setup_fork((start_t)context->eip, vfork); + if (!child) + { + serr("ERROR: nxtask_setup_fork failed\n"); + return (pid_t)ERROR; + } + + sinfo("TCBs: Parent=%p Child=%p\n", parent, child); + + /* How much of the parent's stack was utilized? x86 uses a push-down + * stack, so the caller's stack pointer is below the top of its stack and + * the difference between the two is what is in use. + */ + + stacktop = x86_fork_stacktop(parent); + DEBUGASSERT(stacktop > context->esp); + stackutil = stacktop - context->esp; + + /* Give the child that part of the parent's stack, copied to the same place + * in its own. The copy is aligned with the top of each stack rather than + * the bottom, so a single offset carries any address in the copied region + * from one to the other. + * + * This is a feeble effort in the sense arm_fork.c uses the word: the + * copied bytes surely contain pointers to things that do not exist in the + * child. What is corrected below is the links -- the frame chain -- and + * not the data. A caller that observes the caveats of these primitives + * never reads any of it. + */ + + newtop = x86_fork_stacktop(child); + newsp = newtop - stackutil; + offset = newtop - stacktop; + + memcpy((void *)newsp, (const void *)context->esp, stackutil); + + sinfo("Parent: stackutil:%" PRIu32 " stack top:%08" PRIx32 + " esp:%08" PRIx32 "\n", stackutil, stacktop, context->esp); + sinfo("Child: stack top:%08" PRIx32 " esp:%08" PRIx32 "\n", + newtop, newsp); + + /* Build the register context the child is resumed from. It was zeroed by + * up_initial_state(), which also filled in the entry point, the segments + * and the flags; what is left is what the child inherits from the caller. + * + * x86_fullcontextrestore() resumes it with an IRET off REG_SP, so REG_SP, + * REG_EIP, REG_CS and REG_EFLAGS are the frame it builds, and REG_ESP -- + * the copy of the stack pointer that PUSHA leaves -- is not read. + */ + + child->xcp.regs[REG_EAX] = 0; /* Child returns 0 */ + child->xcp.regs[REG_EBX] = context->ebx; /* Callee-saved */ + child->xcp.regs[REG_ESI] = context->esi; /* Callee-saved */ + child->xcp.regs[REG_EDI] = context->edi; /* Callee-saved */ + child->xcp.regs[REG_DS] = context->ds; + child->xcp.regs[REG_CS] = context->cs; + child->xcp.regs[REG_SS] = context->ss; + child->xcp.regs[REG_EFLAGS] = context->eflags; + child->xcp.regs[REG_EIP] = context->eip; + child->xcp.regs[REG_SP] = newsp; + + /* The frame pointer, and the chain of saved frame pointers it heads, move + * with the stack they point into. + */ + + child->xcp.regs[REG_EBP] = x86_fork_reloc(context->ebp, context->esp, + stacktop, offset); + x86_fork_relocfp(context->ebp, context->esp, stacktop, offset); + + /* And, finally, start the child task. On a failure, nxtask_start_fork() + * will discard the TCB by calling nxtask_abort_fork(). + */ + + return nxtask_start_fork(child, vfork); +} diff --git a/arch/x86/src/common/x86_fork.h b/arch/x86/src/common/x86_fork.h new file mode 100644 index 0000000000000..0dc1198e24c53 --- /dev/null +++ b/arch/x86/src/common/x86_fork.h @@ -0,0 +1,92 @@ +/**************************************************************************** + * arch/x86/src/common/x86_fork.h + * + * 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. + * + ****************************************************************************/ + +#ifndef __ARCH_X86_SRC_COMMON_X86_FORK_H +#define __ARCH_X86_SRC_COMMON_X86_FORK_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +#ifndef __ASSEMBLY__ +# include +# include +#endif + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define FORK_SIZEOF (sizeof(struct fork_s)) + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +#ifndef __ASSEMBLY__ + +/* The caller snapshot fork.S builds on the caller's own stack, in the order + * the pushes leave it in memory: lowest address first. Only the registers + * the i386 System V ABI makes callee-saved are here -- EBX, ESI, EDI and EBP + * -- plus what is needed to resume the caller: its stack pointer, its + * segment selectors, its flags and the address it would have returned to. + * + * EAX, ECX and EDX are absent deliberately. They are caller-saved, so the + * caller has no expectation of them surviving the call, and the child gets + * EAX = 0 as its return value. + */ + +struct fork_s +{ + uint32_t edi; /* 0 */ + uint32_t esi; /* 4 */ + uint32_t ebx; /* 8 */ + uint32_t ebp; /* 12 */ + uint32_t esp; /* 16 */ + uint32_t ds; /* 20 */ + uint32_t cs; /* 24 */ + uint32_t ss; /* 28 */ + uint32_t eflags; /* 32 */ + uint32_t eip; /* 36 */ + + /* The x87/FPU state is not saved. This architecture does not save it on a + * context switch either -- see i486_savestate.c -- so there is nothing for + * the child to inherit that the parent itself preserves across a call. + */ +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +/* The C half of the primitives, called from fork.S with the snapshot above + * and the vfork flag. + */ + +pid_t x86_fork(const struct fork_s *context, bool vfork); + +#endif /* __ASSEMBLY__ */ + +#endif /* __ARCH_X86_SRC_COMMON_X86_FORK_H */ diff --git a/arch/x86/src/qemu/Make.defs b/arch/x86/src/qemu/Make.defs index 034d5ca5bc8e9..0e2435df73deb 100644 --- a/arch/x86/src/qemu/Make.defs +++ b/arch/x86/src/qemu/Make.defs @@ -37,6 +37,11 @@ CMN_CSRCS += i486_irq.c i486_regdump.c i486_releasestack.c CMN_CSRCS += i486_savestate.c i486_stackframe.c CMN_CSRCS += i486_usestack.c +ifneq ($(filter y,$(CONFIG_ARCH_HAVE_TASK_FORK) $(CONFIG_ARCH_HAVE_VFORK)),) +CMN_ASRCS += fork.S +CMN_CSRCS += x86_fork.c +endif + ifeq ($(CONFIG_ENABLE_ALL_SIGNALS),y) CMN_CSRCS += i486_schedulesigaction.c i486_sigdeliver.c endif