Throwing an exception from within a signal handler

2020-01-27 03:19发布

问题:

We have a library that deals with many aspects of error reporting. I have been tasked to port this library to Linux. When running though my little test suite, one of the tests failed. A simplified version of the test appears below.

// Compiler: 4.1.1 20070105 RedHat 4.1.1-52
// Output: Terminate called after throwing an instance of 'int' abort

#include <iostream>
#include <csignal>
using namespace std;

void catch_signal(int signalNumber)
{
    signal(SIGINT, SIG_DFL);
    throw(signalNumber);
}

int test_signal()
{
    signal(SIGINT, catch_signal);

    try
    {
        raise(SIGINT);
    }
    catch (int &z)
    {
        cerr << "Caught exception: " << z << endl;
    }
    return 0;
}

int main()
{
    try
    {
        test_signal();
    }
    catch (int &z)
    {
        cerr << "Caught unexpected exception: " << z << endl;
    }
    return 0;
}

My expectation is that the Caught exception: message will be displayed. What actually happens is that the program terminates as no catch handler appears to be present for the thrown int.

There are a few questions on SO that seem related. I found a number of Google pages that were related. The 'wisdom' seems to boil down to.

  1. Ya can't throw exceptions from signal handlers, cause the signal handler runs with its own stack, so there are no handlers defined on it.
  2. Ya can throw exceptions from signal handlers, just reconstruct a fake frame on the stack, and you are good to go.
  3. Ya, we do it all the time. It works for me on platform X
  4. Ya, that used to be available with gcc, but doesn't seem to work any more. Try the -fnon-call-exceptions option, maybe that will work

    The code works as expected on our AIX/TRU64/MSVC compiler/environments. It fails in our Linux environment.


I am looking for suggestions to help resolve this issue so the library behavior on Linux will match my other platforms, or some sort or workaround that might achieve the same sort of functionality.
Letting the program core dump on signal, is not a viable option.

回答1:

Signals are totally different than C++ exceptions. You can't use a C++ try/catch block to handle a signal. Specifically, signals are a POSIX concept, not a C++ language concept. Signals are delivered asynchronously to your application by the kernel, whereas C++ exceptions are synchronous events defined by the C++ standard.

You are quite limited in what you can do portably in a POSIX signal handler. A common strategy is to have a global flag of type sig_atomic_t which will be set to 1 in the signal handler, and then possibly longjmp to the appropriate execution path.

See here for help writing proper signal handlers.



回答2:

This code demonstrates a technique which moves the throwing of the exception out of the signal handler into the code. My thanks to Charles for the idea.

#include <iostream>
#include <csignal>
#include <csetjmp>

using namespace std;

jmp_buf gBuffer;        // A buffer to hold info on where to jump to

void catch_signal(int signalNumber)
{
    //signal(SIGINT, SIG_DFL);          // Switch to default handling
    signal(SIGINT, catch_signal);       // Reactivate this handler.

    longjmp             // Jump back into the normal flow of the program
    (
        gBuffer,        // using this context to say where to jump to
        signalNumber    // and passing back the value of the signal.
    );
}


int test_signal()
{
    signal(SIGINT, catch_signal);

    try
    {
        int sig;
        if ((sig = setjmp(gBuffer)) == 0) 
        {
            cout << "before raise\n";
            raise(SIGINT);
            cout << "after raise\n";

        }
        else
        {
            // This path implies that a signal was thrown, and
            // that the setjmp function returned the signal
            // which puts use at this point.

            // Now that we are out of the signal handler it is
            // normally safe to throw what ever sort of exception we want.
            throw(sig);
        }
    }
    catch (int &z)
    {
        cerr << "Caught exception: " << z << endl;
    }

    return 0;
}

int main()
{
    try
    {
        test_signal();
    }
    catch (int &z)
    {
        cerr << "Caught unexpected exception: " << z << endl;
    }
    return 0;
}


回答3:

I would mask all signals in every thread, except one which would wait signals with sigwait (). This thread can handle signals without restriction, e.g. throw exceptions or use other communication mechanisms.



回答4:

Throwing out of a signal handler is probably not a good idea as the stack is not necessairly set up in the same way as for function calls thus unwinding from a signal handler may not work as expected.

Important note must be taken for any register used by the C++ ABI that are saved and re-used by the signal handling mechanism.



回答5:

