diff --git a/include/setjmp.h b/include/setjmp.h new file mode 100644 index 0000000..dd4e820 --- /dev/null +++ b/include/setjmp.h @@ -0,0 +1,117 @@ +#ifndef VLIBC_SETJMP_H +#define VLIBC_SETJMP_H + +/* + * vlibc — . + * + * 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 + +#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 */ diff --git a/src/setjmp/x86_64/longjmp.s b/src/setjmp/x86_64/longjmp.s new file mode 100644 index 0000000..021360c --- /dev/null +++ b/src/setjmp/x86_64/longjmp.s @@ -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 diff --git a/src/setjmp/x86_64/setjmp.s b/src/setjmp/x86_64/setjmp.s new file mode 100644 index 0000000..3407861 --- /dev/null +++ b/src/setjmp/x86_64/setjmp.s @@ -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 diff --git a/src/setjmp/x86_64/siglongjmp.s b/src/setjmp/x86_64/siglongjmp.s new file mode 100644 index 0000000..721ab1e --- /dev/null +++ b/src/setjmp/x86_64/siglongjmp.s @@ -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 diff --git a/src/setjmp/x86_64/sigsetjmp.s b/src/setjmp/x86_64/sigsetjmp.s new file mode 100644 index 0000000..404fd63 --- /dev/null +++ b/src/setjmp/x86_64/sigsetjmp.s @@ -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 diff --git a/tests/test_setjmp.c b/tests/test_setjmp.c new file mode 100644 index 0000000..a8f19ed --- /dev/null +++ b/tests/test_setjmp.c @@ -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 + +#include "../src/internal/syscall.h" + +/* x86_64 signal number (kernel UAPI ): 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 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; +}