From 4caeaf6b7e099a281aff72136c8123c4841b8196 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Thu, 4 Dec 2025 05:13:44 +0000 Subject: [PATCH] WIP: Try using preserve_none for setjmp The `preserve_none` calling convention is a new calling convention in clang (>= 19) and gcc that preserves a more minimal set of registers (rsp, rbp on x86_64; lr, fp on aarch64). As a result, if this calling convention is used with setjmp, those registers do not need to be stored in the setjmp buffer, allowing us to reduce the size of this buffer and use fewer instructions to save the buffer. The tradeoff of course is that these registers may need to be saved anyway, in which case both the stack usage and the instructions just move to the caller (which is strictly worse). It is not clear that this is useful for exceptions (which already have a fair bit of state anyway, so even in the happy path the savings are not necessarily that big), but I am thinking about using it for #60281, which has different characteristics, so this is an easy way to try out whether there are any unexpected challenges. Note that preserve_none is a very recent compiler feature, so most compilers out there do not have it yet. For compatibility, this PR supports using different jump buffer formats in the runtime and the generated code. --- src/Makefile | 19 +++++++++ src/_jlsetjmp.S | 67 +++++++++++++++++++++++++++++++ src/codegen.cpp | 54 ++++++++++++++++++++++--- src/interpreter.c | 20 +++++----- src/jl_exported_funcs.inc | 3 ++ src/julia.h | 83 ++++++++++++++++++++++++++++++++++++--- src/julia_internal.h | 1 + src/rtutils.c | 17 ++++++++ src/signals-unix.c | 20 +++++++++- src/stackwalk.c | 14 +++++++ src/task.c | 16 +++++++- 11 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 src/_jlsetjmp.S diff --git a/src/Makefile b/src/Makefile index 832f0b0ae71e4..b77627d8e5b85 100644 --- a/src/Makefile +++ b/src/Makefile @@ -112,6 +112,14 @@ ifeq ($(OS),WINNT) SRCS += win32_ucontext endif +# Assembly sources for minimal setjmp (x86_64 Linux only for now) +ASM_SRCS := +ifeq ($(OS),Linux) +ifeq ($(ARCH),x86_64) +ASM_SRCS += _jlsetjmp +endif +endif + ifeq ($(WITH_DTRACE),1) DTRACE_HEADERS := uprobes.h.gen ifneq ($(OS),Darwin) @@ -229,6 +237,12 @@ CG_RELEASE_LIBS := $(COMMON_LIBPATHS) $(CG_LIBS) -ljulia -ljulia-internal OBJS := $(SRCS:%=$(BUILDDIR)/%.o) DOBJS := $(SRCS:%=$(BUILDDIR)/%.dbg.obj) +# Assembly object files +ASM_OBJS := $(ASM_SRCS:%=$(BUILDDIR)/%.o) +ASM_DOBJS := $(ASM_SRCS:%=$(BUILDDIR)/%.dbg.obj) +OBJS += $(ASM_OBJS) +DOBJS += $(ASM_DOBJS) + CODEGEN_OBJS := $(CODEGEN_SRCS:%=$(BUILDDIR)/%.o) CODEGEN_DOBJS := $(CODEGEN_SRCS:%=$(BUILDDIR)/%.dbg.obj) @@ -313,6 +327,11 @@ $(BUILDDIR)/%.o: $(SRCDIR)/%.cpp $(SRCDIR)/llvm-version.h $(HEADERS) $(LLVM_CONF @$(call PRINT_CC, $(CXX) $(LLVM_CXXFLAGS) $(SHIPFLAGS) $(JCPPFLAGS) $(JCXXFLAGS) $(CXX_DISABLE_ASSERTION) -c $< -o $@) $(BUILDDIR)/%.dbg.obj: $(SRCDIR)/%.cpp $(SRCDIR)/llvm-version.h $(HEADERS) $(LLVM_CONFIG_ABSOLUTE) | $(BUILDDIR) @$(call PRINT_CC, $(CXX) $(LLVM_CXXFLAGS) $(DEBUGFLAGS) $(JCPPFLAGS) $(JCXXFLAGS) -c $< -o $@) +# Assembly source file rules +$(BUILDDIR)/%.o: $(SRCDIR)/%.S | $(BUILDDIR) + @$(call PRINT_CC, $(CC) $(JCPPFLAGS) $(SHIPFLAGS) -c $< -o $@) +$(BUILDDIR)/%.dbg.obj: $(SRCDIR)/%.S | $(BUILDDIR) + @$(call PRINT_CC, $(CC) $(JCPPFLAGS) $(DEBUGFLAGS) -c $< -o $@) $(BUILDDIR)/%.o : $(SRCDIR)/%.d @$(call PRINT_DTRACE, $(DTRACE) -G -s $< -o $@) $(BUILDDIR)/%.dbg.obj : $(SRCDIR)/%.d diff --git a/src/_jlsetjmp.S b/src/_jlsetjmp.S new file mode 100644 index 0000000000000..88698f7e80e85 --- /dev/null +++ b/src/_jlsetjmp.S @@ -0,0 +1,67 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +// Minimal setjmp/longjmp implementation for x86_64 Linux +// These are designed to work with the preserve_none calling convention, +// where all registers are caller-saved. This means we only need to save +// the stack pointer - the compiler will spill all live registers before +// calling setjmp. +// +// IMPORTANT: These functions ONLY work correctly when called with the +// preserve_none calling convention. Without preserve_none, callee-saved +// registers won't be restored properly after longjmp. + +#if defined(__x86_64__) && defined(__linux__) + +// Mark stack as non-executable +.section .note.GNU-stack,"",@progbits + +.text + +// ============================================================================ +// jl_minimal_setjmp / ijl_minimal_setjmp +// ============================================================================ +.p2align 4,0x90 +.globl ijl_minimal_setjmp +.globl jl_minimal_setjmp +.type ijl_minimal_setjmp,@function +.type jl_minimal_setjmp,@function +ijl_minimal_setjmp: +jl_minimal_setjmp: + // N.B: In preserve_none ABI, r12 is the first argument. + // r12 = pointer to buffer (three pointer-sized slots: rbp, rsp, rip) + // Save RSP as it was *before* the call instruction pushed the return address. + lea 8(%rsp), %rax // rax = original RSP (before call) + mov %rbp, 0(%r12) // save RBP to buffer[0] + mov %rax, 8(%r12) // save RSP to buffer[1] + mov (%rsp), %rax // rax = return address + mov %rax, 16(%r12) // save return address to buffer[2] + xor %eax, %eax // return 0 (setjmp returns 0 on first call) + ret +.size ijl_minimal_setjmp, . - ijl_minimal_setjmp +.size jl_minimal_setjmp, . - jl_minimal_setjmp + + +// ============================================================================ +// jl_minimal_longjmp / ijl_minimal_longjmp +// ============================================================================ +.p2align 4,0x90 +.globl ijl_minimal_longjmp +.globl jl_minimal_longjmp +.type ijl_minimal_longjmp,@function +.type jl_minimal_longjmp,@function +ijl_minimal_longjmp: +jl_minimal_longjmp: + // rdi = pointer to buffer (two pointer-sized slots: rsp, rip) + // esi = return value (passed to setjmp caller) + mov %esi, %eax // set return value + test %eax, %eax // longjmp must return non-zero + jne 1f + inc %eax // if val was 0, return 1 +1: + mov 0(%rdi), %rbp // restore RBP + mov 8(%rdi), %rsp // restore RSP (to pre-call value) + jmp *16(%rdi) // jump to saved return address +.size ijl_minimal_longjmp, . - ijl_minimal_longjmp +.size jl_minimal_longjmp, . - jl_minimal_longjmp + +#endif diff --git a/src/codegen.cpp b/src/codegen.cpp index ed427f999e8e3..a37e920f1e239 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -1033,6 +1033,14 @@ static const auto jlenter_func = new JuliaFunction<>{ {T_pjlvalue, getPointerTy(C)}, false); }, nullptr, }; +static const auto jlentermin_func = new JuliaFunction<>{ + XSTR(jl_enter_min_handler), + [](LLVMContext &C) { + auto T_pjlvalue = JuliaType::get_pjlvalue_ty(C); + return FunctionType::get(getVoidTy(C), + {T_pjlvalue, getPointerTy(C)}, false); }, + nullptr, +}; static const auto jl_current_exception_func = new JuliaFunction<>{ XSTR(jl_current_exception), [](LLVMContext &C) { return FunctionType::get(JuliaType::get_prjlvalue_ty(C), {JuliaType::get_pjlvalue_ty(C)}, false); }, @@ -1239,6 +1247,17 @@ static const auto jl_object_id__func = new JuliaFunction{ {T_size, PointerType::get(C, AddressSpace::Derived)}, false); }, nullptr, }; +static const auto setjmp_min_func = new JuliaFunction{ + XSTR(jl_minimal_setjmp), + [](LLVMContext &C, const Triple &T) { + return FunctionType::get(getInt32Ty(C), + {getPointerTy(C)}, false); + }, + [](LLVMContext &C) { return AttributeList::get(C, + Attributes(C, {Attribute::ReturnsTwice}), + AttributeSet(), + None); }, +}; static const auto setjmp_func = new JuliaFunction{ jl_setjmp_name, [](LLVMContext &C, const Triple &T) { @@ -6151,8 +6170,13 @@ static void emit_stmtpos(jl_codectx_t &ctx, jl_value_t *expr, int ssaval_result) } } ctx.builder.CreateCall(prepare_call(jlleave_noexcept_func), {get_current_task(ctx), ConstantInt::get(getInt32Ty(ctx.builder.getContext()), handler_to_end.size())}); +#ifdef JL_HAVE_MIN_SETJMP + auto *handler_sz64 = ConstantInt::get(Type::getInt64Ty(ctx.builder.getContext()), + sizeof(struct _jl_handler_min_setjmp)); +#else auto *handler_sz64 = ConstantInt::get(Type::getInt64Ty(ctx.builder.getContext()), - sizeof(jl_handler_t)); + sizeof(struct _jl_handler_setjmp)); +#endif for (AllocaInst *handler : handler_to_end) { ctx.builder.CreateLifetimeEnd(handler, handler_sz64); } @@ -9355,17 +9379,31 @@ static jl_llvm_functions_t ctx.ssavalue_assigned[cursor] = true; // Actually enter the exception frame auto ct = get_current_task(ctx); +#if JL_HAVE_MIN_SETJMP + auto *handler_sz64 = ConstantInt::get(Type::getInt64Ty(ctx.builder.getContext()), + sizeof(struct _jl_handler_min_setjmp)); + AllocaInst* ehbuff = emit_static_alloca(ctx, sizeof(struct _jl_handler_min_setjmp), Align(16)); +#else auto *handler_sz64 = ConstantInt::get(Type::getInt64Ty(ctx.builder.getContext()), - sizeof(jl_handler_t)); - AllocaInst* ehbuff = emit_static_alloca(ctx, sizeof(jl_handler_t), Align(16)); + sizeof(struct _jl_handler_setjmp)); + AllocaInst* ehbuff = emit_static_alloca(ctx, sizeof(struct _jl_handler_setjmp), Align(16)); +#endif ctx.eh_buffers[stmt] = ehbuff; ctx.builder.CreateLifetimeStart(ehbuff, handler_sz64); - ctx.builder.CreateCall(prepare_call(jlenter_func), {ct, ehbuff}); CallInst *sj; +#if JL_HAVE_MIN_SETJMP + ctx.builder.CreateCall(prepare_call(jlentermin_func), {ct, ehbuff}); + Value *jmpbuf = emit_ptrgep(ctx, ehbuff, offsetof(struct _jl_handler_min_setjmp, min_eh_ctx)); + sj = ctx.builder.CreateCall(prepare_call(setjmp_min_func), {jmpbuf}); + sj->setCallingConv(CallingConv::PreserveNone); +#else + Value *jmpbuf = emit_ptrgep(ctx, ehbuff, offsetof(struct _jl_handler_setjmp, eh_ctx)); + ctx.builder.CreateCall(prepare_call(jlenter_func), {ct, ehbuff}); if (ctx.emission_context.TargetTriple.isOSWindows()) - sj = ctx.builder.CreateCall(prepare_call(setjmp_func), {ehbuff}); + sj = ctx.builder.CreateCall(prepare_call(setjmp_func), {jmpbuf}); else - sj = ctx.builder.CreateCall(prepare_call(setjmp_func), {ehbuff, ConstantInt::get(Type::getInt32Ty(ctx.builder.getContext()), 0)}); + sj = ctx.builder.CreateCall(prepare_call(setjmp_func), {jmpbuf, ConstantInt::get(Type::getInt32Ty(ctx.builder.getContext()), 0)}); +#endif // We need to mark this on the call site as well. See issue #6757 sj->setCanReturnTwice(); Value *isz = ctx.builder.CreateICmpEQ(sj, ConstantInt::get(getInt32Ty(ctx.builder.getContext()), 0)); @@ -9994,6 +10032,9 @@ static void init_jit_functions(void) add_named_global(jlnew_func, &jl_new_structv); add_named_global(jlsplatnew_func, &jl_new_structt); add_named_global(setjmp_func, &jl_setjmp_f); +#if JL_HAVE_MIN_SETJMP + add_named_global(setjmp_min_func, &jl_minimal_setjmp); +#endif add_named_global(memcmp_func, &memcmp); add_named_global(jltypeerror_func, &jl_type_error); add_named_global(jlcheckassign_func, &jl_checked_assignment); @@ -10009,6 +10050,7 @@ static void init_jit_functions(void) add_named_global(jlmethod_func, &jl_method_def); add_named_global(jlgenericfunction_func, &jl_declare_const_gf); add_named_global(jlenter_func, &jl_enter_handler); + add_named_global(jlentermin_func, &jl_enter_min_handler); add_named_global(jl_current_exception_func, &jl_current_exception); add_named_global(jlleave_noexcept_func, &jl_pop_handler_noexcept); add_named_global(jlleave_func, &jl_pop_handler); diff --git a/src/interpreter.c b/src/interpreter.c index 6c0ea1bc16ab1..e862c2729c92a 100644 --- a/src/interpreter.c +++ b/src/interpreter.c @@ -469,7 +469,7 @@ static size_t eval_phi(jl_array_t *stmts, interpreter_state *s, size_t ns, size_ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip, int toplevel) { - jl_handler_t __eh; + jl_handler_preferred_t __eh; size_t ns = jl_array_nrows(stmts); jl_task_t *ct = jl_current_task; @@ -506,7 +506,7 @@ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip, s->locals[jl_source_nslots(s->src) + id] = val; } else if (jl_is_enternode(stmt)) { - jl_enter_handler(ct, &__eh); + jl_enter_handler(ct, &__eh._handler); // This is a bit tricky, but supports the implementation of PhiC nodes. // They are conceptually slots, but the slot to store to doesn't get explicitly // mentioned in the store (aka the "UpsilonNode") (this makes them integrate more @@ -545,29 +545,29 @@ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip, // replaced later JL_GC_PUSH1(&scope); ct->scope = scope; - if (!jl_setjmp(__eh.eh_ctx, 0)) { - ct->eh = &__eh; + if (!JL_EH_SETJMP(__eh)) { + ct->eh = &__eh._handler; eval_body(stmts, s, next_ip, toplevel); jl_unreachable(); } JL_GC_POP(); } else { - if (!jl_setjmp(__eh.eh_ctx, 0)) { - ct->eh = &__eh; + if (!JL_EH_SETJMP(__eh)) { + ct->eh = &__eh._handler; eval_body(stmts, s, next_ip, toplevel); jl_unreachable(); } } if (s->continue_at) { // means we reached a :leave expression - jl_eh_restore_state_noexcept(ct, &__eh); + jl_eh_restore_state_noexcept(ct, &__eh._handler); ip = s->continue_at; s->continue_at = 0; continue; } else { // a real exception - jl_eh_restore_state(ct, &__eh); + jl_eh_restore_state(ct, &__eh._handler); ip = catch_ip; assert(jl_enternode_catch_dest(stmt) != 0); continue; @@ -617,8 +617,8 @@ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip, // leave happens during normal control flow, but we must // longjmp to pop the eval_body call for each enter. s->continue_at = next_ip; - asan_unpoison_task_stack(ct, &eh->eh_ctx); - jl_longjmp(eh->eh_ctx, 1); + asan_unpoison_eh_task_stack(ct, eh); + jl_eh_longjmp(eh); } } else if (head == jl_pop_exception_sym) { diff --git a/src/jl_exported_funcs.inc b/src/jl_exported_funcs.inc index d0cfb16062b7d..6b0015b23653e 100644 --- a/src/jl_exported_funcs.inc +++ b/src/jl_exported_funcs.inc @@ -106,6 +106,7 @@ XX(jl_egal) \ XX(jl_egal__bits) \ XX(jl_egal__bitstag) \ + XX(jl_eh_longjmp) \ XX(jl_eh_restore_state) \ XX(jl_eh_restore_state_noexcept) \ XX(jl_enter_handler) \ @@ -301,6 +302,8 @@ XX(jl_maxrss) \ XX(jl_method_def) \ XX(jl_method_instance_add_backedge) \ + XX(jl_minimal_longjmp) \ + XX(jl_minimal_setjmp) \ XX(jl_method_table_add_backedge) \ XX(jl_method_table_disable) \ XX(jl_method_table_for) \ diff --git a/src/julia.h b/src/julia.h index 9513dfb47941d..bcfd0b8331d97 100644 --- a/src/julia.h +++ b/src/julia.h @@ -35,6 +35,22 @@ #define jl_jmp_buf jmp_buf #endif +// Minimal setjmp buffer: stores stack pointer and return address. +// Used with preserve_none calling convention where the compiler spills all +// live registers before the call. +typedef struct { + void *rbp; // Base pointer + void *rsp; // Stack pointer (as it was before the call to setjmp) + void *rip; // Return address (where to resume after longjmp) +} jl_min_jmp_buf; + +// Check if minimal setjmp is available for this platform +#if defined(__x86_64__) && defined(__linux__) && !defined(_OS_WINDOWS_) +#define JL_HAVE_MIN_SETJMP 1 +#else +#define JL_HAVE_MIN_SETJMP 0 +#endif + // Define the largest size (bytes) of a properly aligned object that the // processor family (MAX_ATOMIC_SIZE) and compiler (MAX_POINTERATOMIC_SIZE) // typically supports without a lock (assumed to be at least a pointer size) @@ -2304,16 +2320,30 @@ JL_DLLEXPORT void jl_sigatomic_end(void); // tasks and exceptions ------------------------------------------------------- // info describing an exception handler +// The eh_ctx field can hold either a full jl_jmp_buf or a minimal jl_min_jmp_buf. +// When using minimal setjmp, the low bit of the buffer pointer (when passed to +// longjmp wrapper) indicates which type is in use. struct _jl_handler_t { - jl_jmp_buf eh_ctx; jl_gcframe_t *gcstack; jl_value_t *scope; struct _jl_handler_t *prev; int8_t gc_state; + int8_t min_jmp; // 1 if using minimal setjmp buffer, 0 otherwise size_t locks_len; sig_atomic_t defer_signal; jl_timing_block_t *timing_stack; size_t world_age; + // eh_ctx follows +}; + +struct _jl_handler_setjmp { + struct _jl_handler_t _handler; + jl_jmp_buf eh_ctx; // Full setjmp buffer +}; + +struct _jl_handler_min_setjmp { + struct _jl_handler_t _handler; + jl_min_jmp_buf min_eh_ctx; // Minimal setjmp buffer (stack pointer only) }; #define JL_TASK_STATE_RUNNABLE 0 @@ -2350,6 +2380,7 @@ JL_DLLEXPORT jl_value_t *jl_exception_occurred(void); JL_DLLEXPORT void jl_exception_clear(void) JL_NOTSAFEPOINT; JL_DLLEXPORT void jl_enter_handler(jl_task_t *ct, jl_handler_t *eh) JL_NOTSAFEPOINT ; +JL_DLLEXPORT void jl_enter_min_handler(jl_task_t *ct, jl_handler_t *eh) JL_NOTSAFEPOINT ; JL_DLLEXPORT void jl_eh_restore_state(jl_task_t *ct, jl_handler_t *eh); JL_DLLEXPORT void jl_eh_restore_state_noexcept(jl_task_t *ct, jl_handler_t *eh) JL_NOTSAFEPOINT; JL_DLLEXPORT void jl_pop_handler(jl_task_t *ct, int n) JL_NOTSAFEPOINT; @@ -2403,6 +2434,43 @@ extern void (*real_siglongjmp)(jmp_buf _Buf, int _Value); #endif #endif +// Minimal setjmp/longjmp declarations for x86_64 Linux +// These work with preserve_none calling convention where the compiler +// spills all live registers, so we only need to save the stack pointer. +#if JL_HAVE_MIN_SETJMP + +// Check if preserve_none calling convention is supported. +// This calling convention makes ALL registers caller-saved, which means the +// caller will spill any live registers before calling. This is essential for +// the minimal setjmp to work correctly. +#if defined(__has_attribute) && __has_attribute(preserve_none) +#define JL_HAVE_PRESERVE_NONE 1 +#define JL_PRESERVE_NONE __attribute__((preserve_none)) +typedef struct _jl_handler_min_setjmp jl_handler_preferred_t; +JL_DLLEXPORT int __attribute__((__nothrow__, __returns_twice__)) + jl_minimal_setjmp(jl_min_jmp_buf *buf) JL_PRESERVE_NONE; +#define JL_EH_SETJMP(eh) (eh._handler.min_jmp = 1,\ + jl_minimal_setjmp(&eh.min_eh_ctx)) +#else +typedef struct _jl_handler_setjmp jl_handler_preferred_t; +#define JL_HAVE_PRESERVE_NONE 0 +static inline int jl_minimal_setjmp(jl_min_jmp_buf *buf) +{ + abort(); // Not supported +} +#define JL_EH_SETJMP(eh) (eh._handler.min_jmp = 0,\ + jl_setjmp(&eh.eh_ctx, 0)) +#endif + +JL_DLLEXPORT void __attribute__((__nothrow__, __noreturn__)) + jl_minimal_longjmp(jl_min_jmp_buf *buf, int val); +#endif // JL_HAVE_MIN_SETJMP + +// Unified longjmp wrapper that handles both minimal and full buffers. +// Declared in task.c +JL_DLLEXPORT void __attribute__((__nothrow__, __noreturn__)) + jl_eh_longjmp(jl_handler_t *eh); + #ifdef __clang_gcanalyzer__ @@ -2426,17 +2494,20 @@ extern int had_exception; #else +// Use minimal setjmp when available and the compiler supports preserve_none. +// This saves memory by having the compiler spill only live registers instead +// of saving all callee-saved registers like traditional setjmp. #define JL_TRY \ - int i__try, i__catch; jl_handler_t __eh; jl_task_t *__eh_ct; \ + int i__try, i__catch; jl_handler_preferred_t __eh; jl_task_t *__eh_ct; \ __eh_ct = jl_current_task; \ size_t __excstack_state = jl_excstack_state(__eh_ct); \ - jl_enter_handler(__eh_ct, &__eh); \ - if (!jl_setjmp(__eh.eh_ctx, 0)) \ - for (i__try=1, __eh_ct->eh = &__eh; i__try; i__try=0, /* TRY BLOCK; */ jl_eh_restore_state_noexcept(__eh_ct, &__eh)) + jl_enter_handler(__eh_ct, &__eh._handler); \ + if (!JL_EH_SETJMP(__eh)) \ + for (i__try=1, __eh_ct->eh = &__eh._handler; i__try; i__try=0, /* TRY BLOCK; */ jl_eh_restore_state_noexcept(__eh_ct, &__eh._handler)) #define JL_CATCH \ else \ - for (i__catch=1, jl_eh_restore_state(__eh_ct, &__eh); i__catch; i__catch=0, /* CATCH BLOCK; */ jl_restore_excstack(__eh_ct, __excstack_state)) + for (i__catch=1, jl_eh_restore_state(__eh_ct, &__eh._handler); i__catch; i__catch=0, /* CATCH BLOCK; */ jl_restore_excstack(__eh_ct, __excstack_state)) #endif diff --git a/src/julia_internal.h b/src/julia_internal.h index 94036981f950a..87301f9f9b169 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -79,6 +79,7 @@ static inline void asan_unpoison_stack_memory(uintptr_t addr, size_t size) { } #else static inline void asan_unpoison_task_stack(jl_task_t *ct, jl_jmp_buf *buf) JL_NOTSAFEPOINT {} +static inline void asan_unpoison_eh_task_stack(jl_task_t *ct, jl_handler_t *eh) JL_NOTSAFEPOINT {} static inline void asan_unpoison_stack_memory(uintptr_t addr, size_t size) JL_NOTSAFEPOINT {} #endif #ifdef _COMPILER_MSAN_ENABLED_ diff --git a/src/rtutils.c b/src/rtutils.c index e73d0de6c69aa..f0c2bee8200e4 100644 --- a/src/rtutils.c +++ b/src/rtutils.c @@ -268,6 +268,23 @@ JL_DLLEXPORT void jl_enter_handler(jl_task_t *ct, jl_handler_t *eh) eh->gcstack = ct->gcstack; eh->scope = ct->scope; eh->gc_state = jl_atomic_load_relaxed(&ct->ptls->gc_state); + eh->min_jmp = 0; // Default to traditional setjmp; caller may override if using minimal setjmp + eh->locks_len = ct->ptls->locks.len; + eh->defer_signal = ct->ptls->defer_signal; + eh->world_age = ct->world_age; +#ifdef ENABLE_TIMINGS + eh->timing_stack = ct->ptls->timing_stack; +#endif +} + +JL_DLLEXPORT void jl_enter_min_handler(jl_task_t *ct, jl_handler_t *eh) +{ + // Must have no safepoint + eh->prev = ct->eh; + eh->gcstack = ct->gcstack; + eh->scope = ct->scope; + eh->gc_state = jl_atomic_load_relaxed(&ct->ptls->gc_state); + eh->min_jmp = 1; eh->locks_len = ct->ptls->locks.len; eh->defer_signal = ct->ptls->defer_signal; eh->world_age = ct->world_age; diff --git a/src/signals-unix.c b/src/signals-unix.c index 16e70ef0f764e..4a7cc74eba222 100644 --- a/src/signals-unix.c +++ b/src/signals-unix.c @@ -67,6 +67,8 @@ static int thread0_exit_count = 0; static void jl_exit_thread0(int signo, jl_bt_element_t *bt_data, size_t bt_size); int jl_simulate_longjmp(jl_jmp_buf mctx, bt_context_t *c) JL_NOTSAFEPOINT; +int jl_simulate_min_longjmp(jl_min_jmp_buf *buf, bt_context_t *c) JL_NOTSAFEPOINT; +static void jl_eh_longjmp_in_ctx(int sig, void *_ctx, jl_handler_t *eh); static void jl_longjmp_in_ctx(int sig, void *_ctx, jl_jmp_buf jmpbuf); #if !defined(_OS_DARWIN_) @@ -210,8 +212,8 @@ static void jl_throw_in_ctx(jl_task_t *ct, jl_value_t *e, int sig, void *sigctx) ptls->io_wait = 0; jl_handler_t *eh = ct->eh; if (eh != NULL) { - asan_unpoison_task_stack(ct, &eh->eh_ctx); - jl_longjmp_in_ctx(sig, sigctx, eh->eh_ctx); + asan_unpoison_eh_task_stack(ct, eh); + jl_eh_longjmp_in_ctx(sig, sigctx, eh); } else { jl_no_exc_handler(e, ct); @@ -1154,6 +1156,20 @@ static void fpe_handler(int sig, siginfo_t *info, void *context) jl_throw_in_ctx(ct, jl_diverror_exception, sig, context); } +static void jl_eh_longjmp_in_ctx(int sig, void *_ctx, jl_handler_t *eh) +{ + bt_context_t *ctx = jl_to_bt_context(_ctx); + if (eh->min_jmp ? + jl_simulate_min_longjmp(&((struct _jl_handler_min_setjmp*)eh)->min_eh_ctx, ctx) : + jl_simulate_longjmp(((struct _jl_handler_setjmp*)eh)->eh_ctx, ctx)) + return; + sigset_t sset; + sigemptyset(&sset); + sigaddset(&sset, sig); + pthread_sigmask(SIG_UNBLOCK, &sset, NULL); + jl_eh_longjmp(eh); +} + static void jl_longjmp_in_ctx(int sig, void *_ctx, jl_jmp_buf jmpbuf) { #if defined(_OS_DARWIN_) diff --git a/src/stackwalk.c b/src/stackwalk.c index 96f263dfd7f68..a5af88d41dd01 100644 --- a/src/stackwalk.c +++ b/src/stackwalk.c @@ -1358,6 +1358,20 @@ return 0; #endif } +int jl_simulate_min_longjmp(jl_min_jmp_buf *buf, bt_context_t *c) JL_NOTSAFEPOINT +{ +#if defined(_CPU_X86_64_) + // minimal longjmp for libunwind on x86_64 + mcontext_t *mc = &c->uc_mcontext; + mc->gregs[REG_RBP] = ptr_demangle((uintptr_t)buf->rbp); + mc->gregs[REG_RSP] = ptr_demangle((uintptr_t)buf->rsp); + mc->gregs[REG_RIP] = ptr_demangle((uintptr_t)buf->rip); + return 1; +#endif +return 0; + +} + JL_DLLEXPORT size_t jl_try_record_thread_backtrace(jl_ptls_t ptls2, jl_bt_element_t *bt_data, size_t max_bt_size) JL_NOTSAFEPOINT { int16_t tid = ptls2->tid; diff --git a/src/task.c b/src/task.c index 18d21b2343053..8e7a7281b1c36 100644 --- a/src/task.c +++ b/src/task.c @@ -773,6 +773,18 @@ JL_DLLEXPORT JL_NORETURN void jl_no_exc_handler(jl_value_t *e, jl_task_t *ct) #define pop_timings_stack() /* Nothing */ #endif +// Unified longjmp wrapper that handles both minimal and full buffers. +// The min_jmp field in the handler indicates which type of buffer is in use. +JL_DLLEXPORT void JL_NORETURN jl_eh_longjmp(jl_handler_t *eh) +{ +#if JL_HAVE_MIN_SETJMP + if (eh->min_jmp) { + jl_minimal_longjmp(&((struct _jl_handler_min_setjmp*)eh)->min_eh_ctx, 1); + } +#endif + jl_longjmp(((struct _jl_handler_setjmp*)eh)->eh_ctx, 1); +} + static void JL_NORETURN throw_internal(jl_task_t *ct, jl_value_t *exception JL_MAYBE_UNROOTED) { JL_GC_PUSH1(&exception); @@ -791,8 +803,8 @@ static void JL_NORETURN throw_internal(jl_task_t *ct, jl_value_t *exception JL_M jl_handler_t *eh = ct->eh; if (eh != NULL) { pop_timings_stack() - asan_unpoison_task_stack(ct, &eh->eh_ctx); - jl_longjmp(eh->eh_ctx, 1); + asan_unpoison_eh_task_stack(ct, eh); + jl_eh_longjmp(eh); } else { jl_no_exc_handler(exception, ct);