feat(setjmp): x86_64 setjmp/longjmp/sigsetjmp

This commit is contained in:
2026-09-03 17:39:20 -04:00
parent dec1017527
commit b2d588885e
6 changed files with 684 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
#ifndef VLIBC_SETJMP_H
#define VLIBC_SETJMP_H
/*
* vlibc — <setjmp.h>.
*
* Non-local jumps. setjmp(env) saves the calling environment and returns 0;
* longjmp(env, val) makes that setjmp return val, with 0 coerced to 1 so the
* two returns are distinguishable. sigsetjmp/siglongjmp additionally save and
* restore the signal mask when the buffer's saved-mask flag is set.
*
* jmp_buf is an array type (C23 7.13.1 requires it); both types below are
* arrays of unsigned long. The word layout is ABI: the x86_64 assembly in
* src/setjmp/x86_64/{setjmp,longjmp,sigsetjmp,siglongjmp}.s reads and writes
* exactly these slots. This comment is the single authoritative map — keep it
* and those files in sync:
*
* [0] rbx callee-saved general registers (System V AMD64 ABI)
* [1] rbp
* [2] r12
* [3] r13
* [4] r14
* [5] r15
* [6] rsp the caller's stack pointer (just past the return address)
* [7] rip the return address, where longjmp resumes execution
* [8] mxcsr SSE control/status register (stmxcsr/ldmxcsr)
* [9] x87 cw x87 control word (fnstcw/fldcw)
*
* sigjmp_buf extends the base layout with two more words:
*
* [10] saved-mask flag (1 when the signal mask was saved)
* [11] signal mask (the x86_64 Linux sigset_t: a single 64-bit word)
*
* Because the base layout occupies words 0..9 of both types, the plain
* setjmp/longjmp assembly works verbatim on sigjmp_buf storage as well.
*/
#include <vlibc/features.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Word counts and per-slot indices (see the authoritative map above). */
#define VLIBC_JMP_BUF_WORDS 10
#define VLIBC_JMP_RBX 0
#define VLIBC_JMP_RBP 1
#define VLIBC_JMP_R12 2
#define VLIBC_JMP_R13 3
#define VLIBC_JMP_R14 4
#define VLIBC_JMP_R15 5
#define VLIBC_JMP_RSP 6
#define VLIBC_JMP_RIP 7
#define VLIBC_JMP_MXCSR 8
#define VLIBC_JMP_X87CW 9
#define VLIBC_SIGJMP_BUF_WORDS 12
#define VLIBC_SIGJMP_FLAG 10
#define VLIBC_SIGJMP_MASK 11
typedef unsigned long jmp_buf[VLIBC_JMP_BUF_WORDS];
typedef unsigned long sigjmp_buf[VLIBC_SIGJMP_BUF_WORDS];
/*
* Save the calling environment and return 0; after longjmp(env, val) the
* matching setjmp invocation returns val. A macro (C23 7.13.1: usable without
* a prototype in scope); it expands to _setjmp(env).
*/
#define setjmp(env) _setjmp(env)
/*
* Like setjmp, plus the current signal mask is saved into env when savemask is
* nonzero (siglongjmp then restores it). Macro expanding to
* __sigsetjmp(env, savemask).
*/
#define sigsetjmp(env, savemask) __sigsetjmp(env, savemask)
/*
* The save behind the setjmp macro; identical semantics (POSIX _setjmp).
*/
int
_setjmp(jmp_buf env); // NOLINT(bugprone-reserved-identifier)
/*
* Restore the environment saved by _setjmp/setjmp and make it return val, with
* 0 coerced to 1. Does not return.
*/
__attribute__((noreturn)) void
_longjmp(jmp_buf env, int val); // NOLINT(bugprone-reserved-identifier)
/*
* ISO C longjmp: same as _longjmp.
*/
__attribute__((noreturn)) void
longjmp(jmp_buf env, int val);
/*
* The save behind the sigsetjmp macro. Implementation-reserved name, so the
* public API keeps only the macro; declared because the macro expansion must
* have a prototype in scope (C23 removed implicit declarations).
*/
int
__sigsetjmp(sigjmp_buf env, int savemask); // NOLINT(bugprone-reserved-identifier)
/*
* Restore the environment saved by sigsetjmp and make it return val, with 0
* coerced to 1; when the buffer's saved-mask flag is set, the signal mask is
* restored first. Does not return.
*/
__attribute__((noreturn)) void
siglongjmp(sigjmp_buf env, int val);
#ifdef __cplusplus
}
#endif
#endif /* VLIBC_SETJMP_H */
+48
View File
@@ -0,0 +1,48 @@
/*
* vlibc — x86_64 longjmp (todo 4).
*
* longjmp(env, val) / _longjmp(env, val): restore the environment saved by
* setjmp and make that setjmp return val — with 0 coerced to 1 (POSIX:
* longjmp(env, 0) must return 1 so it is distinguishable from the initial
* setjmp return of 0). Never returns.
*
* Restores the FP control state (MXCSR and x87 control word), the
* callee-saved registers, then jumps to the saved rip with the saved rsp,
* exactly as if setjmp had returned val.
*
* Registers: env arrives in rdi and is moved to r12, a callee-saved scratch
* register that is itself restored from the buffer, so it can serve as the
* base for every load. val rides in rax, which is never restored from the
* buffer, and is therefore the value setjmp observes. The saved rsp and rip
* travel through rdx and rcx (both caller-saved, safe to clobber) until the
* final `mov %rdx,%rsp; jmp *%rcx`.
*/
.global longjmp
.global _longjmp
.type longjmp,@function
.type _longjmp,@function
longjmp:
_longjmp:
mov %rsi,%rax /* val; coerce 0 to 1 below */
test %rax,%rax
jnz 1f
inc %rax
1:
mov %rdi,%r12 /* env base (r12 is restored from the buffer later) */
ldmxcsr 64(%r12)
fldcw 72(%r12)
mov 0(%r12),%rbx
mov 8(%r12),%rbp
mov 32(%r12),%r14
mov 40(%r12),%r15
mov 48(%r12),%rdx /* saved rsp */
mov 56(%r12),%rcx /* saved rip */
mov 24(%r12),%r13
mov 16(%r12),%r12 /* last read of env */
mov %rdx,%rsp
jmp *%rcx
.size longjmp,.-longjmp
.size _longjmp,.-_longjmp
.section .note.GNU-stack,"",@progbits
+49
View File
@@ -0,0 +1,49 @@
/*
* vlibc — x86_64 setjmp (todo 4).
*
* setjmp(env) / _setjmp(env): save the calling environment into env and
* return 0. Both entry points share one body; the setjmp macro in
* include/setjmp.h expands to _setjmp, while the plain symbol is kept so
* address-takers and #undef-users still link.
*
* jmp_buf slot map (the authoritative layout; see include/setjmp.h):
*
* [0] rbx [1] rbp [2] r12 [3] r13 [4] r14
* [5] r15 [6] rsp [7] rip [8] mxcsr [9] x87 control word
*
* The saved rsp is the caller's stack pointer (rsp + 8, past the return
* address pushed by the call); the saved rip is that return address, so
* longjmp can resume exactly as if setjmp had returned. The FP control
* state (MXCSR and the x87 control word) is saved as well, so a longjmp out
* of code that changed rounding/trap state restores it — this must be
* assembly: C cannot access the register file.
*
* Only the caller's callee-saved registers are preserved by this function
* itself (rbx is saved into the buffer before any use); caller-saved
* registers are untouched, and the return value 0 is delivered in eax.
*/
.global setjmp
.global _setjmp
.type setjmp,@function
.type _setjmp,@function
setjmp:
_setjmp:
mov %rbx,0(%rdi)
mov %rbp,8(%rdi)
mov %r12,16(%rdi)
mov %r13,24(%rdi)
mov %r14,32(%rdi)
mov %r15,40(%rdi)
lea 8(%rsp),%rdx
mov %rdx,48(%rdi)
mov (%rsp),%rax
mov %rax,56(%rdi)
stmxcsr 64(%rdi)
fnstcw 72(%rdi)
xor %eax,%eax
ret
.size setjmp,.-setjmp
.size _setjmp,.-_setjmp
.section .note.GNU-stack,"",@progbits
+55
View File
@@ -0,0 +1,55 @@
/*
* vlibc — x86_64 siglongjmp (todo 4).
*
* siglongjmp(env, val): like longjmp, but when the buffer's saved-mask flag
* (word 10, see include/setjmp.h) is set, first restore the signal mask
* stored at word 11 with rt_sigprocmask(SIG_SETMASK). Never returns.
*
* The syscall is made inline here: siglongjmp is naked assembly that must
* not call into C while the register set is half-restored. The kernel ABI
* facts hardcoded below mirror src/internal/syscall.h:
*
* SYS_rt_sigprocmask = 14, SIG_SETMASK = 2, sigsetsize = 8
* (x86_64 Linux sigset_t is a single 64-bit word)
*
* Register discipline matches longjmp.s: env moves to r12 (callee-saved
* scratch, restored from the buffer last), val rides in rax — stashed in
* r13 across the syscall, which clobbers rax — and the saved rsp/rip travel
* through rdx/rcx until the final `mov %rdx,%rsp; jmp *%rcx`.
*/
.global siglongjmp
.type siglongjmp,@function
siglongjmp:
mov %rsi,%rax /* val; coerce 0 to 1 below */
test %rax,%rax
jnz 1f
inc %rax
1:
mov %rdi,%r12 /* env base (r12 is restored from the buffer later) */
cmpl $0,80(%r12) /* saved-mask flag */
jz 2f
mov %rax,%r13 /* val must survive the syscall */
mov $14,%eax /* SYS_rt_sigprocmask */
mov $2,%edi /* SIG_SETMASK */
lea 88(%r12),%rsi /* set = &env[11] */
xor %edx,%edx /* oldset = NULL */
mov $8,%r10d /* sigsetsize (x86_64 sigset_t = one word) */
syscall
mov %r13,%rax /* val back in rax */
2:
ldmxcsr 64(%r12)
fldcw 72(%r12)
mov 0(%r12),%rbx
mov 8(%r12),%rbp
mov 32(%r12),%r14
mov 40(%r12),%r15
mov 48(%r12),%rdx /* saved rsp */
mov 56(%r12),%rcx /* saved rip */
mov 24(%r12),%r13
mov 16(%r12),%r12 /* last read of env */
mov %rdx,%rsp
jmp *%rcx
.size siglongjmp,.-siglongjmp
.section .note.GNU-stack,"",@progbits
+68
View File
@@ -0,0 +1,68 @@
/*
* vlibc — x86_64 sigsetjmp (todo 4).
*
* __sigsetjmp(env, savemask) / sigsetjmp(env, savemask): like setjmp, plus
* the current signal mask is saved into env when savemask is nonzero. Both
* entry points share one body; the sigsetjmp macro in include/setjmp.h
* expands to __sigsetjmp, while the plain symbol is kept so address-takers
* and #undef-users still link.
*
* Slot map (authoritative; see include/setjmp.h): a sigjmp_buf holds the
* base jmp_buf layout at words 0..9 (same offsets as plain setjmp, so the
* shared longjmp restore logic works on either storage), plus:
*
* [10] saved-mask flag (1 when the signal mask was saved)
* [11] signal mask (the x86_64 Linux sigset_t: a single 64-bit word)
*
* The saved-mask flag is cleared before anything else, so a buffer saved
* with savemask == 0 — or jumped to by plain longjmp — never presents a
* stale flag to siglongjmp. When savemask is nonzero, the mask is queried
* with a raw rt_sigprocmask syscall made inline right here: this function
* must stay pure assembly, because the saved rip/rsp resume DIRECTLY in the
* caller (an intermediate C frame would have its return-address slot above
* the saved stack pointer, where intervening calls would clobber it before
* the longjmp). The kernel ABI facts below mirror src/internal/syscall.h:
*
* SYS_rt_sigprocmask = 14, SIG_BLOCK = 0 (NULL newset queries),
* sigsetsize = 8 (x86_64 sigset_t is one 64-bit word)
*
* The mask slot is written through &env[11]; on success the flag is set and
* 0 is returned in eax, exactly like plain setjmp.
*/
.global __sigsetjmp
.global sigsetjmp
.type __sigsetjmp,@function
.type sigsetjmp,@function
__sigsetjmp:
sigsetjmp:
movq $0,80(%rdi) /* clear the saved-mask flag */
mov %rbx,0(%rdi)
mov %rbp,8(%rdi)
mov %r12,16(%rdi)
mov %r13,24(%rdi)
mov %r14,32(%rdi)
mov %r15,40(%rdi)
lea 8(%rsp),%rdx
mov %rdx,48(%rdi)
mov (%rsp),%rax
mov %rax,56(%rdi)
stmxcsr 64(%rdi)
fnstcw 72(%rdi)
test %esi,%esi
jz 1f
mov %rdi,%rbx /* env base (rbx is restored from the buffer later) */
mov $14,%eax /* SYS_rt_sigprocmask */
xor %edi,%edi /* how = SIG_BLOCK(0) */
xor %esi,%esi /* set = NULL (query only) */
lea 88(%rbx),%rdx /* oldset = &env[11] */
mov $8,%r10d /* sigsetsize (x86_64 sigset_t = one word) */
syscall
movq $1,80(%rbx) /* mask saved: set the flag */
1:
xor %eax,%eax
ret
.size __sigsetjmp,.-__sigsetjmp
.size sigsetjmp,.-sigsetjmp
.section .note.GNU-stack,"",@progbits
+347
View File
@@ -0,0 +1,347 @@
/*
* vlibc — setjmp/longjmp + sigsetjmp/siglongjmp test (todo 4).
*
* Exercises the non-local jump implementation end to end:
*
* 1. setjmp returns 0 on the initial call and 42 after longjmp(env, 42);
* 2. longjmp(env, 0) makes setjmp return 1, not 0 (POSIX coercion);
* 3. a volatile local survives a jump across a real call boundary;
* 4. FP control state (MXCSR and the x87 control word) round-trips:
* changed at the jump site, must come back as saved;
* 5. sigsetjmp(env, 1) captures a blocked SIGUSR1 mask that siglongjmp
* restores; sigsetjmp(env2, 0) must NOT restore it (flag logic);
* 6. _setjmp/_longjmp basic round-trip.
*
* The signal mask is manipulated with raw SYS_rt_sigprocmask (no signal.h:
* that header belongs to a later todo), and all diagnostics go through raw
* SYS_write (no stdio: this test must not depend on host libc headers, which
* the -Iinclude search path could otherwise shadow). With `-f`, only the
* POSIX coercion failure scenario runs: longjmp(env, 0) must return 1 — a 0
* return is the defect.
*
* Not part of the library proper; compiled manually for this todo (the
* tests/ + make check wiring is owned by a later todo).
*/
#include <setjmp.h>
#include "../src/internal/syscall.h"
/* x86_64 signal number (kernel UAPI <asm/signal.h>): SIGUSR1 = 10. */
#define TEST_SIGUSR1 10
/* rt_sigprocmask how codes (kernel UAPI). */
#define TEST_SIG_BLOCK 0
#define TEST_SIG_UNBLOCK 1
#define TEST_SIG_SETMASK 2
/* x86_64 Linux sigset_t is a single 64-bit word. */
#define TEST_SIGSETSIZE 8
static int failures;
/*
* rt_sigprocmask with the kernel argument registers pinned by hand. The
* internal __syscall4() wrapper hands its 4th argument to the compiler as a
* generic "r" operand, which the compiler may place in r8 — but the x86_64
* syscall ABI delivers the 4th argument in r10, so the kernel then sees
* garbage in sigsetsize and rejects the call. Pin r10 here (the same
* technique src/internal/syscall.h uses for __syscall6); this test is
* self-contained and must be deterministic regardless of register
* allocation.
*/
static long
test_rt_sigprocmask(long how, const unsigned long *set,
unsigned long *oldset) // NOLINT(readability-non-const-parameter)
{
register long rdx __asm__("rdx") = (long)oldset;
register long rsi __asm__("rsi") = (long)set;
register long rdi __asm__("rdi") = how;
register long r10 __asm__("r10") = TEST_SIGSETSIZE;
register long rax __asm__("rax") = SYS_rt_sigprocmask;
__asm__ volatile("syscall"
: "+a"(rax)
: "r"(rdi), "r"(rsi), "r"(rdx), "r"(r10)
: "rcx", "r11", "memory");
return rax;
}
/* Write a NUL-terminated string to fd via the raw syscall layer. */
static void
say(int fd, const char *s)
{
long n = 0;
while (s[n] != '\0')
{
n++;
}
__syscall3(SYS_write, fd, (long)s, n);
}
/* Write v in decimal to fd. */
static void
say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters)
{
char buf[24];
int i = (int)sizeof(buf);
buf[--i] = '\0';
do
{
buf[--i] = (char)('0' + (v % 10));
v /= 10;
} while (v != 0);
__syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i));
}
static void
check(int cond, const char *what)
{
if (cond)
{
say(1, "PASS: ");
say(1, what);
say(1, "\n");
}
else
{
say(2, "FAIL: ");
say(2, what);
say(2, "\n");
failures++;
}
}
/* Raw FP control-state access (the public <fenv.h> owns this later). */
static unsigned
read_mxcsr(void)
{
unsigned x;
__asm__ volatile("stmxcsr %0" : "=m"(x));
return x;
}
static void
write_mxcsr(unsigned x)
{
__asm__ volatile("ldmxcsr %0" : : "m"(x));
}
static unsigned short
read_x87_cw(void)
{
unsigned short c;
__asm__ volatile("fnstcw %0" : "=m"(c));
return c;
}
static void
write_x87_cw(unsigned short c)
{
__asm__ volatile("fldcw %0" : : "m"(c));
}
/* Query the current signal mask via raw rt_sigprocmask (SIG_BLOCK + NULL). */
static unsigned long
query_mask(void)
{
unsigned long old = 0;
test_rt_sigprocmask(TEST_SIG_BLOCK, 0, &old);
return old;
}
/*
* Must stay a real function: the volatile-local test needs the longjmp to
* cross an actual call boundary, not an inlined body.
*/
static __attribute__((noinline)) void
jump_out(jmp_buf env, int val)
{
longjmp(env, val);
}
/* 1. Initial return 0, then longjmp(env, 42) -> 42. */
static void
basic_roundtrip(void)
{
jmp_buf env;
int r = setjmp(env);
if (r == 0)
{
longjmp(env, 42);
check(0, "longjmp returned to its caller (noreturn violated)");
return;
}
check(r == 42, "longjmp(env,42) -> setjmp returned 42");
}
/* 2. longjmp(env, 0) -> setjmp returns 1, never 0. */
static void
coercion_test(void)
{
jmp_buf env;
int r = setjmp(env);
if (r == 0)
{
longjmp(env, 0);
check(0, "longjmp returned to its caller (noreturn violated)");
return;
}
check(r == 1, "longjmp(env,0) -> setjmp returned 1, not 0 (POSIX coercion)");
}
/* 3. Volatile locals survive a jump across a call boundary. */
static void
volatile_local_test(void)
{
jmp_buf env;
volatile int local = 1;
int r = setjmp(env);
if (r == 0)
{
local = 99;
jump_out(env, 5);
check(0, "longjmp returned to its caller (noreturn violated)");
return;
}
check(r == 5 && local == 99, "volatile local survives longjmp across a call boundary");
}
/* 4. FP control state round-trips through setjmp/longjmp. */
static void
fp_state_test(void)
{
jmp_buf env;
unsigned mx = read_mxcsr();
unsigned short cw = read_x87_cw();
int r = setjmp(env);
if (r == 0)
{
/* Toggle the MXCSR rounding-control bits and the x87 PC+RC bits. */
write_mxcsr(mx ^ 0x6000U);
write_x87_cw((unsigned short)(cw ^ 0x0f00U));
longjmp(env, 3);
check(0, "longjmp returned to its caller (noreturn violated)");
return;
}
check(r == 3, "FP-state test: longjmp(env,3) -> setjmp returned 3");
check(read_mxcsr() == mx, "mxcsr restored by longjmp");
check(read_x87_cw() == cw, "x87 control word restored by longjmp");
}
/* 5. sigsetjmp/siglongjmp round-trip a blocked mask; savemask=0 skips it. */
static void
sigmask_test(void)
{
sigjmp_buf env;
sigjmp_buf env2;
unsigned long block = 1UL << (TEST_SIGUSR1 - 1);
unsigned long orig = query_mask();
int r;
/* Block SIGUSR1 (raw rt_sigprocmask), then save the masked state. */
test_rt_sigprocmask(TEST_SIG_BLOCK, &block, 0);
r = sigsetjmp(env, 1);
if (r == 0)
{
/* Drop the block; siglongjmp must restore it. */
test_rt_sigprocmask(TEST_SIG_UNBLOCK, &block, 0);
siglongjmp(env, 7);
check(0, "siglongjmp returned to its caller (noreturn violated)");
return;
}
check(r == 7, "siglongjmp(env,7) -> sigsetjmp returned 7");
check((query_mask() & block) != 0, "siglongjmp restored the blocked SIGUSR1 mask");
/* Negative: savemask == 0 -> the mask must NOT be restored. */
r = sigsetjmp(env2, 0);
if (r == 0)
{
test_rt_sigprocmask(TEST_SIG_UNBLOCK, &block, 0);
siglongjmp(env2, 8);
check(0, "siglongjmp returned to its caller (noreturn violated)");
return;
}
check(r == 8, "siglongjmp(env2,8) -> sigsetjmp returned 8");
check((query_mask() & block) == 0, "sigsetjmp(env2,0): mask NOT restored (flag logic)");
/* Leave the process mask as it was found. */
test_rt_sigprocmask(TEST_SIG_SETMASK, &orig, 0);
}
/* 6. _setjmp/_longjmp basic round-trip. */
static void
underscore_forms_test(void)
{
jmp_buf env;
int r = _setjmp(env);
if (r == 0)
{
_longjmp(env, 9);
check(0, "_longjmp returned to its caller (noreturn violated)");
return;
}
check(r == 9, "_longjmp(env,9) -> _setjmp returned 9");
}
/*
* Failure scenario (-f): longjmp(env, 0) must make setjmp return 1. Exits 0
* only when the coercion behaved exactly as POSIX specifies.
*/
static int
failure_scenario(void)
{
jmp_buf env;
int r = setjmp(env);
if (r == 0)
{
longjmp(env, 0);
return 1; /* longjmp returned: noreturn violated */
}
if (r == 1)
{
say(1, "longjmp(env,0) -> setjmp returned 1 (a 0 return would be the defect)\n");
return 0;
}
say(1, "longjmp(env,0) -> setjmp returned ");
say_dec(1, (unsigned long)r);
say(1, ", want 1\n");
return 1;
}
int
main(int argc, char **argv)
{
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
{
return failure_scenario();
}
basic_roundtrip();
coercion_test();
volatile_local_test();
fp_state_test();
sigmask_test();
underscore_forms_test();
if (failures > 0)
{
say(2, "FAILED (");
say_dec(2, (unsigned long)failures);
say(2, " check(s))\n");
return 1;
}
say(1, "all setjmp tests passed\n");
return 0;
}