x86-64: add another way to avoid POSIX signals for internal errors

In you case you need to debug code which actually gets a SIGILL,
so UD2 or INTO aren't the best choice of trap instruction.
This doesn't pass the regression suite, but it's enough to help debug
a crashing self-build using gdb.
Add --without-int3-breakpoints in make-config to use this feature.
Works only if #+linux at present.
This commit is contained in:
Douglas Katzman 2023-04-02 11:06:31 -04:00
parent c3dd14b430
commit aa44aa035f
6 changed files with 140 additions and 5 deletions

View file

@ -1,3 +1,4 @@
:int3-breakpoints
:64-bit
:gencgc
:use-cons-region

View file

@ -5,6 +5,33 @@
(in-package "SB-VM")
;;; The SYNCHRONOUS-TRAP routine has nearly the same effect as executing INT3
;;; but is more friendly to gdb. There may be some subtle bugs with regard to
;;; blocking/unblocking of async signals which arrive nearly around the same
;;; time as a synchronous trap.
#+sw-int-avoidance ; "software interrupt avoidance"
(define-assembly-routine (synchronous-trap) ()
(inst pushf)
(inst push rbp-tn)
(inst mov rbp-tn rsp-tn)
(inst and rsp-tn (- 16))
(inst sub rsp-tn 8) ; PUSHing an odd number of GPRs
;; Arrange in the utterly confusing order that a linux signal context has them
;; so that we can memcpy() into a context. Push RBX twice to maintain alignment.
(regs-pushlist rcx rax rdx rbx rbx rsi rdi r15 r14 r13 r12 r11 r10 r9 r8)
;; ^^^ technically this is the slot for RBP
(inst sub rsp-tn (* 16 16))
(dotimes (i 16) (inst movdqa (ea (* i 16) rsp-tn) (sb-x86-64-asm::get-fpr :xmm i)))
(inst lea rdi-tn (ea 24 rbp-tn)) ; stack-pointer at moment of "interrupt"
(inst mov rsi-tn rsp-tn) ; pointer to saved CPU state
(inst call (make-fixup "synchronous_trap" :foreign))
(dotimes (i 16) (inst movdqa (sb-x86-64-asm::get-fpr :xmm i) (ea (* i 16) rsp-tn)))
(inst add rsp-tn (* 16 16))
(regs-poplist rcx rax rdx rbx rbx rsi rdi r15 r14 r13 r12 r11 r10 r9 r8)
(inst mov rsp-tn rbp-tn)
(inst pop rbp-tn)
(inst popf))
(macrolet ((do-fprs (operation regset &aux (displacement 0))
;; The YMM case could be removed now I suppose, since we use XSAVE + XRSTOR
(multiple-value-bind (mnemonic fpr-align)

View file

@ -310,6 +310,17 @@
;; Just print something and go on with life.
(setq sb-xc:*features* (remove :int4-breakpoints sb-xc:*features*))
(warn "Removed :INT4-BREAKPOINTS from target features"))
(when (target-featurep :x86-64)
(let ((int3-enable (target-featurep :int3-breakpoints))
(int4-enable (target-featurep :int4-breakpoints))
(ud2-enable (target-featurep :ud2-breakpoints)))
(when (or ud2-enable int4-enable)
(setq sb-xc:*features* (remove :int3-breakpoints sb-xc:*features*))
(when (and ud2-enable int4-enable)
(error "UD2-BREAKPOINTS and INT4-BREAKPOINTS are mutually exclusive choices")))
(unless (or int3-enable int4-enable ud2-enable)
;; don't love the name, but couldn't think of a better one
(push :sw-int-avoidance sb-xc:*features*))))
(when (or (target-featurep :arm64)
(and (target-featurep :x86-64)
(member :sse4 backend-subfeatures)))

View file

@ -2244,6 +2244,19 @@
;;;; interrupt instructions
;;; The default interrupt instruction is INT3 which signals SIGTRAP.
;;; This makes for a lot of trouble when using gdb to debug lisp, because gdb really wants
;;; to use SIGTRAP for itself. And allegedly there were OSes where SIGTRAP was unreliable
;;; but I have never seen it, other than it being intercepted by gdb.
;;; (Maybe that's what someone meant by "unreliable"?)
;;; So depending on your requirement, SIGILL can be raised instead via either the INTO
;;; instruction which is illegal on amd64, or UD2 for compabitility with 32-bit code.
;;; UD2 is not needed on amd64 but is on 32-bit where INTO is a legal instruction.
;;; However, if trying to debug code which also gets an "actual" SIGILL, this still poses
;;; a problem for gdb. To workaround that we can emit a call to a asm routine which
;;; has essentially the same effect as the signal.
;;; Orthogonal to the preceding choices, INT1 can be used for pseudo-atomic-interrupted
;;; but that doesn't work on all systems.
(define-instruction break (segment &optional (code nil codep))
(:printer byte-imm ((op #xCC)) :default :print-name 'int3 :control #'break-control)
(:printer word-imm ((op #x0B0F)) :default :print-name 'ud2 :control #'break-control)
@ -2251,6 +2264,11 @@
;; use of sigtrap and shortens the error break by 1 byte relative to UD2.
(:printer byte-imm ((op #xCE)) :default :print-name 'into :control #'break-control)
(:emitter
#+sw-int-avoidance ; emit CALL [EA] to skip over the trap instruction
(let ((where (ea (make-fixup 'sb-vm::synchronous-trap :assembly-routine*))))
(emit-prefixes segment where nil :do-not-set)
(emit-byte segment #xFF)
(emit-ea segment where #b010))
#-ud2-breakpoints (emit-byte segment (or #+int4-breakpoints #xCE #xCC))
#+ud2-breakpoints (emit-word segment #x0B0F)
(when codep (emit-byte segment (the (unsigned-byte 8) code)))))

View file

@ -267,9 +267,9 @@ resignal_to_lisp_thread(int signal, os_context_t *context)
/* Not safe in general, but if your thread names are all
* simple-base-string and won't move, this is slightly ok */
__attribute__((unused)) static char* cur_thread_name()
char* vm_thread_name(struct thread* th)
{
struct thread* th = get_sb_vm_thread();
if (!th) return "non-lisp";
struct thread_instance *lispthread =
(void*)(th->lisp_thread - INSTANCE_POINTER_LOWTAG);
struct vector* name = VECTOR(lispthread->name);
@ -375,16 +375,21 @@ sigset_tostring(const sigset_t *sigset, char* result, int result_length)
{
int i;
int len = 0;
for(i = 1; i <= MAX_SIGNUM; i++)
if (!sigset) { strcpy(result,"nil"); return; }
if (*(uint32_t*)sigset == 0xFFFFFFFF) { strcpy(result,"All"); return; }
result[0] = '{';
len = 1;
for (i = 1; i <= MAX_SIGNUM; i++)
if (sigismember(sigset, i)) {
// ensure room for (generously) 3 digits + comma + null, or give up
if (len > result_length - 5) {
strcpy(result, "too many to list");
return;
}
len += sprintf(result+len, "%s%d", len?",":"", i);
len += sprintf(result+len, "%s%d", len>1?",":"", i);
}
result[len] = 0;
result[len] = '}';
result[len+1] = 0;
}

View file

@ -211,3 +211,76 @@ os_flush_icache(os_vm_address_t __attribute__((unused)) address,
// observable to the linker. Any one symbol suffices to resolve all of them.
#include <math.h>
const long libm_anchor = (long)acos;
#ifdef LISP_FEATURE_SW_INT_AVOIDANCE
extern void sigtrap_handler();
extern char* vm_thread_name(struct thread*);
extern void sigset_tostring(const sigset_t*, char*, int);
void synchronous_trap(lispobj* sp_at_interrupt, char* savearea)
{
os_context_t context;
memset(&context, 0, sizeof context);
// Create the signal context from the values pushed on the stack
// by the lisp assembly routine.
context.uc_mcontext.fpregs = &context.__fpregs_mem;
if (sizeof context.uc_mcontext.fpregs->_xmm[0].element != 16) lose("sigcontext size bug");
memcpy(context.uc_mcontext.fpregs->_xmm[0].element, savearea, 16*16);
char* gprsave = savearea + 16*16;
memcpy(context.uc_mcontext.gregs, gprsave, 15*8);
context.uc_mcontext.gregs[REG_RSP] = (greg_t)sp_at_interrupt;
// Take the return-PC to the user code which is 1 word down from exactly where
// the stack-pointer was at the simulated INT3, then add 1 because a real INT
// instructions leaves the PC pointing after it.
long pc_at_interrupt = sp_at_interrupt[-1];
context.uc_mcontext.gregs[REG_RIP] = 1 + pc_at_interrupt;
// The first instruction of the asm routine was to push EFLAGS
context.uc_mcontext.gregs[REG_EFL] = sp_at_interrupt[-2];
// The next instruction was to push RBP
context.uc_mcontext.gregs[REG_RBP] = sp_at_interrupt[-3];
sigset_t curmask;
thread_sigmask(SIG_UNBLOCK, 0, &curmask); // to read the mask
# define REAL_SIGSET_SIZE_BYTES ((NSIG/8))
memcpy(&context.uc_sigmask, &curmask, REAL_SIGSET_SIZE_BYTES);
thread_sigmask(SIG_BLOCK, &blockable_sigset, 0);
sigset_t newmask;
sigorset(&newmask, &blockable_sigset, &curmask);
/* char newmask_string[100];
sigset_tostring(&newmask, newmask_string, sizeof newmask_string);
fprintf(stderr, "[%s]: trap: pc=%lx sp=%p savearea=%p newmask=%s\n",
vm_thread_name(get_sb_vm_thread()),
os_context_pc(&context), sp_at_interrupt, savearea,
newmask_string); */
sigtrap_handler(0, 0, &context);
if (context.uc_mcontext.gregs[REG_RSP] != (greg_t)sp_at_interrupt ||
context.uc_mcontext.gregs[REG_RBP] != (greg_t)sp_at_interrupt[-3])
lose("don't know how return to a different frame\n");
// Handler can alter the return PC which we need to stuff into
// the return PC location that the assembly routine received.
uword_t return_pc = context.uc_mcontext.gregs[REG_RIP];
sp_at_interrupt[-1] = return_pc;
// act like a return-from-signal by restoring the signal mask
// Ideally this would be performed in the asm routine only after restoring
// registers, but it doesn't matter too much.
thread_sigmask(SIG_SETMASK, &context.uc_sigmask, 0);
}
int wrapped_pthread_sigmask(int how, const void* new, void* old)
{
char new_string[80], old_string[80];
sigset_tostring(new, new_string, sizeof new_string);
int res = pthread_sigmask(how, new, old);
sigset_tostring(old, old_string, sizeof old_string);
fprintf(stderr, "[%s]: pthread_sigmask(%s,%s) -> %s\n",
vm_thread_name(get_sb_vm_thread()),
how==SIG_BLOCK?"BLOCK":how==SIG_UNBLOCK?"UNBLOCK":"SETMASK",
new_string, old_string);
return res;
}
#endif