Skip to content

Instantly share code, notes, and snippets.

@ecatmur
Last active February 4, 2019 08:03
Show Gist options
  • Save ecatmur/8321439b4c8bb5f6851aa55ec2aeecc2 to your computer and use it in GitHub Desktop.
Save ecatmur/8321439b4c8bb5f6851aa55ec2aeecc2 to your computer and use it in GitHub Desktop.
AnyException

Here's a fairly simple program. Can you see the bug? (Aside from using functions with wide contracts, which is a matter of opinion.)

That's right, we failed to invoke any cleanup. That's because when the exception handling mechanism cannot find a handler for a thrown exception, it's implementation-specified whether the stack is unwound. So, if we want to invoke destructors and shut down the program cleanly, we need to wrap main() in a try-catch block.

Oh dear. Now we've managed to invoke destructors and shutdown cleanly, we've lost a heap of useful information about the exception - where previously we had a backtrace and a core file, now all we've got is the type and the explanatory string (what()).

Can we have both? You might say, why not capture information in the exception object? And yes, Boost.Exception allows that, but you're paying a heavy cost in case the exception is expected and will be caught and the information discarded. And that only works if everyone uses Boost.Exception to augment their throw sites; we want to be able to control this from the catch site, e.g. the catch-all block in main(), or at the root of a handler for a client request in a server, allowing the server to return an error to that client and keep running. (Say, like other languages have no problem with.)

