mirror of
https://github.com/apache/nuttx.git
synced 2026-08-13 16:35:21 +00:00
arch/x86_64: Build fork() children from the caller's syscall frame.
In a kernel build vfork() is reached through a system call, so the return
address and stack pointer the architecture's entry point can see for itself
are the kernel's, not the caller's. A child built from those resumes at a
kernel address, which is why x86_64 selected the fork family only for the
flat build.
x86_64_syscall() now publishes the caller's frame in xcp.sregs for the
duration of the stub call, and x86_64_fork() builds the child from it:
x86_64_fork_syscall() when xcp.sregs is non-NULL, so that the child
returns from the very same `syscall' instruction as
the parent, in user mode, on its own stack;
x86_64_fork_direct() otherwise, which is the flat build and any kernel
thread that calls the entry point as a plain
function.
The discriminator is xcp.sregs rather than TCB_FLAG_SYSCALL, which arm64 and
RISC-V use: that flag also defers signal actions, x86_64 has never raised it,
and its kernel-build signal path does not survive being made to -- a
pre-existing problem that does not belong to this work.
Two properties of SYSCALL/SYSRET shape the child's frame. The instruction
leaves the caller's RIP and RFLAGS in RCX and R11 rather than on a stack, so
they are moved into the RIP and RFLAGS slots of the interrupt frame the child
is resumed from; and the hardware never records the caller's CS and SS at all,
SYSRETQ reconstructing them from IA32_STAR, so the child's are filled in with
the user code and data selectors at RPL 3. The frame is therefore not copied
wholesale: the extended state and the general registers are inherited, while
the segment registers and the thread pointer stay as up_initial_state() left
them, the child's stack being a fresh allocation the parent's FS base does not
describe.
x86_64_fork_relocfp() is new and is not optional here. A function returns
with `leave', which feeds the frame pointer into the stack pointer, so
relocating only the RBP the child resumes with gets it exactly one frame:
the next return loads a saved RBP still pointing into the parent's stack.
With that in place ARCH_X86_64 can select ARCH_HAVE_VFORK unconditionally.
Build-verified on qemu-intel64:knsh_romfs and qemu-intel64:ostest. NuttX on
qemu-intel64 requires tsc-deadline and pcid, which TCG does not implement, so
it cannot be run on this host.
Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
This commit is contained in:
parent
3557791ae2
commit
067e30e14f
5 changed files with 450 additions and 63 deletions
|
|
@ -153,7 +153,7 @@ exception frame when it traps -- ``xcp.sregs`` is the field that exists for
|
|||
this -- and build the child from that instead, while a kernel thread that calls
|
||||
the entry point directly still takes the ordinary path.
|
||||
|
||||
Three architectures do it, and they are worth copying:
|
||||
Four architectures do it, and they are worth copying:
|
||||
|
||||
* RISC-V: ``riscv_swint.c`` stores the frame in ``xcp.sregs``, and
|
||||
``riscv_fork.c`` rebuilds the child from it.
|
||||
|
|
@ -177,6 +177,21 @@ Three architectures do it, and they are worth copying:
|
|||
``arm_fork()`` runs, its PC, CPSR and SP are the kernel's; the caller's are
|
||||
read from where ``arm_syscall()`` put them -- ``syscall[0].sysreturn``,
|
||||
``syscall[0].cpsr`` and ``ustkptr``.
|
||||
* x86_64: ``x86_64_syscall()`` stores the frame in ``xcp.sregs``, and
|
||||
``x86_64_fork()`` dispatches to ``x86_64_fork_syscall()`` or
|
||||
``x86_64_fork_direct()``. The discriminator here is ``xcp.sregs`` itself
|
||||
being non-NULL, because raising ``TCB_FLAG_SYSCALL`` would also defer signal
|
||||
actions -- something x86_64 has never done and its kernel-build signal path
|
||||
does not currently survive. Two properties of ``SYSCALL``/``SYSRET`` shape
|
||||
the child's frame: the instruction leaves the caller's RIP and RFLAGS in
|
||||
RCX and R11 rather than on a stack, so they have to be moved into the RIP
|
||||
and RFLAGS slots of the interrupt frame the child is resumed from; and the
|
||||
hardware never records the caller's CS and SS at all -- ``SYSRETQ``
|
||||
reconstructs them from ``IA32_STAR`` -- so the child's have to be filled in
|
||||
with the user code and data selectors at RPL 3. For the same reason the
|
||||
saved frame is not copied wholesale: only the extended state and the
|
||||
general registers are inherited, and the segment registers and thread
|
||||
pointer come from the frame ``up_initial_state()`` built for the child.
|
||||
|
||||
Nothing else is required: the ``up_fork()`` entry point and the libc wrapper
|
||||
are already there and become live automatically.
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ config ARCH_X86_64
|
|||
select PCI_LATE_DRIVERS_REGISTER if PCI
|
||||
select ARCH_TOOLCHAIN_GNU
|
||||
select ARCH_HAVE_BACKTRACE
|
||||
select ARCH_HAVE_VFORK if !BUILD_KERNEL
|
||||
select ARCH_HAVE_VFORK
|
||||
select ARCH_HAVE_SETJMP
|
||||
select ARCH_HAVE_PERF_EVENTS
|
||||
select ARCH_HAVE_POWEROFF
|
||||
|
|
|
|||
|
|
@ -544,6 +544,18 @@ struct xcptcontext
|
|||
|
||||
uint64_t *regs;
|
||||
|
||||
#ifdef CONFIG_LIB_SYSCALL
|
||||
/* The register context of the user code that is currently in a system
|
||||
* call, as x86_64_syscall_entry() saved it on the kernel stack. This is
|
||||
* what the caller of a system call was doing, as opposed to xcp.regs,
|
||||
* which during a system call describes the kernel side of it.
|
||||
* x86_64_fork() needs it to build a child from the caller rather than
|
||||
* from the stub.
|
||||
*/
|
||||
|
||||
uint64_t *sregs;
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_ARCH_ADDRENV
|
||||
# ifdef CONFIG_ARCH_KERNEL_STACK
|
||||
/* In this configuration, all syscalls execute from an internal kernel
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
#include <nuttx/sched.h>
|
||||
#include <nuttx/arch.h>
|
||||
#include <arch/arch.h>
|
||||
#include <arch/irq.h>
|
||||
|
||||
#include "x86_64_fork.h"
|
||||
|
|
@ -42,57 +43,156 @@
|
|||
#include "sched/sched.h"
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
* Pre-processor Definitions
|
||||
****************************************************************************/
|
||||
|
||||
#ifdef CONFIG_LIB_SYSCALL
|
||||
|
||||
/* Requested privilege level 3 in a segment selector. A caller that reached
|
||||
* here through the `syscall' instruction was in user mode, and SYSRETQ is
|
||||
* going to put it back there with the selectors IA32_STAR describes -- see
|
||||
* x86_64_fork_syscall().
|
||||
*/
|
||||
|
||||
# define X86_GDT_RPL_USER 3
|
||||
|
||||
#endif
|
||||
|
||||
/****************************************************************************
|
||||
* Private Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: x86_64_fork
|
||||
* Name: x86_64_fork_stacktop
|
||||
*
|
||||
* Description:
|
||||
* The fork() function has the same effect as posix fork(), except that the
|
||||
* behavior is undefined if the process created by fork() either modifies
|
||||
* any data other than a variable of type pid_t used to store the return
|
||||
* value from fork(), or returns from the function in which fork() was
|
||||
* called, or calls any other function before successfully calling _exit()
|
||||
* or one of the exec family of functions.
|
||||
*
|
||||
* The overall sequence is:
|
||||
*
|
||||
* 1) User code calls fork(). fork() collects context information and
|
||||
* transfers control up x86_64_fork().
|
||||
* 2) x86_64_fork() and 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_64_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_64_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.
|
||||
* The high end of the part of a task's stack that a fork copies: the
|
||||
* bottom of the register save area that up_initial_state() reserved at the
|
||||
* very top of the stack. The save area itself is not stack and must not
|
||||
* be copied over -- it is where the child's own resume frame is built.
|
||||
*
|
||||
* Input Parameters:
|
||||
* vfork - true for vfork(), false for fork()
|
||||
* context - Caller context information saved by up_fork()
|
||||
* tcb - The task whose stack is in question
|
||||
*
|
||||
* Returned Value:
|
||||
* Upon successful completion, fork() returns 0 to the child process and
|
||||
* returns the process ID of the child process to the parent process.
|
||||
* Otherwise, -1 is returned to the parent, no child process is created,
|
||||
* and errno is set to indicate the error.
|
||||
* The address one past the last byte of stack that is copied.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
pid_t x86_64_fork(bool vfork, const struct fork_s *context)
|
||||
static uint64_t x86_64_fork_stacktop(struct tcb_s *tcb)
|
||||
{
|
||||
return (uint64_t)XCP_ALIGN_DOWN((uintptr_t)tcb->stack_base_ptr +
|
||||
tcb->adj_stack_size - XCPTCONTEXT_SIZE);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: x86_64_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
|
||||
* rsp - 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 uint64_t x86_64_fork_reloc(uint64_t addr, uint64_t rsp,
|
||||
uint64_t stacktop, uint64_t offset)
|
||||
{
|
||||
if (addr >= rsp && addr < stacktop)
|
||||
{
|
||||
return addr + offset;
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: x86_64_fork_relocfp
|
||||
*
|
||||
* Description:
|
||||
* Relocate the saved frame-pointer chain inside the child's copy of the
|
||||
* parent's stack.
|
||||
*
|
||||
* This is x86_64-specific and it is not optional. A function returns here
|
||||
* with `leave', which is `mov %rbp,%rsp' followed by `pop %rbp': the
|
||||
* frame pointer feeds the *stack* pointer. Relocating only the RBP the
|
||||
* child resumes with therefore gets it exactly one frame; the moment it
|
||||
* returns through the next one it loads a saved RBP 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 other architectures with this fork path do not need it: they return
|
||||
* through a link register, so 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 RBP does -- and refuses to move backwards, so a
|
||||
* corrupt chain terminates it rather than looping.
|
||||
*
|
||||
* Input Parameters:
|
||||
* rbp - The parent's frame pointer where the primitive was called
|
||||
* rsp - 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_64_fork_relocfp(uint64_t rbp, uint64_t rsp,
|
||||
uint64_t stacktop, uint64_t offset)
|
||||
{
|
||||
while (rbp >= rsp && rbp < stacktop)
|
||||
{
|
||||
uint64_t *slot = (uint64_t *)(rbp + offset);
|
||||
uint64_t next = *slot;
|
||||
|
||||
if (next <= rbp || next >= stacktop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
*slot = next + offset;
|
||||
rbp = next;
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: x86_64_fork_direct
|
||||
*
|
||||
* Description:
|
||||
* Clone a caller that reached up_fork() by an ordinary function call, so
|
||||
* that the register snapshot fork.S took describes the caller itself.
|
||||
* That is the case in a flat build, and for a kernel thread in any build.
|
||||
*
|
||||
* The child has no exception frame to inherit, so one is synthesised: it
|
||||
* resumes at the caller's return address, in the caller's own segments,
|
||||
* with the callee-saved registers the caller had.
|
||||
*
|
||||
* Input Parameters:
|
||||
* vfork - true for vfork(), false for fork()
|
||||
* parent - The calling task's TCB
|
||||
* context - Caller context information saved by fork.S
|
||||
*
|
||||
* Returned Value:
|
||||
* The process ID of the child, or ERROR on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static pid_t x86_64_fork_direct(bool vfork, struct tcb_s *parent,
|
||||
const struct fork_s *context)
|
||||
{
|
||||
struct tcb_s *parent = this_task();
|
||||
struct tcb_s *child;
|
||||
uint64_t newsp;
|
||||
uint64_t newfp;
|
||||
|
|
@ -120,15 +220,13 @@ pid_t x86_64_fork(bool vfork, const struct fork_s *context)
|
|||
|
||||
sinfo("TCBs: Parent=%p Child=%p\n", parent, child);
|
||||
|
||||
/* How much of the parent's stack was utilized? The ARM uses
|
||||
* a push-down stack so that the current stack pointer should
|
||||
* be lower than the initial, adjusted stack pointer. The
|
||||
* stack usage should be the difference between those two.
|
||||
/* How much of the parent's stack was utilized? x86_64 uses a push-down
|
||||
* stack so that the current stack pointer should be lower than the
|
||||
* initial, adjusted stack pointer. The stack usage should be the
|
||||
* difference between those two.
|
||||
*/
|
||||
|
||||
stacktop = (uint64_t)XCP_ALIGN_DOWN((uintptr_t)parent->stack_base_ptr +
|
||||
parent->adj_stack_size -
|
||||
XCPTCONTEXT_SIZE);
|
||||
stacktop = x86_64_fork_stacktop(parent);
|
||||
DEBUGASSERT(stacktop > context->rsp);
|
||||
stackutil = stacktop - context->rsp;
|
||||
|
||||
|
|
@ -161,25 +259,17 @@ pid_t x86_64_fork(bool vfork, const struct fork_s *context)
|
|||
* effort: the child is entitled to use them, and it does.
|
||||
*/
|
||||
|
||||
newtop = (uint64_t)XCP_ALIGN_DOWN((uintptr_t)child->stack_base_ptr +
|
||||
child->adj_stack_size -
|
||||
XCPTCONTEXT_SIZE);
|
||||
|
||||
newsp = newtop - stackutil;
|
||||
newtop = x86_64_fork_stacktop(child);
|
||||
newsp = newtop - stackutil;
|
||||
|
||||
memcpy((void *)newsp, (const void *)context->rsp, stackutil);
|
||||
|
||||
/* Was there a frame pointer in place before? */
|
||||
|
||||
if (context->rbp >= context->rsp && context->rbp < stacktop)
|
||||
{
|
||||
uint32_t frameutil = stacktop - context->rbp;
|
||||
newfp = newtop - frameutil;
|
||||
}
|
||||
else
|
||||
{
|
||||
newfp = context->rbp;
|
||||
}
|
||||
newfp = x86_64_fork_reloc(context->rbp, context->rsp, stacktop,
|
||||
newtop - stacktop);
|
||||
x86_64_fork_relocfp(context->rbp, context->rsp, stacktop,
|
||||
newtop - stacktop);
|
||||
|
||||
sinfo("Old stack top:%08" PRIx64 " RSP:%08" PRIx64
|
||||
" RBP:%08" PRIx64 "\n", stacktop, context->rsp, context->rbp);
|
||||
|
|
@ -213,3 +303,242 @@ pid_t x86_64_fork(bool vfork, const struct fork_s *context)
|
|||
|
||||
return nxtask_start_fork(child, vfork);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_LIB_SYSCALL
|
||||
|
||||
/****************************************************************************
|
||||
* Name: x86_64_fork_syscall
|
||||
*
|
||||
* Description:
|
||||
* Clone a caller that reached up_fork() through a system call. The
|
||||
* register snapshot fork.S took is useless here: it describes the
|
||||
* kernel-side stub, so a child built from it would resume at a kernel
|
||||
* address on a kernel stack. What the caller was actually doing is the
|
||||
* frame x86_64_syscall_entry() saved and x86_64_syscall() recorded in
|
||||
* xcp.sregs; the child is built from that.
|
||||
*
|
||||
* The child therefore returns from the very same `syscall' instruction as
|
||||
* the parent, in user mode, differing only in that it sees 0 as the return
|
||||
* value and runs on its own stack.
|
||||
*
|
||||
* Two details of the SYSCALL/SYSRET pair shape this:
|
||||
*
|
||||
* 1. `syscall' does not save the caller's RIP and RFLAGS on a stack; it
|
||||
* leaves them in RCX and R11, which is where the saved frame has them.
|
||||
* The child is resumed by IRETQ (x86_64_fullcontextrestore()), so they
|
||||
* have to be moved into the RIP and RFLAGS slots of its frame.
|
||||
* 2. The hardware never tells the kernel which CS and SS the caller had --
|
||||
* SYSRETQ reconstructs them from IA32_STAR -- so those slots of the
|
||||
* saved frame hold nothing, and the child's have to be filled with the
|
||||
* selectors SYSRETQ would have produced, which is where the parent is
|
||||
* about to return to.
|
||||
*
|
||||
* Everything the frame does hold -- the general registers and the extended
|
||||
* (FPU/SSE) state -- is inherited. Everything it does not is taken from
|
||||
* the frame up_initial_state() built for the child, so that the child
|
||||
* keeps its own segment registers and, importantly, its own thread
|
||||
* pointer: the child's stack is a fresh allocation at a different virtual
|
||||
* address, so the parent's FS base does not describe it.
|
||||
*
|
||||
* Input Parameters:
|
||||
* vfork - true for vfork(), false for fork()
|
||||
* parent - The calling task's TCB
|
||||
*
|
||||
* Returned Value:
|
||||
* The process ID of the child, or ERROR on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static pid_t x86_64_fork_syscall(bool vfork, struct tcb_s *parent)
|
||||
{
|
||||
uint64_t *sregs = parent->xcp.sregs;
|
||||
struct tcb_s *child;
|
||||
uint64_t newsp;
|
||||
uint64_t newtop;
|
||||
uint64_t offset;
|
||||
uint64_t stacktop;
|
||||
uint64_t stackutil;
|
||||
uint64_t rsp;
|
||||
uint64_t rip;
|
||||
|
||||
DEBUGASSERT(sregs != NULL);
|
||||
|
||||
/* Where the caller was and what it was doing */
|
||||
|
||||
rsp = sregs[REG_RSP];
|
||||
rip = sregs[REG_RCX];
|
||||
|
||||
sinfo("syscall frame [%p]: RSP:%08" PRIx64 " RIP:%08" PRIx64 "\n",
|
||||
sregs, rsp, rip);
|
||||
|
||||
/* Allocate and initialize a TCB for the child task. The child resumes at
|
||||
* the instruction after the `syscall', which is where the parent resumes
|
||||
* too.
|
||||
*/
|
||||
|
||||
child = nxtask_setup_fork((start_t)rip, vfork);
|
||||
if (!child)
|
||||
{
|
||||
serr("ERROR: nxtask_setup_fork failed\n");
|
||||
return (pid_t)ERROR;
|
||||
}
|
||||
|
||||
sinfo("TCBs: Parent=%p Child=%p\n", parent, child);
|
||||
|
||||
/* Give the child the part of the parent's stack that is in use, copied to
|
||||
* the same place in its own stack. 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.
|
||||
*/
|
||||
|
||||
stacktop = x86_64_fork_stacktop(parent);
|
||||
DEBUGASSERT(stacktop > rsp);
|
||||
stackutil = stacktop - rsp;
|
||||
|
||||
newtop = x86_64_fork_stacktop(child);
|
||||
newsp = newtop - stackutil;
|
||||
offset = newtop - stacktop;
|
||||
|
||||
memcpy((void *)newsp, (const void *)rsp, stackutil);
|
||||
|
||||
sinfo("Old stack top:%08" PRIx64 " RSP:%08" PRIx64 "\n", stacktop, rsp);
|
||||
sinfo("New stack top:%08" PRIx64 " RSP:%08" PRIx64 "\n", newtop, newsp);
|
||||
|
||||
/* Inherit the parent's extended (FPU/SSE) state, which
|
||||
* x86_64_syscall_entry saved at the front of the frame, exactly where
|
||||
* the child's belongs.
|
||||
*/
|
||||
|
||||
memcpy(child->xcp.regs, sregs, XCPTCONTEXT_XMM_AREA_SIZE);
|
||||
|
||||
/* Inherit the general registers. RCX and R11 are included deliberately:
|
||||
* SYSRETQ leaves the return address in RCX and RFLAGS in R11, so the
|
||||
* parent resumes with those values and the child must too.
|
||||
*/
|
||||
|
||||
child->xcp.regs[REG_RBX] = sregs[REG_RBX];
|
||||
child->xcp.regs[REG_R8] = sregs[REG_R8];
|
||||
child->xcp.regs[REG_R9] = sregs[REG_R9];
|
||||
child->xcp.regs[REG_R10] = sregs[REG_R10];
|
||||
child->xcp.regs[REG_R11] = sregs[REG_R11];
|
||||
child->xcp.regs[REG_R12] = sregs[REG_R12];
|
||||
child->xcp.regs[REG_R13] = sregs[REG_R13];
|
||||
child->xcp.regs[REG_R14] = sregs[REG_R14];
|
||||
child->xcp.regs[REG_R15] = sregs[REG_R15];
|
||||
child->xcp.regs[REG_RCX] = sregs[REG_RCX];
|
||||
child->xcp.regs[REG_RDX] = sregs[REG_RDX];
|
||||
child->xcp.regs[REG_RSI] = sregs[REG_RSI];
|
||||
child->xcp.regs[REG_RDI] = sregs[REG_RDI];
|
||||
|
||||
/* The frame pointer moves with the stack it points into */
|
||||
|
||||
child->xcp.regs[REG_RBP] = x86_64_fork_reloc(sregs[REG_RBP], rsp,
|
||||
stacktop, offset);
|
||||
x86_64_fork_relocfp(sregs[REG_RBP], rsp, stacktop, offset);
|
||||
|
||||
/* Build the interrupt frame the child is resumed from. RIP and RFLAGS
|
||||
* come out of RCX and R11, and the selectors are the ones SYSRETQ derives
|
||||
* from IA32_STAR: CS is the user code segment and SS the user data
|
||||
* segment, both at RPL 3. See x86_64_cpu_priv_set(), which programs
|
||||
* IA32_STAR.
|
||||
*/
|
||||
|
||||
child->xcp.regs[REG_RAX] = 0;
|
||||
child->xcp.regs[REG_RIP] = rip;
|
||||
child->xcp.regs[REG_RFLAGS] = sregs[REG_R11];
|
||||
child->xcp.regs[REG_RSP] = newsp;
|
||||
child->xcp.regs[REG_CS] = X86_GDT_USERCODE_SEL | X86_GDT_RPL_USER;
|
||||
child->xcp.regs[REG_SS] = X86_GDT_USERDATA_SEL | X86_GDT_RPL_USER;
|
||||
|
||||
#ifdef CONFIG_ARCH_KERNEL_STACK
|
||||
/* The child's own user stack pointer, for the signal dispatch path */
|
||||
|
||||
child->xcp.ustkptr = (uintptr_t *)newsp;
|
||||
#endif
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_LIB_SYSCALL */
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: x86_64_fork
|
||||
*
|
||||
* Description:
|
||||
* The common x86_64 worker behind up_fork(). vfork() and fork() snapshot
|
||||
* the caller's registers identically; `vfork' says which primitive was
|
||||
* called, and is passed straight through to nxtask_setup_fork(), which is
|
||||
* where the memory semantics are decided.
|
||||
*
|
||||
* The overall sequence is:
|
||||
*
|
||||
* 1) User code calls vfork() or fork(). The libc wrapper enters
|
||||
* up_fork(), which collects context information and transfers control
|
||||
* to x86_64_fork().
|
||||
* 2) x86_64_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_64_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_64_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.
|
||||
*
|
||||
* Everything above is common to the two ways this can be reached, which
|
||||
* differ only in where the caller's registers are to be found -- see
|
||||
* x86_64_fork_direct() and x86_64_fork_syscall().
|
||||
*
|
||||
* Input Parameters:
|
||||
* vfork - true for vfork(), false for fork()
|
||||
* context - Caller context information saved by fork.S
|
||||
*
|
||||
* Returned Value:
|
||||
* Upon successful completion, 0 is returned to the child and the process
|
||||
* ID of the child is returned to the parent. Otherwise, -1 is returned to
|
||||
* the parent, no child is created, and errno is set to indicate the error.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
pid_t x86_64_fork(bool vfork, const struct fork_s *context)
|
||||
{
|
||||
struct tcb_s *parent = this_task();
|
||||
|
||||
#ifdef CONFIG_LIB_SYSCALL
|
||||
/* A non-NULL xcp.sregs means a system call is in progress:
|
||||
* x86_64_syscall() publishes the caller's frame there for the duration of
|
||||
* the call and nowhere else. So this was reached from a kernel-side stub,
|
||||
* and the caller to clone is the user task that trapped, not the code that
|
||||
* called into fork.S.
|
||||
*
|
||||
* arm64 and RISC-V discriminate on TCB_FLAG_SYSCALL instead. x86_64
|
||||
* cannot: that flag also defers signal actions, which x86_64 has never
|
||||
* done and which its kernel-build signal path does not currently survive
|
||||
* -- see the note in x86_64_syscall(). xcp.sregs says exactly what is
|
||||
* needed here and means nothing to anyone else.
|
||||
*/
|
||||
|
||||
if (parent->xcp.sregs != NULL)
|
||||
{
|
||||
return x86_64_fork_syscall(vfork, parent);
|
||||
}
|
||||
#endif
|
||||
|
||||
return x86_64_fork_direct(vfork, parent, context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,15 +313,42 @@ uint64_t *x86_64_syscall(uint64_t *regs)
|
|||
#ifdef CONFIG_LIB_SYSCALL
|
||||
int nbr = cmd - CONFIG_SYS_RESERVED;
|
||||
syscall_stub_t stub = (syscall_stub_t)g_stublookup[nbr];
|
||||
struct tcb_s *rtcb = nxsched_self();
|
||||
uint64_t *sregs;
|
||||
|
||||
#ifdef CONFIG_ARCH_KERNEL_STACK
|
||||
struct tcb_s *rtcb = nxsched_self();
|
||||
|
||||
/* Store reference to user RSP for signals */
|
||||
|
||||
rtcb->xcp.saved_ursp = regs[REG_RSP];
|
||||
#endif
|
||||
|
||||
/* Publish the caller's register context. up_fork() has to clone
|
||||
* the caller rather than the stub that is about to invoke it, and
|
||||
* this frame is the only description of it -- see
|
||||
* x86_64_fork_syscall(), which also takes a non-NULL xcp.sregs as
|
||||
* its "reached here through a system call" discriminator.
|
||||
*
|
||||
* It is saved and restored rather than simply set and cleared:
|
||||
* x86_64_syscall_entry() has an explicit path for a nested system
|
||||
* call, and when the inner one returns the outer one must still be
|
||||
* described by its own frame.
|
||||
*
|
||||
* Note what is deliberately *not* done here. arm64 and RISC-V
|
||||
* also raise TCB_FLAG_SYSCALL across the stub call, which defers
|
||||
* any signal action until the system call returns. x86_64 has
|
||||
* never set it, and making it do so is not free: the deferred
|
||||
* action then has to be picked up by nxsig_unmask_pendingsignal()
|
||||
* on the way out, and the signal dispatch path of an x86_64 kernel
|
||||
* build does not survive that today -- it faults in
|
||||
* x86_64_syscall_entry()'s return path with RSP == 0. That is a
|
||||
* pre-existing bug in a configuration nothing has exercised, and
|
||||
* fixing it does not belong to the fork/vfork work; so this
|
||||
* records the frame and changes nothing else.
|
||||
*/
|
||||
|
||||
sregs = rtcb->xcp.sregs;
|
||||
rtcb->xcp.sregs = regs;
|
||||
|
||||
/* Re-enable interrupts if enabled before.
|
||||
* Current task RFLAGS are stored in R11.
|
||||
*/
|
||||
|
|
@ -334,6 +361,10 @@ uint64_t *x86_64_syscall(uint64_t *regs)
|
|||
/* Call syscall function and store return value in RAX register */
|
||||
|
||||
regs[REG_RAX] = stub(nbr, arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
|
||||
/* The system call is now done */
|
||||
|
||||
rtcb->xcp.sregs = sregs;
|
||||
#else
|
||||
svcerr("ERROR: Bad SYS call: %" PRId32 "\n", cmd);
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue