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:
Marco Casaroli 2026-08-09 15:13:39 +02:00 committed by Alan C. Assis
parent 3557791ae2
commit 067e30e14f
5 changed files with 450 additions and 63 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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