Search code examples
c++gccx86djgpp

Throwing C++ exceptions from a hardware exception handler. Why does -fnon-call-exceptions not behave as expected?


I had this funny idea last night, to trap hardware exceptions and throw a C++ exception instead. Thought that might be useful for things like FPU exceptions, which normally either crash, or silently return NaN and then cause unexpected behaviour. A C++ exception would be far more desirable here.

So I've been hacking all morning and finally got it to work. Well, almost. The compiler still doesn't realize that arithmetic operations can now throw C++ exceptions, and will silently discard the try/catch block around it. It does work when the exception occurs in a function.

void throw_exception()
{ 
    throw std::runtime_error("Division by zero!");
}

__attribute__((noinline))
void try_div0()
{
    cout << 1 / 0 << endl;
}

int main()
{
    // this class traps a hardware exception (division by zero, in this case) and calls the supplied lambda function.
    // uh, no, you probably don't want to see the assembly code behind this...
    exception_wrapper div0_exc { 0, [] (exception_frame* frame, bool)
    { 
        if (frame->address.segment != get_cs()) return false;           // only handle exceptions that occured in our own code
        frame->stack.offset -= 4;                                       // sub <fault esp>, 4;
        auto* stack = reinterpret_cast<std::uintptr_t *>(frame->stack.offset); // get <fault esp>
        *stack = frame->address.offset;                                 // mov [<fault esp>], <fault address>;
        frame->address.offset = reinterpret_cast<std::uintptr_t>(throw_exception);  // set return address to throw_exception()
        return true;    // exception handled!
    } };

    try
    {
        // cout << 1 / 0 << endl;   // this throws, as expected, but calls std::terminate().
        try_div0();                 // this exception is caught.
    }
    catch (std::exception& e)
    {
        cout << "oops: " << e.what() << endl;
    }
}

I realize this is an unusual question... but is there any way I could make this work? Some way to tell gcc that exceptions can occur anywhere?

I'm compiling with djgpp which (I believe) uses DWARF exception handling.


edit: I just found gcc flags -fnon-call-exceptions and -fasynchronous-unwind-tables, which appear to be what I'm looking for. But it still doesn't work...


edit: Now using the previously mentioned gcc flags, it does catch when the exception occurs in between two function calls:

inline void nop() { asm(""); } 
    // or { cout << flush; } or something. empty function does not work.

int main()
{
    /* ... */
    try
    {
        nop();
        cout << 1 / 0 << endl;
        nop();
    }
    /* ... */
}

edit: Nested try/catch blocks have the same effect, no exception is caught unless the trapped instruction is preceded by a function call.

inline void nop() { asm(""); }

void try_div(int i)
{
    try
    {
        // this works, catches exception in try_div(0).
        nop();
        cout << 1 / i << endl;
        try_div(i - 1);

        // without the first nop(), calls std::terminate()
        //cout << 1 / i << endl;
        //try_div(i - 1);

        // reverse order, also terminates.
        //if (i != 0) try_div(i - 1);
        //cout << 1 / i << endl;
        //nop();
    }
    catch (std::exception& e)
    {
        cout << "caught in try_div(" << i << "): " << e.what() << endl;
    }
}

int main()
{
    /* ... */

    try
    {
        try_div(4);
    }
    catch (std::exception& e)
    {
        cout << "caught in main(): " << e.what() << endl;
    }
}

edit: I have submitted this as a possible bug in gcc, and reduced my code to a simple test case.


Solution

  • It's been a while, but I finally figured it out... The throwing function needs to be marked as having a signal frame.

    [[gnu::no_caller_saved_registers]]
    void throw_exception()
    {
        asm(".cfi_signal_frame"); 
        throw std::runtime_error("Division by zero!");
    }