google g++ option

-fnon-call-exceptions

This is essentially what you want. I think this was developed due to pressure from apple for their OS. I'm not certain how supported it is on LINUX. And I'm not certain if one can catch SIGINT -- but all CPU triggered signals (aeh exceptions) can be caught. Coders needing this feature (and don't care about ideology) should create some pressure on the LINUX developer community so that it will be supported on LINUX as well one day -- after having been supported on Windows since nearly two decades.



回答6:

Here's a potential solution. It's probably rather complex to implement, and certainly at least part of it needs re-implemented per CPU archicture and OS and/or C-library combination:

In the signal handler, the stack contains a saved copy of all the registers of the interrupted code. You can manipulate this to modify program state once the signal handler exits. You'd want to do something like this in the handler:

1) Move the bottom part of the stack down (current stack frame, the CPU state the kernel saved, whatever is required for the handler to return back into the kernel) in memory.

2) In the spare space in the middle of the stack, invent a new stack frame as if some "exception invocation" function had been executing when the signal was raised. This frame should be laid out exactly in the same manner as if the interrupted code had called this function in a normal way.

3) Modify the saved CPU state's PC to point at this "exception invocation" function.

4) Exit the signal handler.

The signal handler will return to the kernel. The kernel will return back to this new stack frame (the "exception invocation" function) instead of the original code. This "exception invocation" function should simply raise whatever exception that you want raised.

There are probably a few details here; e.g.:

1) The "exception invocation" function probably needs to save a bunch of registers onto the stack that it normally wouldn't; i.e. all the callee-saved registers that the interrupted code may have been using. You might need to write (part of?) the "exception invocation" function in assembly to assist here. Perhaps step 2 above could save the registers as part of setting up the stack frame.

2) The signal handler is messing about with the stack. This will confuse compiler-generated code a lot. You probably have to write the exception handler (or just perhaps some function it calls, which would require moving more stack frames) in assembly to make this work.

3) You might need to manually generate some C++ exception handler unwind information so that the C++ exception handling code knows how to unwind the stack out of this "exception invocation" function. If you can write the function in C++, probably not. If you can't, then almost certainly.

4) Probably all kinds of nasty details I've overlooked:-)



回答7:

At least in Ubuntu 16.04 x86-64, throwing out of a signal handler seems to work fine. Whether this is by design (i.e. guaranteed to work, rather than working accidentally somehow), I have not researched. I compiled the program below using g++ -o sig-throw sig-throw.cpp:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern "C" void handler(int sig, siginfo_t *info, void *xxx)
{
    throw "Foo";
}

int main(int argc, char **argv)
{
    struct sigaction sa = {0};

    sa.sa_sigaction = handler;
#if 0
    // To ensure SIGALRM doesn't remain blocked once the signal handler raises
    // an exception, either enable the following, or add enable the sigprocmask
    // logic in the exception handler below.
    sa.sa_flags = SA_NODEFER;
#endif
    sigaction(SIGALRM, &sa, NULL);

    alarm(3);

    try {
        printf("Sleeping...\n");
        sleep(10);
        printf("Awoke\n"); // syscall interrupted
    }
    catch (...) {
        printf("Exception!\n");
#if 1
        // To ensure SIGALRM doesn't remain blocked once the signal handler
        // raises an exception, either enable the following, or add enable
        // SA_NODEFER when registering the signal handler.
        sigset_t sigs_alarm;
        sigemptyset(&sigs_alarm);
        sigaddset(&sigs_alarm, SIGALRM);
        sigprocmask(SIG_UNBLOCK, &sigs_alarm, NULL);
#endif
    }

    alarm(3);

    try {
        printf("Sleeping...\n");
        sleep(10);
        printf("Awoke\n"); // syscall interrupted
    }
    catch (...) {
        printf("Exception!\n");
    }

    return 0;
}

Here's it running:

[swarren@swarren-lx1 sig-throw]$ ./sig-throw 
Sleeping...
Exception!

For reference:

[swarren@swarren-lx1 sig-throw]$ lsb_release -a
...
Description:    Ubuntu 16.04.6 LTS
...

[swarren@swarren-lx1 sig-throw]$ dpkg -l libc6
...
ii  libc6:amd64  2.23-0ubuntu11  amd64  GNU C Library: Shared libraries

[swarren@swarren-lx1 sig-throw]$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609