diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml new file mode 100644 index 000000000..2bd785553 --- /dev/null +++ b/.github/workflows/windows-arm64.yml @@ -0,0 +1,68 @@ +name: Windows arm64 + +on: [push] + +jobs: + build: + + runs-on: windows-11-arm + + strategy: + matrix: + include: + - { sys: clangarm64, env: clang-aarch64, arch: arm64} + + fail-fast: false + + defaults: + run: + shell: msys2 {0} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: msys2/setup-msys2@v2 + with: + install: mingw-w64-${{matrix.env}}-clang mingw-w64-${{matrix.env}}-toolchain make diffutils git libzstd-devel + msystem: ${{matrix.sys}} + + - name: install host sbcl + shell: pwsh + run: | + choco install sbcl -source tools-for-build + - name: install WiX Toolset + shell: pwsh + run: | + choco install wixtoolset + $wixPath = (Get-ChildItem "C:\Program Files (x86)\WiX Toolset*" | Select-Object -First 1).FullName + echo "WIX=$wixPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: build + env: + SBCL_HOME: "/c/Program Files/Steel Bank Common Lisp/1.4.14" + run: | + PATH=$PATH:"/c/Program Files/Steel Bank Common Lisp/1.4.14" + export PATH + ./make.sh --arch=${{matrix.arch}} --with-sb-core-compression --xc-host='sbcl --lose-on-corruption --disable-ldb --disable-debugger' + - name: make installer + env: + WIX: ${{ env.WIX }} + run: | + ./make-windows-installer.sh + mkdir sbcl-windows-installer; mv output/*msi sbcl-windows-installer + - name: save installer + uses: actions/upload-artifact@v4 + with: + name: sbcl-windows-installer-${{ matrix.sys }} + path: sbcl-windows-installer + - name: tests + continue-on-error: true + working-directory: tests + run: ./run-tests.sh + - name: ansi-tests + continue-on-error: true + working-directory: tests + run: ./ansi-tests.sh + + + diff --git a/make-config.sh b/make-config.sh index 1f885a540..5940463fc 100755 --- a/make-config.sh +++ b/make-config.sh @@ -694,6 +694,17 @@ case "$sbcl_os" in printf ' :os-provides-dlopen' >> $ltf printf ' :sb-thread :sb-safepoint' >> $ltf # + case "$sbcl_arch" in + arm64) + printf ' :immobile-space' >> $ltf + ;; + x86 | x86-64) + ;; + *) + echo "unsupported architecture for win32: $sbcl_arch" + exit 1 + ;; + esac link_or_copy Config.$sbcl_arch-win32 Config link_or_copy $sbcl_arch-win32-os.h target-arch-os.h link_or_copy win32-os.h target-os.h diff --git a/make-windows-installer.sh b/make-windows-installer.sh index b64b95a21..6f6b18976 100644 --- a/make-windows-installer.sh +++ b/make-windows-installer.sh @@ -26,7 +26,7 @@ cd ./output :if-exists :supersede) (format f "~a-~a-windows-binary" (lisp-implementation-version) - #+x86 "x86" #+x86-64 "x86-64")) + #+x86 "x86" #+x86-64 "x86-64" #+arm64 "arm64")) (exit))' "$WIX_PATH/candle" sbcl.wxs diff --git a/src/assembly/arm64/tramps.lisp b/src/assembly/arm64/tramps.lisp index 44e9fc5f5..d548943c1 100644 --- a/src/assembly/arm64/tramps.lisp +++ b/src/assembly/arm64/tramps.lisp @@ -90,7 +90,7 @@ (inst ldr lr-tn (@ csp-tn -104)) (inst sub csp-tn csp-tn (+ 32 80)) ;; deallocate the frame - #+sb-thread + #+(and sb-thread (not win32)) (inst str zr-tn (@ thread-tn (* thread-control-stack-pointer-slot n-word-bytes))) #-sb-thread (progn @@ -146,6 +146,7 @@ (map-pairs ldp csp-tn -16 lisp-registers :delta -16) (inst ldr lr-tn (@ csp-tn -104)) (inst sub csp-tn csp-tn (+ 32 80)) + #-win32 (inst str zr-tn (@ thread-tn (* thread-control-stack-pointer-slot n-word-bytes)))) (map-pairs ldp nsp-tn 64 nl-registers :post-index 80 :delta -16) (inst ret)) @@ -167,6 +168,7 @@ (map-pairs ldp csp-tn -16 lisp-registers :delta -16) (inst ldr lr-tn (@ csp-tn -104)) (inst sub csp-tn csp-tn (+ 32 80)) + #-win32 (inst str zr-tn (@ thread-tn (* thread-control-stack-pointer-slot n-word-bytes)))) (map-pairs ldp nsp-tn 64 nl-registers :post-index 80 :delta -16) (inst ret))))) diff --git a/src/code/debug-int.lisp b/src/code/debug-int.lisp index a34061bc5..84d0e11b0 100644 --- a/src/code/debug-int.lisp +++ b/src/code/debug-int.lisp @@ -2625,11 +2625,11 @@ register." :invalid-value-for-unescaped-register-storage)) (with-nfp ((var) &body body) ;; x86oids have no separate number stack, so dummy it - ;; up for them. - #+c-stack-is-control-stack + ;; up for them. ARM64 Windows is similar - use control stack pointer. + #+(or c-stack-is-control-stack (and arm64 win32)) `(let ((,var fp)) ,@body) - #-c-stack-is-control-stack + #-(or c-stack-is-control-stack (and arm64 win32)) `(let ((,var (if escaped (int-sap (context-register escaped sb-vm::nfp-offset)) diff --git a/src/code/float-trap.lisp b/src/code/float-trap.lisp index 8bc7755b4..224f750d3 100644 --- a/src/code/float-trap.lisp +++ b/src/code/float-trap.lisp @@ -158,7 +158,9 @@ sets the floating point modes to their current values (and thus is a no-op)." ;;; disabled by default. Joe User can explicitly enable them if ;;; desired. (define-load-time-global *saved-floating-point-modes* - '(:traps (:overflow #-(or netbsd ppc) :invalid :divide-by-zero) + '(:traps (#-(and arm64 win32) :overflow + #-(or netbsd ppc (and arm64 win32)) :invalid + #-(and arm64 win32) :divide-by-zero) :rounding-mode :nearest :current-exceptions nil :accrued-exceptions nil :fast-mode nil #+x86 :precision #+x86 :53-bit)) diff --git a/src/compiler/arm64/c-call.lisp b/src/compiler/arm64/c-call.lisp index 03843f4e4..43b98d227 100644 --- a/src/compiler/arm64/c-call.lisp +++ b/src/compiler/arm64/c-call.lisp @@ -481,6 +481,7 @@ ;; No longer OK to run GC except at safepoints. #+(or sb-safepoint nonstop-foreign-call) (storew zr-tn thread-tn thread-saved-csp-slot)) + #-win32 (storew zr-tn thread-tn thread-control-stack-pointer-slot) return #-sb-thread diff --git a/src/compiler/arm64/parms.lisp b/src/compiler/arm64/parms.lisp index ad2ac2101..3e8518903 100644 --- a/src/compiler/arm64/parms.lisp +++ b/src/compiler/arm64/parms.lisp @@ -69,7 +69,7 @@ ;;;; Where to put the different spaces. -(gc-space-setup #+(or linux openbsd netbsd freebsd) +(gc-space-setup #+(or linux openbsd netbsd freebsd win32) #x2F0000000 #+darwin #x300000000 #-darwin :read-only-space-size #-darwin 0 diff --git a/src/compiler/arm64/vm.lisp b/src/compiler/arm64/vm.lisp index a2cf196a0..945c7578b 100644 --- a/src/compiler/arm64/vm.lisp +++ b/src/compiler/arm64/vm.lisp @@ -45,7 +45,8 @@ (defreg r5 15) (defreg r6 16) (defreg r7 17) - (defreg #-darwin r8 #+darwin reserved 18) + ;; x18 is reserved on Darwin (macOS) and Windows (TEB pointer) + (defreg #-(or darwin win32) r8 #+(or darwin win32) reserved 18) (defreg r9 19) (defreg r10 20) @@ -68,7 +69,7 @@ null cfp nsp lr) (defregset descriptor-regs - r0 r1 r2 r3 r4 r5 r6 r7 #-darwin r8 r9 r10 #-sb-thread r11 lexenv) + r0 r1 r2 r3 r4 r5 r6 r7 #-(or darwin win32) r8 r9 r10 #-sb-thread r11 lexenv) ;; nl9 can't be selected by PACK as it is a freely usable temp reg (defregset non-descriptor-regs @@ -76,7 +77,7 @@ (defregset boxed-regs r0 r1 r2 r3 r4 r5 r6 - r7 #-darwin r8 r9 r10 #-sb-thread r11 lexenv) + r7 #-(or darwin win32) r8 r9 r10 #-sb-thread r11 lexenv) ;; registers used to pass arguments ;; @@ -85,7 +86,7 @@ ;; names and offsets for registers used to pass arguments (defregset *register-arg-offsets* r0 r1 r2 r3) (defconstant-eqx register-arg-names '(r0 r1 r2 r3) #'equal) - (defregset *descriptor-args* r0 r1 r2 r3 r4 r5 r6 r7 #-darwin r8 r9 r10) + (defregset *descriptor-args* r0 r1 r2 r3 r4 r5 r6 r7 #-(or darwin win32) r8 r9 r10) (defregset *non-descriptor-args* nl0 nl1 nl2 nl3 nl4 nl5 nl6 nl7 nl8) (defglobal *float-regs* (loop for i below 32 collect i))) diff --git a/src/compiler/generic/genesis.lisp b/src/compiler/generic/genesis.lisp index c30f0a753..5a321706f 100644 --- a/src/compiler/generic/genesis.lisp +++ b/src/compiler/generic/genesis.lisp @@ -3297,12 +3297,14 @@ Legal values for OFFSET are -4, -8, -12, ..." "#define PSEUDO_ATOMIC_TRAP ~D /* 0x~:*~X */~%" sb-vm::pseudo-atomic-trap) (terpri)) + ;; x86-64 uses GC_SAFEPOINT_PAGE_ADDR from gc.h (at STATIC_SPACE_END). + ;; Other platforms (including ARM64 Windows) place it before STATIC_SPACE_START. #+(and sb-safepoint (not x86-64)) (progn - (format t "#define GC_SAFEPOINT_PAGE_ADDR (void*)((char*)STATIC_SPACE_START - ~d)~%" - sb-c:+backend-page-bytes+) - (format t "#define GC_SAFEPOINT_TRAP_ADDR (void*)((char*)STATIC_SPACE_START - ~d)~%" - sb-vm:gc-safepoint-trap-offset)) + (format t "#define GC_SAFEPOINT_PAGE_ADDR (void*)((char*)STATIC_SPACE_START - ~d)~%" + sb-c:+backend-page-bytes+) + (format t "#define GC_SAFEPOINT_TRAP_ADDR (void*)((char*)STATIC_SPACE_START - ~d)~%" + sb-vm:gc-safepoint-trap-offset)) (dolist (symbol '(sb-vm:float-traps-byte sb-vm::float-exceptions-byte diff --git a/src/compiler/generic/parms.lisp b/src/compiler/generic/parms.lisp index 52c4e72d0..845cbc263 100644 --- a/src/compiler/generic/parms.lisp +++ b/src/compiler/generic/parms.lisp @@ -83,10 +83,9 @@ ;; #+immobile-space implies a relocatable alien linkage space. And x86-64 always ;; has relocatable linkage tables #-(or x86-64 immobile-space) (alien-linkage ,alien-linkage-space-size) - ;; safepoint on 64-bit uses a relocatable trap page just below the card mark - ;; table, which works nicely assuming a register is wired to the card table + ;; x86-64 uses a relocatable trap page just below the card mark + ;; table (wired to a register). Other platforms allocate a separate page. #+(and sb-safepoint (not x86-64)) - ;; Must be just before NIL. (safepoint ,(symbol-value '+backend-page-bytes+)) (static ,small-space-size) #+darwin-jit (static-code ,small-space-size))) diff --git a/src/runtime/Config.arm64-win32 b/src/runtime/Config.arm64-win32 new file mode 100644 index 000000000..8c2843953 --- /dev/null +++ b/src/runtime/Config.arm64-win32 @@ -0,0 +1,55 @@ +# This software is part of the SBCL system. See the README file for +# more information. +# +# This software is derived from the CMU CL system, which was +# written at Carnegie Mellon University and released into the +# public domain. The software is in the public domain and is +# provided with absolutely no warranty. See the COPYING and CREDITS +# files for more information. + +TARGET=sbcl.exe + +ASSEM_SRC = arm64-assem.S +ARCH_SRC = arm64-arch.c + +OS_SRC = win32-os.c arm64-win32-os.c + +ifdef LISP_FEATURE_SB_LINKABLE_RUNTIME + LIBSBCL = libsbcl.a + USE_LIBSBCL = -Wl,--whole-archive libsbcl.a -Wl,--no-whole-archive +endif + +LINKFLAGS = -Wl,-export-all-symbols +LIBSBCL += mswin64.def mswin.def +USE_LIBSBCL += -Wl,mswin64.def -Wl,mswin.def +SOFLAGS += -Wl,-export-all-symbols -Wl,mswin64.def -Wl,mswin.def + +__LDFLAGS__ = + +OS_LIBS = -l ws2_32 -ladvapi32 +ifdef LISP_FEATURE_SB_CORE_COMPRESSION + OS_LIBS += -lzstd +endif +ifdef LISP_FEATURE_SB_FUTEX + OS_LIBS += -lSynchronization +endif + +ifdef LISP_FEATURE_IMMOBILE_SPACE + GC_SRC = fullcgc.c gencgc.c traceroot.c immobile-space.c +else + GC_SRC = fullcgc.c gencgc.c traceroot.c +endif + +CFLAGS += -g -W -Wall \ + -Wno-unused-function -Wno-unused-parameter -Wno-cast-function-type \ + -Wno-type-limits \ + -fno-omit-frame-pointer \ + -O3 -DWINVER=0x0501 \ + -D__W32API_USE_DLLIMPORT__ \ + -std=gnu99 + +CC = clang + +ifeq ($(shell $(LD) --disable-dynamicbase 2>&1 | grep disable-dynamicbase),) +LINKFLAGS += -Wl,--disable-dynamicbase +endif diff --git a/src/runtime/arm64-arch.c b/src/runtime/arm64-arch.c index 2296c2762..01266d755 100644 --- a/src/runtime/arm64-arch.c +++ b/src/runtime/arm64-arch.c @@ -25,7 +25,14 @@ os_vm_address_t arch_get_bad_addr(int sig, siginfo_t *code, os_context_t *context) { +#ifdef WIN32 + /* The `code` argument is really a pointer to an EXCEPTION_RECORD, + * not a siginfo_t. */ + EXCEPTION_RECORD *exception = (EXCEPTION_RECORD *)code; + return (os_vm_address_t)exception->ExceptionInformation[1]; +#else return (os_vm_address_t)code->si_addr; +#endif } void arch_skip_instruction(os_context_t *context) @@ -210,20 +217,30 @@ void arch_do_displaced_inst(os_context_t *context, unsigned int orig_inst) } else { // Do orig_inst by copying it into a trampoline. +#ifdef WIN32 + SYSTEM_INFO sys_info; + GetSystemInfo(&sys_info); + size_t size = sys_info.dwPageSize; +#else size_t size = getpagesize(); +#endif // Allocate the thread-local trampoline on-demand. struct thread *th = get_sb_vm_thread(); unsigned int *trampoline = (unsigned int*)th->breakpoint_misc; if (!trampoline) { +#ifdef WIN32 + trampoline = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +#else trampoline = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); +#endif th->breakpoint_misc = trampoline; } else - os_protect((os_vm_address_t)trampoline, size, PROT_READ | PROT_WRITE); + os_protect((os_vm_address_t)trampoline, size, OS_VM_PROT_READ | OS_VM_PROT_WRITE); unsigned int *inst_ptr = trampoline; unsigned int inst; @@ -246,11 +263,13 @@ void arch_do_displaced_inst(os_context_t *context, unsigned int orig_inst) inst = 0xD61F0000 | BREAKPOINT_TEMP_REG << 5; *inst_ptr++ = inst; - // address - *(unsigned long *)inst_ptr++ = (unsigned long)next_pc; + // address (8 bytes for a 64-bit pointer; unsigned long is only + // 4 bytes on Windows LLP64, so use uint64_t explicitly) + *(uint64_t *)inst_ptr = (uint64_t)next_pc; + inst_ptr += 2; OS_CONTEXT_PC(context) = (uword_t)trampoline; os_flush_icache((os_vm_address_t) trampoline, (char*) inst_ptr - (char*)trampoline); - os_protect((os_vm_address_t)trampoline, size, PROT_READ | PROT_EXEC); + os_protect((os_vm_address_t)trampoline, size, OS_VM_PROT_READ | OS_VM_PROT_EXECUTE); return; } @@ -277,6 +296,7 @@ arch_handle_single_step_trap(os_context_t *context, int trap) arch_skip_instruction(context); } +#ifndef WIN32 static void sigtrap_handler(int signal, siginfo_t *siginfo, os_context_t *context) { @@ -289,10 +309,11 @@ sigtrap_handler(int signal, siginfo_t *siginfo, os_context_t *context) handle_trap(context, code); } +#endif void sigill_handler(int signal, siginfo_t *siginfo, os_context_t *context); -#ifndef LISP_FEATURE_DARWIN +#if !defined(LISP_FEATURE_DARWIN) && !defined(WIN32) void sigill_handler(int signal, siginfo_t *siginfo, os_context_t *context) { fake_foreign_function_call(context); @@ -302,8 +323,12 @@ sigill_handler(int signal, siginfo_t *siginfo, os_context_t *context) { void arch_install_interrupt_handlers() { +#ifndef WIN32 ll_install_handler(SIGTRAP, sigtrap_handler); ll_install_handler(SIGILL, sigill_handler); +#else + // Interrupt handlers are installed in win32-os.c +#endif } @@ -320,7 +345,7 @@ void arch_write_linkage_table_entry(int index, void *target_addr, int datap) char *reloc_addr = (char*)ALIEN_LINKAGE_SPACE_START + index * ALIEN_LINKAGE_TABLE_ENTRY_SIZE; if (datap) { - *(unsigned long *)reloc_addr = (unsigned long)target_addr; + *(uword_t *)reloc_addr = (uword_t)target_addr; goto DONE; } /* @@ -341,8 +366,9 @@ void arch_write_linkage_table_entry(int index, void *target_addr, int datap) inst = 0xD61F0000 | LINKAGE_TEMP_REG << 5; *inst_ptr++ = inst; - // address - *(unsigned long *)inst_ptr++ = (unsigned long)target_addr; + // address (64-bit pointer - must use uword_t on Windows where unsigned long is 32-bit) + *(uword_t *)inst_ptr = (uword_t)target_addr; + inst_ptr += 2; // advance by 8 bytes (2 x 4-byte ints) os_flush_icache((os_vm_address_t) reloc_addr, (char*) inst_ptr - reloc_addr); @@ -369,6 +395,19 @@ os_vm_address_t coreparse_alloc_space(int space_id, int attr, addr -= extra_request; // try to put text space start where expected } #endif + +#ifdef WIN32 + // On Windows ARM64, STATIC_SPACE needs extra space before it for NIL header access. + // ARM64 accesses NIL-0x1F for type checking, which must be within allocated memory. + // This must be done even when size==0. + if (space_id == STATIC_CORE_SPACE_ID) { + uword_t alloc_size = (size == 0) ? BACKEND_PAGE_BYTES : size + BACKEND_PAGE_BYTES; + addr = os_alloc_gc_space(space_id, attr, addr - BACKEND_PAGE_BYTES, alloc_size) + BACKEND_PAGE_BYTES; + if (!addr) lose("Can't allocate %#"OBJ_FMTX" bytes for space %d", size, space_id); + return addr; + } +#endif + if (size == 0) return addr; #ifdef LISP_FEATURE_SB_SAFEPOINT @@ -385,14 +424,28 @@ os_vm_address_t coreparse_alloc_space(int space_id, int attr, #ifdef LISP_FEATURE_IMMOBILE_SPACE if (space_id == IMMOBILE_TEXT_CORE_SPACE_ID) { ALIEN_LINKAGE_SPACE_START = (uword_t)addr; + // TEXT_SPACE actually starts after ALIEN_LINKAGE_SPACE + TEXT_SPACE_START = (uword_t)addr + extra_request; addr += extra_request; } #endif return addr; } +#ifdef _WIN32 +/* Windows ARM64 (LLP64): unsigned long is 32-bit, and __uint128_t + passing via registers differs from the Lisp alien-funcall ABI. + Take hi/lo as separate 64-bit args to match the Lisp transform. */ +uint64_t sb_udivmodti4(uint64_t lo, uint64_t hi, uint64_t y, uint64_t *rem) { + __uint128_t x = ((__uint128_t)hi << 64) | lo; + if (rem) + *rem = x % y; + return x / y; +} +#else long sb_udivmodti4(__uint128_t x, unsigned long y, unsigned long *rem) { if (rem) *rem = x % y; return x / y; } +#endif diff --git a/src/runtime/arm64-assem.S b/src/runtime/arm64-assem.S index 57b6e945e..34fd25543 100644 --- a/src/runtime/arm64-assem.S +++ b/src/runtime/arm64-assem.S @@ -23,8 +23,16 @@ ldr \dest, [\dest, _\symbol@PAGEOFF] .endm -#else +#elif defined(LISP_FEATURE_WIN32) +#define TYPE(name) +#define SIZE(name) +#define GNAME(var) var +.macro LOAD_GNAME, dest, symbol + adrp \dest, \symbol + ldr \dest, [\dest, #:lo12:\symbol] +.endm +#else #define TYPE(name) .type name,%function #define SIZE(name) .size name,.-name #define GNAME(var) var @@ -153,6 +161,22 @@ GNAME(call_into_lisp): mov reg_LEXENV, x0 #ifdef LISP_FEATURE_SB_THREAD +#if defined(LISP_FEATURE_WIN32) + // On Windows ARM64, get thread pointer via TlsGetValue(OUR_TLS_INDEX) + // Save arguments (x1, x2) and link register before C call + stp x1, x2, [sp, #-32]! + stp x29, x30, [sp, #16] + + // Load OUR_TLS_INDEX (sbcl_thread_tls_index) and call TlsGetValue + adrp x0, sbcl_thread_tls_index + ldr w0, [x0, #:lo12:sbcl_thread_tls_index] + bl TlsGetValue + mov reg_THREAD, x0 + + // Restore arguments and link register + ldp x29, x30, [sp, #16] + ldp x1, x2, [sp], #32 +#else #ifdef LISP_FEATURE_GCC_TLS #ifdef LISP_FEATURE_DARWIN @@ -179,6 +203,7 @@ GNAME(call_into_lisp): bl pthread_getspecific mov reg_THREAD, x0 #endif +#endif #endif // Clear the boxed registers that don't already have something // in them. @@ -190,7 +215,8 @@ GNAME(call_into_lisp): mov reg_R5, #0 mov reg_R6, #0 mov reg_R7, #0 -#ifndef LISP_FEATURE_DARWIN +#if !defined(LISP_FEATURE_DARWIN) && !defined(LISP_FEATURE_WIN32) + // x18 is reserved on Darwin (macOS) and Windows (TEB pointer) mov reg_R8, #0 #endif mov reg_R10, #0 @@ -219,7 +245,10 @@ GNAME(call_into_lisp): 1: #endif +// Windows uses control_stack_pointer as actual SP, not FFCA flag +#ifndef LISP_FEATURE_WIN32 str xzr,[reg_THREAD, THREAD_CONTROL_STACK_POINTER_OFFSET] +#endif #else ldr reg_NL3, =GNAME(foreign_function_call_active) str xzr, [reg_NL3] @@ -311,13 +340,21 @@ GNAME(call_into_c): // All other C arguments are already stashed on the C stack. // Build a Lisp stack frame. +#ifdef LISP_FEATURE_WIN32 + // On Windows ARM64, save thread CFP/CSP before overwriting. + // CSP cannot be zeroed to indicate "in Lisp" state on safepoint builds. + ldp x3, x4, [reg_THREAD, THREAD_CONTROL_FRAME_POINTER_OFFSET] + stp x3, x4, [reg_CSP, #-16]! + stp reg_CFP, reg_LR, [reg_CSP, #-16]! + add reg_R10, reg_CSP, #32 +#else // Can store two values above the stack pointer, interrupts ignore them. stp reg_CFP, reg_LR, [reg_CSP] add reg_R10, reg_CSP, #2*8 +#endif mov reg_LEXENV, reg_LR // Save the lisp stack and frame pointers. - #ifdef LISP_FEATURE_SB_THREAD stp reg_CSP, reg_R10, [reg_THREAD, THREAD_CONTROL_FRAME_POINTER_OFFSET] #else @@ -359,7 +396,8 @@ GNAME(call_into_c): mov reg_R5, #0 mov reg_R6, #0 mov reg_R7, #0 -#ifndef LISP_FEATURE_DARWIN +#if !defined(LISP_FEATURE_DARWIN) && !defined(LISP_FEATURE_WIN32) + // x18 is reserved on Darwin (macOS) and Windows (TEB pointer) mov reg_R8,#0 #endif #ifndef LISP_FEATURE_SB_THREAD @@ -373,6 +411,12 @@ GNAME(call_into_c): // Restore the Lisp stack and frame pointers +#ifdef LISP_FEATURE_WIN32 + // Restore CFP/LR from stack, then restore saved thread CFP/CSP. + ldp reg_CFP, reg_LR, [reg_CSP], #16 + ldp x3, x4, [reg_CSP], #16 + stp x3, x4, [reg_THREAD, THREAD_CONTROL_FRAME_POINTER_OFFSET] +#else #ifdef LISP_FEATURE_SB_THREAD str xzr, [reg_THREAD, THREAD_CONTROL_STACK_POINTER_OFFSET] #else @@ -380,6 +424,7 @@ GNAME(call_into_c): str xzr, [reg_OCFP] #endif mov reg_LR, reg_LEXENV +#endif ret SIZE(call_into_c) diff --git a/src/runtime/arm64-win32-os.c b/src/runtime/arm64-win32-os.c new file mode 100644 index 000000000..ac18482db --- /dev/null +++ b/src/runtime/arm64-win32-os.c @@ -0,0 +1,240 @@ +/* + * The ARM64 Win32 incarnation of arch-dependent OS-dependent routines. + * See also "win32-os.c". + */ + +/* + * This software is part of the SBCL system. See the README file for + * more information. + * + * This software is derived from the CMU CL system, which was + * written at Carnegie Mellon University and released into the + * public domain. The software is in the public domain and is + * provided with absolutely no warranty. See the COPYING and CREDITS + * files for more information. + */ + +#include +#include +#include // For memset +#include +#include +#include +#include +#include + +#include "os.h" +#include "arch.h" +#include "globals.h" +#include "interrupt.h" +#include "interr.h" +#include "lispregs.h" +#include "genesis/sbcl.h" + +#include +#include "runtime.h" +#include +#include +#include +#include "thread.h" /* dynamic_values_bytes */ +#include "align.h" + +#include "validate.h" + +#include // For VirtualQuery, GetLastError, FlushInstructionCache + +int arch_os_thread_init(struct thread *thread) +{ + // On ARM64 Windows, we use the allocated Lisp control stack (from alloc_thread_struct), + // not the OS thread's C stack. Unlike x86-64 which uses C_STACK_IS_CONTROL_STACK, + // ARM64 has a separate Lisp control stack. + // Therefore, we do NOT call VirtualQuery here to overwrite control_stack_start/end. + // They have already been set correctly by alloc_thread_struct(). + + // CRITICAL: Set the TLS value so that get_sb_vm_thread() returns the correct thread! + // Without this, funcall0-3 will get the wrong thread from TlsGetValue. + extern DWORD OUR_TLS_INDEX; + TlsSetValue(OUR_TLS_INDEX, thread); + + extern void win32_set_stack_guarantee(void); + win32_set_stack_guarantee(); + + return 1; +} + +/* free any arch/os-specific resources used by thread, which is now + * defunct. Not called on live threads + */ +int arch_os_thread_cleanup(struct thread *thread) { + return 0; +} + +sigset_t *os_context_sigmask_addr(os_context_t *context) +{ + return &context->sigmask; +} + +void visit_context_registers(void (*proc)(os_context_register_t,void*), + os_context_t *context, void* arg) +{ + // ARM64 general purpose registers X0-X30 and program counter Pc + proc(context->win32_context->X0, arg); + proc(context->win32_context->X1, arg); + proc(context->win32_context->X2, arg); + proc(context->win32_context->X3, arg); + proc(context->win32_context->X4, arg); + proc(context->win32_context->X5, arg); + proc(context->win32_context->X6, arg); + proc(context->win32_context->X7, arg); + proc(context->win32_context->X8, arg); + proc(context->win32_context->X9, arg); + proc(context->win32_context->X10, arg); + proc(context->win32_context->X11, arg); + proc(context->win32_context->X12, arg); + proc(context->win32_context->X13, arg); + proc(context->win32_context->X14, arg); + proc(context->win32_context->X15, arg); + proc(context->win32_context->X16, arg); + proc(context->win32_context->X17, arg); + proc(context->win32_context->X18, arg); + proc(context->win32_context->X19, arg); + proc(context->win32_context->X20, arg); + proc(context->win32_context->X21, arg); + proc(context->win32_context->X22, arg); + proc(context->win32_context->X23, arg); + proc(context->win32_context->X24, arg); + proc(context->win32_context->X25, arg); + proc(context->win32_context->X26, arg); + proc(context->win32_context->X27, arg); + proc(context->win32_context->X28, arg); + proc(context->win32_context->Fp, arg); // X29 Frame Pointer + proc(context->win32_context->Lr, arg); // X30 Link Register + proc(context->win32_context->Pc, arg); // Program Counter +} + +#include "lispregs.h" // This will pull in arm64-lispregs.h via target-lispregs.h + +// ... (rest of the file remains the same until os_context_register_addr) ... + +os_context_register_t * +os_context_register_addr(os_context_t *context, int offset) +{ + if (!context) { + return NULL; + } + if (!context->win32_context) { + return NULL; + } + + // Map physical ARM64 register number (0-31) to CONTEXT structure fields + // The offset parameter is the physical register number from arm64-lispregs.h + + switch (offset) { + case 0: return (os_context_register_t*)&context->win32_context->X0; + case 1: return (os_context_register_t*)&context->win32_context->X1; + case 2: return (os_context_register_t*)&context->win32_context->X2; + case 3: return (os_context_register_t*)&context->win32_context->X3; + case 4: return (os_context_register_t*)&context->win32_context->X4; + case 5: return (os_context_register_t*)&context->win32_context->X5; + case 6: return (os_context_register_t*)&context->win32_context->X6; + case 7: return (os_context_register_t*)&context->win32_context->X7; + case 8: return (os_context_register_t*)&context->win32_context->X8; + case 9: return (os_context_register_t*)&context->win32_context->X9; + case 10: return (os_context_register_t*)&context->win32_context->X10; + case 11: return (os_context_register_t*)&context->win32_context->X11; + case 12: return (os_context_register_t*)&context->win32_context->X12; + case 13: return (os_context_register_t*)&context->win32_context->X13; + case 14: return (os_context_register_t*)&context->win32_context->X14; + case 15: return (os_context_register_t*)&context->win32_context->X15; + case 16: return (os_context_register_t*)&context->win32_context->X16; + case 17: return (os_context_register_t*)&context->win32_context->X17; + case 18: return (os_context_register_t*)&context->win32_context->X18; + case 19: return (os_context_register_t*)&context->win32_context->X19; + case 20: return (os_context_register_t*)&context->win32_context->X20; + case 21: return (os_context_register_t*)&context->win32_context->X21; + case 22: return (os_context_register_t*)&context->win32_context->X22; + case 23: return (os_context_register_t*)&context->win32_context->X23; + case 24: return (os_context_register_t*)&context->win32_context->X24; + case 25: return (os_context_register_t*)&context->win32_context->X25; + case 26: return (os_context_register_t*)&context->win32_context->X26; + case 27: return (os_context_register_t*)&context->win32_context->X27; + case 28: return (os_context_register_t*)&context->win32_context->X28; + case 29: return (os_context_register_t*)&context->win32_context->Fp; // X29 + case 30: return (os_context_register_t*)&context->win32_context->Lr; // X30 + case 31: return (os_context_register_t*)&context->win32_context->Sp; // X31 (SP) + default: + return NULL; + } +} + +os_context_register_t * +os_context_sp_addr(os_context_t *context) +{ + return (os_context_register_t*)&context->win32_context->Sp; +} + +os_context_register_t * +os_context_fp_addr(os_context_t *context) +{ + return (os_context_register_t*)&context->win32_context->Fp; +} + +os_context_register_t * +os_context_lr_addr(os_context_t *context) +{ + return (os_context_register_t*)&context->win32_context->Lr; +} + +os_context_register_t * +os_context_flags_addr(os_context_t *context) +{ + // On ARM64 Windows, the CPSR is typically available as a separate field + // within the CONTEXT structure or can be accessed via an appropriate + // field for flags. Assuming `Cpsr` is the field name. + return (os_context_register_t*)&context->win32_context->Cpsr; +} + + +unsigned long +os_context_fp_control(os_context_t *context) +{ + return context->win32_context->Fpsr | context->win32_context->Fpcr; +} + +void +os_restore_fp_control(os_context_t *context) +{ + /* No-op on ARM64, same as Linux and BSD. FPCR is preserved across exceptions. */ +} + +os_context_register_t * +os_context_float_register_addr(os_context_t *context, int offset) +{ + if (!context) { + return NULL; + } + if (!context->win32_context) { + return NULL; + } + + // ARM64 Windows CONTEXT structure has V[0-31] array for SIMD/FP registers + // Each V register is 128 bits (16 bytes) + // The offset parameter is the physical register number (0-31) + + if (offset >= 0 && offset < 32) { + return (os_context_register_t*)&context->win32_context->V[offset]; + } + + return NULL; +} + +void +os_flush_icache(os_vm_address_t address, os_vm_size_t length) +{ + // Use the Windows API function for flushing instruction cache. + // The first argument is a process handle. For the current process, GetCurrentProcess() can be used. + if (!FlushInstructionCache(GetCurrentProcess(), address, length)) { + // Log an error or handle failure if necessary. + fprintf(stderr, "FlushInstructionCache failed: 0x%lx.\n", GetLastError()); + } +} diff --git a/src/runtime/arm64-win32-os.h b/src/runtime/arm64-win32-os.h new file mode 100644 index 000000000..60154d046 --- /dev/null +++ b/src/runtime/arm64-win32-os.h @@ -0,0 +1,26 @@ +#ifndef _ARM64_WIN32_OS_H +#define _ARM64_WIN32_OS_H + +typedef struct os_context_t { + CONTEXT* win32_context; + sigset_t sigmask; +} os_context_t; + +typedef intptr_t os_context_register_t; + +static inline DWORD NT_GetLastError() { + return GetLastError(); +} + +unsigned long os_context_fp_control(os_context_t *context); +void os_restore_fp_control(os_context_t *context); +os_context_register_t * os_context_fp_addr(os_context_t *context); +os_context_register_t * os_context_sp_addr(os_context_t *context); +os_context_register_t * os_context_lr_addr(os_context_t *context); +os_context_register_t * os_context_flags_addr(os_context_t *context); +os_context_register_t * os_context_register_addr(os_context_t *context, int offset); +os_context_register_t * os_context_float_register_addr(os_context_t *context, int offset); + +#define OS_CONTEXT_PC(context) context->win32_context->Pc + +#endif /* _ARM64_WIN32_OS_H */ diff --git a/src/runtime/coreparse.c b/src/runtime/coreparse.c index ee1339809..ad8e9bb41 100644 --- a/src/runtime/coreparse.c +++ b/src/runtime/coreparse.c @@ -853,7 +853,9 @@ process_directory(int count, struct ndir_entry *entry, break; #endif case IMMOBILE_TEXT_CORE_SPACE_ID: +#ifndef LISP_FEATURE_ARM64 TEXT_SPACE_START = addr; +#endif break; case DYNAMIC_CORE_SPACE_ID: { diff --git a/src/runtime/win32-os.c b/src/runtime/win32-os.c index 849ce3fce..63bc29460 100644 --- a/src/runtime/win32-os.c +++ b/src/runtime/win32-os.c @@ -115,7 +115,14 @@ static void set_seh_frame(void *frame) void alloc_gc_page() { -#ifndef LISP_FEATURE_64_BIT // 64-bit uses the page below the card mark table +#ifdef LISP_FEATURE_ARM64 + // ARM64: safepoint page address is reserved by the address space layout + // (parms.lisp), but needs to be committed. Use MEM_COMMIT only. + gc_assert(VirtualAlloc(GC_SAFEPOINT_PAGE_ADDR, BACKEND_PAGE_BYTES, + MEM_COMMIT, PAGE_READWRITE)); +#elif !defined(LISP_FEATURE_64_BIT) + // 32-bit: reserve and commit safepoint page. + // 64-bit x86-64: allocated as part of static space (extra_above in x86-64-arch.c). gc_assert(VirtualAlloc(GC_SAFEPOINT_PAGE_ADDR, BACKEND_PAGE_BYTES, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE)); #endif @@ -144,16 +151,20 @@ void alloc_gc_page() */ void map_gc_page() { +#if defined(LISP_FEATURE_SB_SAFEPOINT) DWORD oldProt; gc_assert(VirtualProtect((void*) GC_SAFEPOINT_PAGE_ADDR, BACKEND_PAGE_BYTES, PAGE_READWRITE, &oldProt)); +#endif } void unmap_gc_page() { +#if defined(LISP_FEATURE_SB_SAFEPOINT) DWORD oldProt; gc_assert(VirtualProtect((void*) GC_SAFEPOINT_PAGE_ADDR, BACKEND_PAGE_BYTES, PAGE_NOACCESS, &oldProt)); +#endif } uint32_t os_get_build_time_shared_libraries(uint32_t excl_maximum, @@ -735,7 +746,6 @@ void os_protect(os_vm_address_t address, os_vm_size_t length, os_vm_prot_t prot) { DWORD old_prot; - DWORD new_prot = os_protect_modes[prot]; gc_assert(VirtualProtect(address, length, new_prot, &old_prot)|| (VirtualAlloc(address, length, MEM_COMMIT, new_prot) && @@ -750,16 +760,19 @@ extern int internal_errors_enabled; extern void exception_handler_wrapper(); -#ifdef LISP_FEATURE_X86 +#if defined(LISP_FEATURE_X86) #define voidreg(ctxptr,name) ((void*)((ctxptr)->E##name)) -#else +#elif defined(LISP_FEATURE_X86_64) #define voidreg(ctxptr,name) ((void*)((ctxptr)->R##name)) +#else // ARM64 and others. Stub out for now. +#define voidreg(ctxptr,name) (0) #endif static int handle_single_step(os_context_t *ctx) { +#if defined(LISP_FEATURE_X86) || defined(LISP_FEATURE_X86_64) if (!single_stepping) return -1; @@ -768,19 +781,48 @@ handle_single_step(os_context_t *ctx) restore_breakpoint_from_single_step(ctx); return 0; +#else + return -1; +#endif } -#ifdef LISP_FEATURE_UD2_BREAKPOINTS -#define SBCL_EXCEPTION_BREAKPOINT EXCEPTION_ILLEGAL_INSTRUCTION -#define TRAP_CODE_WIDTH 2 -#else -#define SBCL_EXCEPTION_BREAKPOINT EXCEPTION_BREAKPOINT -#define TRAP_CODE_WIDTH 1 +#if defined(LISP_FEATURE_X86) || defined(LISP_FEATURE_X86_64) + #ifdef LISP_FEATURE_UD2_BREAKPOINTS + #define SBCL_EXCEPTION_BREAKPOINT EXCEPTION_ILLEGAL_INSTRUCTION + #define TRAP_CODE_WIDTH 2 + #else + #define SBCL_EXCEPTION_BREAKPOINT EXCEPTION_BREAKPOINT + #define TRAP_CODE_WIDTH 1 + #endif +#elif defined(LISP_FEATURE_ARM64) + // On Windows ARM64, BRK instructions trigger EXCEPTION_ILLEGAL_INSTRUCTION, not EXCEPTION_BREAKPOINT + #define SBCL_EXCEPTION_BREAKPOINT EXCEPTION_ILLEGAL_INSTRUCTION + #define TRAP_CODE_WIDTH 4 // ARM breakpoint instruction is 4 bytes #endif static int handle_breakpoint_trap(os_context_t *ctx, struct thread* self) { +#if defined(LISP_FEATURE_ARM64) + uint32_t trap_instruction = *(uint32_t *)OS_CONTEXT_PC(ctx); + unsigned int trap; + + // Check for BRK instruction format used by SBCL. The high bits are + // 11010100001, which is 0x6a1. + if ((trap_instruction >> 21) == 0x6a1) { + // Extract 8-bit trap code from bits 5..12 (matching sigtrap_handler on Linux) + trap = (trap_instruction >> 5) & 0xFF; + } else { + // Not a recognized SBCL trap, could be a debugger breakpoint + // or a real illegal instruction. Let other handlers deal with it. + return -1; + } + + /* On ARM64, do NOT advance PC past the BRK instruction here. + * The Lisp-side internal-error-args reads the BRK instruction from + * the context PC to decode error number and arguments. + * PC will be advanced by arch_skip_instruction after error handling. */ +#else /* Not ARM64 */ #ifdef LISP_FEATURE_UD2_BREAKPOINTS if (((unsigned short *)OS_CONTEXT_PC(ctx))[0] != 0x0b0f) return -1; @@ -793,14 +835,14 @@ handle_breakpoint_trap(os_context_t *ctx, struct thread* self) /* Now EIP points just after the INT3 byte and aims at the * 'kind' value (eg trap_Cerror). */ unsigned trap = *(unsigned char *)OS_CONTEXT_PC(ctx); +#endif /* Before any other trap handler: gc_safepoint ensures that inner alloc_sap for passing the context won't trap on pseudo-atomic. */ /* Now that there is no alloc_sap, I don't know what happens here. */ if (trap == trap_PendingInterrupt) { - /* Done everything needed for this trap, except EIP - adjustment */ + /* Advance PC past the trap instruction and any trailing data. */ arch_skip_instruction(ctx); thread_interrupted(ctx); return 0; @@ -840,9 +882,9 @@ handle_access_violation(os_context_t *ctx, win32_context->Edi, fault_address, exception_record->ExceptionInformation[0]); -#else +#elif defined(LISP_FEATURE_X86_64) odxprint(pagefaults, - "SEGV. ThSap %p, Eip %p, Esp %p, Esi %p, Edi %p, " + "SEGV. ThSap %p, Rip %p, Rsp %p, Rsi %p, Rdi %p, " "Addr %p Access %d\n", self, win32_context->Rip, @@ -851,11 +893,42 @@ handle_access_violation(os_context_t *ctx, win32_context->Rdi, fault_address, exception_record->ExceptionInformation[0]); +#elif defined(LISP_FEATURE_ARM64) + odxprint(pagefaults, + "SEGV. ThSap %p, Pc %p, Sp %p, " + "Addr %p Access %d\n", + self, + win32_context->Pc, + win32_context->Sp, + fault_address, + exception_record->ExceptionInformation[0]); #endif /* Safepoint pages */ if (fault_address == (void *) GC_SAFEPOINT_TRAP_ADDR) { +#ifdef LISP_FEATURE_C_STACK_IS_CONTROL_STACK + /* x86/x86-64: set_csp_from_context arranges for the conservative + * stack scan to cover the CONTEXT, so register values are found. */ thread_in_lisp_raised(ctx); +#else + /* ARM64: separate control and C stacks. The register context must + * be explicitly saved so that GC can scan Lisp register values. + * (Mirrors handle_safepoint_violation in safepoint.c for Unix.) */ + fake_foreign_function_call(ctx); + thread_in_lisp_raised(ctx); + undo_fake_foreign_function_call(ctx); + /* Scrub stale Lisp frames left on the control stack by SUB-GC. + * undo_fake_foreign_function_call zeroed thread->csp (it IS + * foreign_function_call_active_p on ARM64), so temporarily + * restore it from the interrupt context for scrubbing. */ + { + lispobj *csp = (lispobj*)(uword_t) + (*os_context_register_addr(ctx, reg_CSP)); + access_control_stack_pointer(self) = csp; + scrub_thread_control_stack(self); + access_control_stack_pointer(self) = 0; + } +#endif return 0; } @@ -867,7 +940,15 @@ handle_access_violation(os_context_t *ctx, /* dynamic space */ page_index_t page = find_page_index(fault_address); #ifdef LISP_FEATURE_SOFT_CARD_MARKS - if (page >= 0) lose("should not get access violation in dynamic space %p", fault_address); + if (page >= 0) { + /* With soft card marks, pages are never write-protected, so a + * dynamic space access violation means the page needs committing. + * (Windows reserves the full dynamic space but only commits pages + * on demand or via prepare_pages/gc_alloc_large.) */ + os_commit_memory(PTR_ALIGN_DOWN(fault_address, os_vm_page_size), + os_vm_page_size); + return 0; + } #else if (page != -1 && !PAGE_WRITEPROTECTED_P(page)) { os_commit_memory(PTR_ALIGN_DOWN(fault_address, os_vm_page_size), @@ -913,7 +994,9 @@ signal_internal_error_or_lose(os_context_t *ctx, /* The exception system doesn't automatically clear pending * exceptions, so we lose as soon as we execute any FP * instruction unless we do this first. */ + #if defined(LISP_FEATURE_X86) || defined(LISP_FEATURE_X86_64) asm("fnclex"); +#endif /* We're making the somewhat arbitrary decision that having * internal errors enabled means that lisp has sufficient * marbles to be able to handle exceptions, but exceptions @@ -928,7 +1011,7 @@ signal_internal_error_or_lose(os_context_t *ctx, DX_ALLOC_SAP(exception_record_sap, exception_record); thread_sigmask(SIG_SETMASK, &ctx->sigmask, NULL); -#ifdef LISP_FEATURE_X86_64 +#if defined(LISP_FEATURE_X86_64) asm("fninit"); #endif @@ -1003,6 +1086,7 @@ handle_exception_ex(EXCEPTION_RECORD *exception_record, /* For EXCEPTION_ACCESS_VIOLATION only. */ void *fault_address = (void *)exception_record->ExceptionInformation[1]; + #if defined(LISP_FEATURE_X86) || defined(LISP_FEATURE_X86_64) odxprint(seh, "SEH: rec %p, ctxptr %p, rip %p, fault %p\n" "... code %p, rcx %p, fp-tags %p\n\n", @@ -1013,6 +1097,15 @@ handle_exception_ex(EXCEPTION_RECORD *exception_record, (void*)(intptr_t)code, voidreg(win32_context,cx), win32_context->FloatSave.TagWord); +#else // ARM64 + odxprint(seh, + "SEH: rec %p, ctxptr %p, pc %p, fault %p, code %p\n\n", + exception_record, + win32_context, + (void*)win32_context->Pc, + fault_address, + (void*)(intptr_t)code); +#endif /* This function had become unwieldy. Let's cut it down into * pieces based on the different exception codes. Each exception @@ -1093,7 +1186,7 @@ handle_exception(EXCEPTION_RECORD *exception_record, return handle_exception_ex(exception_record, exception_frame, win32_context, FALSE); } -#ifdef LISP_FEATURE_X86_64 +#if defined(LISP_FEATURE_X86_64) || defined(LISP_FEATURE_ARM64) #define RESTORING_ERRNO() \ int sbcl__lastErrno = errno; \ @@ -1119,7 +1212,11 @@ veh(EXCEPTION_POINTERS *ep) return EXCEPTION_CONTINUE_SEARCH; } +#if defined(LISP_FEATURE_X86_64) DWORD64 rip = ep->ContextRecord->Rip; +#elif defined(LISP_FEATURE_ARM64) + DWORD64 rip = ep->ContextRecord->Pc; +#endif long int code = ep->ExceptionRecord->ExceptionCode; BOOL from_lisp = (rip >= DYNAMIC_SPACE_START && rip < DYNAMIC_SPACE_START+dynamic_space_size) || @@ -1166,7 +1263,7 @@ wos_install_interrupt_handlers handler->next_frame = get_seh_frame(); handler->handler = (void*)exception_handler_wrapper; set_seh_frame(handler); -#else +#elif defined(LISP_FEATURE_X86_64) || defined(LISP_FEATURE_ARM64) static int once = 0; if (!once++) AddVectoredExceptionHandler(1,veh); diff --git a/tests/subr.sh b/tests/subr.sh index 1c115f15c..b6471f17e 100644 --- a/tests/subr.sh +++ b/tests/subr.sh @@ -28,7 +28,12 @@ set -a # export all variables at assignment-time. # quote them (with double quotes), to contend with whitespace. SBCL_HOME="${TEST_SBCL_HOME:-$SBCL_PWD/../obj/sbcl-home}" SBCL_CORE="${TEST_SBCL_CORE:-$SBCL_PWD/../output/sbcl.core}" -SBCL_RUNTIME="${TEST_SBCL_RUNTIME:-$SBCL_PWD/../src/runtime/sbcl}" +# On Windows, the runtime is sbcl.exe; on Unix, it's sbcl +if [ -f "$SBCL_PWD/../src/runtime/sbcl.exe" ]; then + SBCL_RUNTIME="${TEST_SBCL_RUNTIME:-$SBCL_PWD/../src/runtime/sbcl.exe}" +else + SBCL_RUNTIME="${TEST_SBCL_RUNTIME:-$SBCL_PWD/../src/runtime/sbcl}" +fi SBCL_ARGS="${TEST_SBCL_ARGS:---disable-ldb --noinform --no-sysinit --no-userinit --noprint --disable-debugger}"