Re: Expected behavior when returning from a SIGFPE handler
> On May 27, 2021, at 6:17 AM, Jason Thorpe wrote: > >> >> On May 27, 2021, at 3:35 AM, Taylor R Campbell >> wrote: >> >>> Date: Wed, 26 May 2021 19:46:57 -0700 >>> From: Jason Thorpe >>> >>> The test program sets up a SIGFPE handler, and the handler make a >>> copy of the siginfo, sets a global flag, and returns. The program >>> then does "1.0 / 0.0" and prints the result. It checks to ensure >>> that the DZE exception is set via fpgetsticky(). It then does "1.0 >>> / 0.0" again, and then verifies that the SIGFPE handler was not >>> called a second time (because I never cleared DZE with >>> fpsetsticky()). >> >> This strikes me as wrong. >> >> The status flags (fpgetsticky, fetestexcept) don't determine whether a >> floating-point operation `signals an exception' (in the language of >> IEEE 754-2019); they only record whether it happened in the past. >> >> It seems to me that if an operation [ieee754-]signals an exception >> that the user has asked (with fpsetmask/feenableexcept) to be trapped, >> then it should deliver a [unix-]signal, irrespective of whether some >> past operation already [ieee754-]signalled an exception. > > I agree. I was describing behavior the alpha port already had. I will write > a unit test for this behavior (specifically round DZE), or make sure that > there is one that covers it already (there may be … I’m still peeling the > onion on the alpha port…) Ok, circling back on this point, the behavior I described for the alpha port is also how amd64 behaves, which is to say “if the an exception is enabled and the exception’s flag is already set, then the signal handler will not be called on a second triggering of the exceptional condition”. Consider this test program: #include #include #include #include volatile float f_zero = 0.0; siginfo_t siginfo_copy; jmp_buf sigfpe_env; volatile float f_result; static void sigfpe_action(int sig, siginfo_t *info, void *ctx) { siginfo_copy = *info; longjmp(sigfpe_env, 1); } static void __noinline divide_by_zero(void) { f_result = 1.0 / f_zero; } int main(int argc, char *argv[]) { struct sigaction sigact = { .sa_sigaction = sigfpe_action, .sa_flags = SA_SIGINFO, }; (void) sigaction(SIGFPE, , NULL); fp_except_t mask = fpgetmask(); printf("default MASK:\n"); if (mask & FP_X_INV) printf("\tFP_X_INV\n"); if (mask & FP_X_DZ) printf("\tFP_X_DZ\n"); if (mask & FP_X_OFL) printf("\tFP_X_OFL\n"); if (mask & FP_X_UFL) printf("\tFP_X_UFL\n"); if (mask & FP_X_IMP) printf("\tFP_X_IMP\n"); #ifdef FP_X_IOV if (mask & FP_X_IOV) printf("\tFP_X_IOV\n"); #endif fpsetmask(FP_X_DZ); if (setjmp(sigfpe_env)) { printf("SIGFPE 1 received: signo=%d code=%d\n", siginfo_copy.si_signo, siginfo_copy.si_code); } else { divide_by_zero(); printf("1: 1.0 / f_zero -> %f\n", f_result); } fp_except_t sticky = fpgetsticky(); if (sticky & FP_X_DZ) { printf(“1: GOT FP_X_DZ!\n"); } if (setjmp(sigfpe_env)) { printf("SIGFPE 2 received: signo=%d code=%d\n", siginfo_copy.si_signo, siginfo_copy.si_code); } else { divide_by_zero(); printf("2: 1.0 / f_zero -> %f\n", f_result); } sticky = fpgetsticky(); if (sticky & FP_X_DZ) { printf("2: GOT FP_X_DZ!\n"); } return 0; } Running this program on amd64 results in: the-ripe-vessel:thorpej 50$ ./fptest default MASK: SIGFPE 1 received: signo=8 code=3 2: 1.0 / f_zero -> inf 2: GOT FP_X_DZ! the-ripe-vessel:thorpej 51$ …which seems very counter-intuitive to me. I enable FP_X_DZ, I get the SIGFPE signal the first time, and fpgetsticky() does NOT indicate FP_X_DZ … yet I do not get the second SIGFPE, and I get FP_X_DZ from the second call to fpgetsticky()? WTF is going on here? -- thorpej
Re: Expected behavior when returning from a SIGFPE handler
> On May 27, 2021, at 6:29 AM, Jason Thorpe wrote: > > >> On May 27, 2021, at 3:35 AM, Taylor R Campbell >> wrote: >> >> The default exception handling defined in IEEE 754-2019 precisely >> defines what the results of the operation should be, so there's no >> semantic ambiguity about what the program should observe when it >> proceeds on return from the signal handler. > > Hm, I guess this means that I really need to do software-completion for DZE > faults, as well, even if SWC was not explicitly requested for it (we already > do that for INV faults to handle QNaNs). Otherwise, I’m going to be stuck > with UNPREDICTABLE as the result, and that’s not great. …and while we’re at it… The fenv(3) man suggests that if you have a SIGFPE handler set up in your program with exceptions enabled, and you do a: feholdexcept(); /* do stuff that might raise an exception. */ feupdateenv(); …that the feupdateenv() call would result in the SIGFPE being delivered to the handler. Not sure if we have unit tests for that stuff at the moment, but I’m pretty sure the alpha will not do that correctly as it stands (the fenv implementation for alpha appears to have been lifted from a FreeBSD implementation, and it interacts with the FPCR directly, rather than the architecturally mandated FP_C virtual register). -- thorpej
Re: Expected behavior when returning from a SIGFPE handler
>> In some cases, you want to re-execute the instruction. A simple >> example is "FPU disabled" on architectures that have such a notion, >> eg for lazy FPU switching. > Weâ??re not talking about lazy FPU switching hereâ?? weâ??re talking about p$ (Or timing. Yes.) A better example might be turning on denormalized-result exceptions (does 754 specify them? I'm fairly sure I've seen at least one FPU that's documented to support them) with a handler that handles them by setting a global flag, disabling the exception, and re-executing the instruction, so as to note that a denormal occurred but otherwise proceeding with the computation. >> On others, like the VAX, [advancing over an instruction is] a right >> pain to do in software. > Iâ??m not particularly concerned about VAX in this case; it doesnâ??t > have IEEE 754 floating point, so all bets are off :-) 68k, then, or x86 - anything with 754 FP and CISCy enough to have variable-sized instructions. The VAX just happens to be the CISC architecture I know best. /~\ The ASCII Mouse \ / Ribbon Campaign X Against HTMLmo...@rodents-montreal.org / \ Email! 7D C8 61 52 5D E7 2D 39 4E F1 31 3E E8 B3 27 4B
Re: Expected behavior when returning from a SIGFPE handler
> On May 27, 2021, at 3:35 AM, Taylor R Campbell > wrote: > > The default exception handling defined in IEEE 754-2019 precisely > defines what the results of the operation should be, so there's no > semantic ambiguity about what the program should observe when it > proceeds on return from the signal handler. Hm, I guess this means that I really need to do software-completion for DZE faults, as well, even if SWC was not explicitly requested for it (we already do that for INV faults to handle QNaNs). Otherwise, I’m going to be stuck with UNPREDICTABLE as the result, and that’s not great. -- thorpej
Re: Expected behavior when returning from a SIGFPE handler
> On May 26, 2021, at 8:14 PM, Mouse wrote: > >> But the x86_64 code appears to return to the same instruction, banging its h$ > >> It's my belief that the alpha behavior is more desirable. > >> Please, discuss. > > I could argue that either way. > > In some cases, you want to re-execute the instruction. A simple > example is "FPU disabled" on architectures that have such a notion, eg > for lazy FPU switching. We’re not talking about lazy FPU switching here… we’re talking about performing an FP operation that causes an exception per the IEEE 754 rules. The traps generated in these two situations are easily distinguishable, and user-space code has no visibility into lazy FPU switching traps (modulo the sorts of speculative execution information leaks that modern CPUs are vulnerable to). This is all about what to do AFTER the user-specified signal handler has been called, and it returns to normal program flow via the sigreturn path. > In some cases, you don't want to. An example might be soft-float, or > partial soft-float (such as, emulation of cases the hardware doesn't > handle). As I mentioned, on the Alpha, the PALcode pushes a trap frame with the PC pointing at least one instruction *after* the one that triggered the arithmetic exception (on EV6, it’s the instruction immediately following, on pre-EV6, you have to go looking backwards in the instruction stream for it). > On architectures like SPARC or, I think - you'd know better than I - > Alpha, where there are very few possible instruction sizes, sometimes > as few as just one, advancing past the instruction in the trap handler > is easy even if you have to do it in software. On others, like the > VAX, it's a right pain to do in software. On the latter sort, ideally, > the hardware would give you both the PC to use to re-execute the > instruction and the PC to use to skip the instruction, letting the trap > handler choose, but I'm not aware of any architecture that does that. > (The closest I'm aware of is, ironically, the SPARC, on which advancing > past an instruction in software is about as simple as it gets - but it > has both PC and next-PC in hardware, though admittedly for other > reasons.) > > I see no clear single right answer here. I’m not particularly concerned about VAX in this case; it doesn’t have IEEE 754 floating point, so all bets are off :-) -- thorpej
Re: Expected behavior when returning from a SIGFPE handler
> On May 27, 2021, at 3:35 AM, Taylor R Campbell > wrote: > >> Date: Wed, 26 May 2021 19:46:57 -0700 >> From: Jason Thorpe >> >> The test program sets up a SIGFPE handler, and the handler make a >> copy of the siginfo, sets a global flag, and returns. The program >> then does "1.0 / 0.0" and prints the result. It checks to ensure >> that the DZE exception is set via fpgetsticky(). It then does "1.0 >> / 0.0" again, and then verifies that the SIGFPE handler was not >> called a second time (because I never cleared DZE with >> fpsetsticky()). > > This strikes me as wrong. > > The status flags (fpgetsticky, fetestexcept) don't determine whether a > floating-point operation `signals an exception' (in the language of > IEEE 754-2019); they only record whether it happened in the past. > > It seems to me that if an operation [ieee754-]signals an exception > that the user has asked (with fpsetmask/feenableexcept) to be trapped, > then it should deliver a [unix-]signal, irrespective of whether some > past operation already [ieee754-]signalled an exception. I agree. I was describing behavior the alpha port already had. I will write a unit test for this behavior (specifically round DZE), or make sure that there is one that covers it already (there may be … I’m still peeling the onion on the alpha port…) >> The alpha code has, for a very long time, always advanced the PC >> past the faulting instruction on an arithmetic trap[1]. This, in >> essence, makes it behave exactly as if the exception were disabled, >> while still giving the handler a chance to "do something"). >> >> But the x86_64 code appears to return to the same instruction, >> banging its head against the proverbial wall. >> >> It's my belief that the alpha behavior is more desirable. > > I agree. It would be perfectly reasonable to use a SIGFPE handler to, > say, record a history of the instructions (and perhaps stack traces) > that signalled floating-point exceptions, to give more precise > diagnostic information about where they're happening than the status > flags do -- without otherwise interrupting the flow of the program. > > The default exception handling defined in IEEE 754-2019 precisely > defines what the results of the operation should be, so there's no > semantic ambiguity about what the program should observe when it > proceeds on return from the signal handler. Ok, I will write a unit test that verifies this behavior. Thanks! -- thorpej
Re: Expected behavior when returning from a SIGFPE handler
> Date: Wed, 26 May 2021 19:46:57 -0700 > From: Jason Thorpe > > The test program sets up a SIGFPE handler, and the handler make a > copy of the siginfo, sets a global flag, and returns. The program > then does "1.0 / 0.0" and prints the result. It checks to ensure > that the DZE exception is set via fpgetsticky(). It then does "1.0 > / 0.0" again, and then verifies that the SIGFPE handler was not > called a second time (because I never cleared DZE with > fpsetsticky()). This strikes me as wrong. The status flags (fpgetsticky, fetestexcept) don't determine whether a floating-point operation `signals an exception' (in the language of IEEE 754-2019); they only record whether it happened in the past. It seems to me that if an operation [ieee754-]signals an exception that the user has asked (with fpsetmask/feenableexcept) to be trapped, then it should deliver a [unix-]signal, irrespective of whether some past operation already [ieee754-]signalled an exception. > The alpha code has, for a very long time, always advanced the PC > past the faulting instruction on an arithmetic trap[1]. This, in > essence, makes it behave exactly as if the exception were disabled, > while still giving the handler a chance to "do something"). > > But the x86_64 code appears to return to the same instruction, > banging its head against the proverbial wall. > > It's my belief that the alpha behavior is more desirable. I agree. It would be perfectly reasonable to use a SIGFPE handler to, say, record a history of the instructions (and perhaps stack traces) that signalled floating-point exceptions, to give more precise diagnostic information about where they're happening than the status flags do -- without otherwise interrupting the flow of the program. The default exception handling defined in IEEE 754-2019 precisely defines what the results of the operation should be, so there's no semantic ambiguity about what the program should observe when it proceeds on return from the signal handler.
Re: Expected behavior when returning from a SIGFPE handler
> But the x86_64 code appears to return to the same instruction, banging its h$ > It's my belief that the alpha behavior is more desirable. > Please, discuss. I could argue that either way. In some cases, you want to re-execute the instruction. A simple example is "FPU disabled" on architectures that have such a notion, eg for lazy FPU switching. In some cases, you don't want to. An example might be soft-float, or partial soft-float (such as, emulation of cases the hardware doesn't handle). On architectures like SPARC or, I think - you'd know better than I - Alpha, where there are very few possible instruction sizes, sometimes as few as just one, advancing past the instruction in the trap handler is easy even if you have to do it in software. On others, like the VAX, it's a right pain to do in software. On the latter sort, ideally, the hardware would give you both the PC to use to re-execute the instruction and the PC to use to skip the instruction, letting the trap handler choose, but I'm not aware of any architecture that does that. (The closest I'm aware of is, ironically, the SPARC, on which advancing past an instruction in software is about as simple as it gets - but it has both PC and next-PC in hardware, though admittedly for other reasons.) I see no clear single right answer here. /~\ The ASCII Mouse \ / Ribbon Campaign X Against HTMLmo...@rodents-montreal.org / \ Email! 7D C8 61 52 5D E7 2D 39 4E F1 31 3E E8 B3 27 4B
Re: Expected behavior when returning from a SIGFPE handler
On Wed, 26 May 2021, Jason Thorpe wrote: ... The alpha code has, for a very long time, always advanced the PC past the faulting instruction on an arithmetic trap[1]. This, in essence, makes it behave exactly as if the exception were disabled, while still giving the handler a chance to "do something"). But the x86_64 code appears to return to the same instruction, banging its head against the proverbial wall. It's my belief that the alpha behavior is more desirable. I agree. To me, the x86_64 behaviour as reported sounds quite "sub- optimal" and rather unuseful. ++--+---+ | Paul Goyette | PGP Key fingerprint: | E-mail addresses: | | (Retired) | FA29 0E3B 35AF E8AE 6651 | p...@whooppee.com | | Software Developer | 0786 F758 55DE 53BA 7731 | pgoye...@netbsd.org | ++--+---+
Expected behavior when returning from a SIGFPE handler
I've been working on fixing some test case failures on the Alpha port, and I'm elbow-deep in FP-land right now. The Alpha has a somewhat complicated FP story because it has architecture-mandated software completion for essentially anything outside the happy path in hardware, and to support that it has a virtual floating point control register called "FP_C" that is completely orthogonal to the hardware floating point control register (called "FPCR"). (Don't get me started on the "trap shadow"...) I wrote a reduced test case program to exercise the different code paths around the DZE exception. I can run this program in 2 different modes: DZE traps disabled (the default), DZE traps enabled. The test program sets up a SIGFPE handler, and the handler make a copy of the siginfo, sets a global flag, and returns. The program then does "1.0 / 0.0" and prints the result. It checks to ensure that the DZE exception is set via fpgetsticky(). It then does "1.0 / 0.0" again, and then verifies that the SIGFPE handler was not called a second time (because I never cleared DZE with fpsetsticky()). In the "disabled" case, the test program performs the same way on x86_64 and alpha (modulo the result of dividing by zero... on x86_64 I consistently get +inf, and on alpha sometimes I get +inf, sometimes I get 0; the AARM officially states that the results are UNPREDICTABLE). In the "enabled" case, the alpha and x86_64 behavior differ! On the alpha, the program progresses all the way to the end. But on x86_64, it hangs... spinning around the SIGFPE handler. The alpha code has, for a very long time, always advanced the PC past the faulting instruction on an arithmetic trap[1]. This, in essence, makes it behave exactly as if the exception were disabled, while still giving the handler a chance to "do something"). But the x86_64 code appears to return to the same instruction, banging its head against the proverbial wall. It's my belief that the alpha behavior is more desirable. Please, discuss. [1] The PALcode, in fact, does this when delivering the exception. The alpha FP trap handler has to do some shenanigans to find the actual faulting PC. These shenanigans are simple (PC += 4) for EV6 and later CPUs, and somewhat complicated for pre-EV6 (which has to deal with the "trap shadow"). In any case, even when software completion is not possible because trap shadow rules were violated by either the program-writer or the compiler, you're pretty much guaranteed to not execute the faulting instruction again. -- thorpej