But maybe this is possible, with a lot of hacking and a little undefined (and platform-specific) behavior. I'll assume you're all familiar with how exception handling works in the Itanium ABI, so let's just consider the challenge. More or less, we want pass 1 to succeed, so that terminate is not called and pass 2 proceeds to invoke destructors. However, we also want to execute a chunk of user-defined code before pass 2 begins. It turns out there's one (and only one, as far as I'm aware) hookable step: https://github.com/gcc-mirror/gcc/blob/41d6b10e96a1de98e90a7c0378437c3255814b16/libstdc%2B%2B-v3/libsupc%2B%2B/eh_personality.cc#L228 Great! - the x86 EH personality calls a virtual method, __do_catch, on the catch specification's typeinfo to determine whether the catch specification can handle the exception. So, all we need is to create a type with a custom typeinfo -

Wait, what?

Well, under the Itanium ABI, typeinfo objects are polymorphic, which means they have a vtable pointer, which doesn't have to point to the std::typeinfo vtable, it can point to any vtable with the same layout. They live in .rodata, so in order to override the vtable pointer we have to suppress generation of the typeinfo and supply our own, in assembler. Typeinfo - being RTTI - gets emitted along with the vtable when the first virtual member function is defined, so we need to declare a dummy virtual member function and omit to define it. To recap:

  • derive from std::typeinfo,
  • override __do_catch to execute our code and return true,
  • declare a dummy virtual member function and not define it,
  • write assembler to output our own vtable and typeinfo with a custom vptr.

I'm not suggesting that you should use this - although I've used it successfully - but I think it does demonstrate a need for more information available - on request - at a catch site; and also that this is eminently implementable, at least on the Itanium ABI. (I would appreciate pointers on how to implement this within the Windows exception handling system.) Anyone feel like writing a paper?

#include <exception>
#include <iostream>
#include <optional>
std::terminate_handler prev = std::set_terminate([] {
::system("gdb -q --batch -p $PPID -ex where");
prev();
});
struct Greeter {
std::optional<std::string> name;
int result();
};
int Greeter::result() {
std::cout << "Hello, " << name.value() << std::endl;
return 0;
}
struct Cleanup {
~Cleanup() { std::cout << "Did some cleanup" << std::endl; }
};
int main() {
auto guard = Cleanup{};
auto name = std::optional<std::string>{};
auto g = Greeter{name};
return g.result();
}
$ g++ -std=c++2a -ggdb3 prog1.c++ && ./a.out
0x00007f270734c07a in __GI___waitpid (pid=10411, stat_loc=stat_loc@entry=0x7fffdf8e87a0, options=options@entry=0) at ../sysdeps/unix/sysv/linux/waitpid.c:29
29 ../sysdeps/unix/sysv/linux/waitpid.c: No such file or directory.
#0 0x00007f270734c07a in __GI___waitpid (pid=10411, stat_loc=stat_loc@entry=0x7fffdf8e87a0, options=options@entry=0) at ../sysdeps/unix/sysv/linux/waitpid.c:29
#1 0x00007f27072c4fbb in do_system (line=<optimized out>) at ../sysdeps/posix/system.c:148
#2 0x0000000000401b58 in <lambda()>::operator()(void) const (__closure=0x0) at prog1.c++:7
#3 0x0000000000401b72 in <lambda()>::_FUN(void) () at prog1.c++:9
#4 0x00007f2707902a46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007f2707902a81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6 0x00007f2707902cb4 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x0000000000401e49 in std::__throw_bad_optional_access () at /usr/include/c++/8/optional:99
#8 0x0000000000402083 in std::optional<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::value() & (this=0x7fffdf8e8a20) at /usr/include/c++/8/optional:1265
#9 0x0000000000401baf in Greeter::result (this=0x7fffdf8e8a20) at prog1.c++:17
#10 0x0000000000401c3e in main () at prog1.c++:29
[Inferior 1 (process 10410) detached]
terminate called after throwing an instance of 'std::bad_optional_access'
what(): bad optional access
Hello, Aborted (core dumped)
134
#include <exception>
#include <iostream>
#include <optional>
struct Greeter {
std::optional<std::string> name;
int result();
};
int Greeter::result() {
std::cout << "Hello, " << name.value() << std::endl;
return 0;
}
struct Cleanup {
~Cleanup() { std::cout << "Did some cleanup" << std::endl; }
};
int main() {
try {
auto guard = Cleanup{};
auto name = std::optional<std::string>{};
auto g = Greeter{name};
return g.result();
} catch (std::exception& ex) {
std::cerr << "Exception " << typeid(ex).name() << ": " << ex.what() << std::endl;
return 1;
}
}
$ g++ -std=c++2a -ggdb3 prog2.c++ && ./a.out
Hello, Did some cleanup
Exception St19bad_optional_access: bad optional access
1
#include <exception>
#include <iostream>
#include <optional>
#include <typeinfo>
struct AnyExceptionTypeInfo : std::type_info {
bool __do_catch(std::type_info const* thr_type, void** thr_obj, unsigned outer) const override;
};
class AnyException {
virtual int dummy();
};
__asm__(R"(
.weak _ZTV12AnyException
.section .rodata._ZTV12AnyException,"aG",@progbits,_ZTV12AnyException,comdat
.align 8
.type _ZTV12AnyException, @object
.size _ZTV12AnyException, 24
_ZTV12AnyException:
.quad 0
.quad _ZTI12AnyException
.quad 0
.weak _ZTI12AnyException
.section .rodata._ZTI12AnyException,"aG",@progbits,_ZTI12AnyException,comdat
.align 8
.type _ZTI12AnyException, @object
.size _ZTI12AnyException, 16
_ZTI12AnyException:
.quad _ZTV20AnyExceptionTypeInfo+16
.quad _ZTS12AnyException
.weak _ZTS12AnyException
.section .rodata._ZTS12AnyException,"aG",@progbits,_ZTS12AnyException,comdat
.align 8
.type _ZTS12AnyException, @object
.size _ZTS12AnyException, 15
_ZTS12AnyException:
.string "12AnyException"
)");
bool AnyExceptionTypeInfo::__do_catch(std::type_info const* thr_type, void** thr_obj, unsigned outer) const {
::system("gdb -q --batch -p $PPID -ex where -ex gcore");
return true;
}
struct Greeter {
std::optional<std::string> name;
int result();
};
int Greeter::result() {
std::cout << "Hello, " << name.value() << std::endl;
return 0;
}
struct Cleanup {
~Cleanup() { std::cout << "Did some cleanup" << std::endl; }
};
int main() {
try {
auto guard = Cleanup{};
auto name = std::optional<std::string>{};
auto g = Greeter{name};
return g.result();
} catch (AnyException&) {
try {
std::rethrow_exception(std::current_exception());
} catch (std::exception& ex) {
std::cerr << "Exception " << typeid(ex).name() << ": " << ex.what() << std::endl;
return 1;
}
}
}
$ g++-8 -std=c++2a -ggdb3 prog3.c++ && ./a.out
0x00007fa8e654c07a in __GI___waitpid (pid=10509, stat_loc=stat_loc@entry=0x7fffdada9980, options=options@entry=0) at ../sysdeps/unix/sysv/linux/waitpid.c:29
29 ../sysdeps/unix/sysv/linux/waitpid.c: No such file or directory.
#0 0x00007fa8e654c07a in __GI___waitpid (pid=10509, stat_loc=stat_loc@entry=0x7fffdada9980, options=options@entry=0) at ../sysdeps/unix/sysv/linux/waitpid.c:29
#1 0x00007fa8e64c4fbb in do_system (line=<optimized out>) at ../sysdeps/posix/system.c:148
#2 0x0000000000401f93 in AnyExceptionTypeInfo::__do_catch (this=0x403128 <typeinfo for AnyException>, thr_type=0x4031e8 <typeinfo for std::bad_optional_access>, thr_obj=0x7fffdada9b10, outer=1) at prog3.c++:42
#3 0x00007fa8e6b01d65 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4 0x00007fa8e6b02657 in __gxx_personality_v0 () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007fa8e686096b in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#6 0x00007fa8e6b02ca7 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x000000000040236f in std::__throw_bad_optional_access () at /usr/include/c++/8/optional:99
#8 0x00000000004025a9 in std::optional<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::value() & (this=0x7fffdadaa0d0) at /usr/include/c++/8/optional:1265
#9 0x0000000000401fc5 in Greeter::result (this=0x7fffdadaa0d0) at prog3.c++:52
#10 0x0000000000402056 in main () at prog3.c++:65
Saved corefile core.10508
[Inferior 1 (process 10508) detached]
Hello, Did some cleanup
Exception St19bad_optional_access: bad optional access
1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment