21 ms·
C++ Exceptions: Under the Hood (2013)
- AshamedCaptain 5y ago[2013]ish if I remember. Also note the ABI for C++ exceptions followed by G++ et al is actually documented as part of the Itanium C++ ABI : https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html
- gpderetta 5y agoThe doc is actually incomplete, it refers to implementation defined ABI entry points for the actual unwinding. The actual unwind tables and compensation opcodes are, IIRC, part of the DWARF standard, but last time I looked at it that standard was also incomplete and left a lot unspecified. Many of the details (including bugs that have now become part of the ABI) are basically folklore. IIRC Ian LAnce Taylor had a series of blog posts that did shed light on a lot of these details.
- jcranmer 5y agoThere's essentially three separate moving pieces here. At the bottom layer you have the unwind API. This is actually generally defined by the platform-specific ABI, although the definition here on most Unixes is "we have a .eh_frame section that's basically the same as .debug_frame in DWARF." The API is provided by some platform library (libunwind), and it's language-agnostic. Part of the generic unwind structure is that each stack frame has a personality function and a Language-Specific Data Area, and the unwind semantics will call the personality function to figure out whether to drop off into the stack frame, and where in the function to drop off, or continue unwinding the stack. The personality function itself uses the LSDA to figure out what to do. The personality function lives in the language standard library (e.g., libstdc++), and the LSDA format is of course entirely undocumented (i.e., read Ian Lance Taylor's blog posts as the best documentation that exists). The final level of the ABI is how you actually represent the exceptions themselves. This is provided by the Itanium ABI and describes the names of the functions needed to effect exception handling (e.g., __cxa_begin_catch), the structure layouts involved, and even some nominal data on how to handle foreign exceptions which don't really exist. And that's not entirely true for all systems. ARM notably uses a different ABI exception handling, which is rather close to the standard Itanium exception handling except the details are different. Some operating systems choose to forgo unwinding-based exceptions in lieu of setjmp/longjmp as the standard ABI, which of course requires different versions of everything. And Windows has an entirely different form of exception handling which isn't centered around unwinding but actually calling exception handlers as new functions on the stack, requiring frame links to the original-would-be-unwound-to function.
- nicolasbrailo 5y agoAccording to GitHub, published in February 2013, written during 2012. I feel old now.
- zabzonk 5y agoUnder the hood of GCC specifically.
- cogman10 5y agoDoesn't GCC support multiple exception handling options?
- zabzonk 5y agoI'm not sure what your point is - mine was that the original post is specifically about GCC, not Standard C++.
- deleted 5y ago[deleted]
- cogman10 5y agoMainly that exception handling doesn't have a single solution, even in GCC. I'm agreeing with you that this article only highlights one version of exception handling for one compiler. There's also potential operating system involvement that's not really covered in this article.
- gumby 5y ago“Standard C++” just specifies `throw` and `catch` (plus what happens when you traverse a block, nothrow declaration etc) Every implementation has to do something to actually be standard C, and this is an example. It’s rather similar on other hardware, but as far as the standard goes that is irrelevant.
- mhh__ 5y agoI'm not sure exactly what you mean but there was a switch to DWARF EH ages ago (GCC 2?)
- account42 5y agosjlj is still supported, at least on some platforms, e.g. MinGW.
- monkeycantype 5y agoJust dropping by to say hi to a fellow c++ing monkey
- nicolasbrailo 5y ago:D
- adzm 5y agoImplementation in Windows is quite different. Especially x86 vs x86-64 which has much less overhead. Also gets quite complicated with the Windows built-in Structured Exceptions/ asynchronous exceptions which can be translated into c++ exceptions -- and even more complexity due to different compiler options that handle these!
- WalterBright 5y agoWorking with and implementing C++ exceptions for 30 years now, including implementing exception handling for Windows, DOS extenders, and Posix (all very different), and then re-implementing them for D, I have sadly come to the conclusion that exceptions are a giant mistake. 1. they are very hard to understand all the way down 2. they are largely undocumented in how they're implemented 3. they are slow when thrown 4. they are slow when not thrown 5. it is hard to write exception-safe code 6. very few understand how to write exception-safe code 7. there is no such thing as zero-cost exception handling 8. optimizers just give up trying to do flow analysis in try-exception blocks 9. consider double-fault exceptions - there's something that shows how rotten it is 10. has anyone yet found a legitimate use for throwing an `int`? I have quit using exceptions in my own code, making everything 'nothrow'. I regret propagating exception handling into D. Constructors that may throw are an abomination. Destructors that throw are even worse.
- tux3 5y agoWhat do you think of languages that use sum types for error handling, but can still unwind in a few scenarios? Reasonnable compromise, or should we get rid of all unwinding always? And if so, do we abort() or do we ask users to handle any and all possible errors.
- WalterBright 5y agoI've read the proposals for it. It certainly looks good, yet exception handling looked good 30 years ago, too. I haven't used sum types myself, and often it takes years to discern whether things are really good ideas or not. What I personally use is the "poisoning" technique. This involves marking an object as being in an error state, much like a floating point value can be in a NaN state. Any operation on a poisoned object produces another poisoned object, until eventually this is dealt with at some point in the program. I've had satisfactory success with this technique. It does have a lot of parallels with the sum type method.
- winstonewert 5y agoSo that sounds like the way invalid floating point operations give NaN, and then the NaN propagates everywhere. I've always found this super annoying because its often hard to figure out where the NaN comes from. Does your solution differ from this in a way that's less annoying?
- cecilpl2 5y ago> When the personality function doesn't know what to do it will invoke the default exception handler, meaning that in most cases throwing from a nothrow method will end up calling std::terminate. This is an interesting tidbit that cost me a week of debugging recently - a try/catch block at the top of the call stack wasn't catching an exception. We set up an exception handler that calls main in a try/catch block, so that any thrown exceptions can be caught, processed, and dispatched to our crash-logging system. But destructors are marked nothrow by default. So we had a case where an object was destroyed, and about 10 levels down from its destructor some other system threw an exception, intending it to be caught by the top-level catch block. But during stack unwinding we passed through the nothrow destructor and std::terminate got called before unwinding got to the top-level try/catch.
- jcelerier 5y agoHow can that take a week to debug ? gdb would stop at the std::terminate call in your dtor, and catch throw would allow you to see exactly where the exception was thrown
- cecilpl2 5y agoWell first of all I wasn't using gdb since it's not available on the platform this code was running on. Second, the std::terminate call doesn't get called from the dtor, it gets called from the stdc runtime in the call frame of the throw (with OS code in between). The stack isn't actually unwound at this point, it's more like the stdc runtime is walking up the call stack looking for a landing pad at each frame. Third, I didn't know about how this all worked, so I was trying to piece it all together for the first time. Yes, I saw the throw happen. But the symptom was then that the program just... terminated.
- jcelerier 5y ago> Second, the std::terminate call doesn't get called from the dtor, it gets called from the stdc runtime in the call frame of the throw (with OS code in between). The stack isn't actually unwound at this point, it's more like the stdc runtime is walking up the call stack looking for a landing pad at each frame. what I mean is that, if you run that in a debugger, you're going to see something akin to this when the crash occurs (and have your ide stop exactly where the offending exception was thrown ; I don't even have to set `catch throw` for this to work): https://ibb.co/hK3skvz https://ibb.co/hK3skvz - at least on windows, mac, linux. What platform are you running that does not support gdb at all ? pretty much anything that isn't a PIC16F or Z80 supports it..
- nicolasbrailo 5y agoAuthor here; worth noting this article was written a decade ago, and while the concepts it describes are probably still useful (or so I'm told) the text is starting to show its age. Most notably, the examples are completely broken for x86-64, as I didn't have a 64bit processor when writing this.
- pjmlp 5y agoVery interesting read, however it is under the hood on a specific implementation.
- deleted 5y ago[deleted]