[llvm-dev] Catching exceptions while unwinding through -fno-exceptions code

Hubert Tong via llvm-dev llvm-dev at lists.llvm.org
Tue Dec 22 21:08:24 PST 2020


On Tue, Dec 22, 2020 at 7:57 PM Everett Maus via llvm-dev <
llvm-dev at lists.llvm.org> wrote:

> So, I've been digging into this a bit more over the past week or so.
>
> I think both options are viable.
>
> A custom personality function is definitely easier (and does seem to be
> necessary for C code); so it may actually be ideal to do a mixed version of
> the proposal (custom personality for C code, normal personality + different
> tables for C++ code and noexcept functions).
>
> The work to re-do how exception tables are emitted for noexcept functions
> is much larger, but does seem like it would work pretty well & give the
> desired behavior.
>
> The general idea of the second approach would be:
> - We add a new attribute (noexcept/haltonunwind/name still in process) to
> LLVM IR.
> - In Clang: by default, noexcept functions receive this attribute.  Also,
> so do all functions in a translation unit compiled with -fno-exceptions +
> -fterminate-exceptions. (So we would have a number of functions with
> "nounwind noexcept" attributes on them or similar.)  This would affect the
> code generation for noexcept functions as well (removing the need for the
> explicit terminate handler block).
> - In LLVM, when emitting unwind tables, if a function that has this
> particular attribute, we only emit regions for explicit try/catch blocks,
> and intentionally leave other areas out of the exception table (but we do
> still emit the default personality function/CFI/LSDA/etc. for the method).
> This leads to there being a "gap" in the table (and for functions compiled
> with -fno-exceptions, this means basically emitting a table that has no
> call sites/ranges in it at all). That then causes __gxx_personality_v0 to
> halt.
>
What is the nature of this "halting"? If it's not calling a
terminate_handler, then the behaviour is not correct to use by default for
noexcept functions.


>
> I'm still trying to figure out if/how this would work for Windows/other
> exception models like SJLG or WASM, though, which adds some complexity to
> this proposal that I'd need to figure out.
>
> Open question about process--now that I've done enough prototyping to
> potentially put together a bit more of a proposal, is it best to just start
> a new thread for discussion of the two options?
>
> Thanks,
> Everett Maus
>
> On Thu, Dec 10, 2020 at 8:44 PM Everett Maus <evmaus at google.com> wrote:
>
>> In short, if you just require the feature to work like a poor man's
>>> sanitizer,
>>> -fno-exceptions -fno-asynchronous-unwind-tables already work.
>>>
>> &
>>
>>> I agree we probably do need a flag for this change in behavior, but I
>>> think this flag is going to be very difficult to name and explain, because
>>> there are currently *different* behaviors depending on the target and
>>> other flags you pass.
>>>
>>
>> I guess my take is that the way it should behave is "If exceptions are
>> disabled on a TU via -fno-exceptions, and this flag is passed to that TU,
>> then we should guarantee that any method in it that an exception handler
>> passes through is guaranteed to exit".
>>
>> I think the complexity of the state of -fno-asynchronous-unwind-tables
>> vs. -fasynchronous-unwind-tables that you highlighted makes an additional
>> case for something that ideally won't be affected by those flags but does
>> work well with -fno-exceptions.
>>
>> That absolutely seems, at least to me, to imply that we need something
>> other than just -fno-asynchronous-unwind-tables (consider that:
>> -fno-asynchronous-unwind-tables + -fno-exceptions does exit, but
>> -fno-asynchronous-unwind-tables + -funwind-tables + -fno-exceptions does
>> not).  It'd be nice to have something that guaranteed this behavior for all
>> translation units compiled with -fno-exceptions (and perhaps implied that
>> unwind tables are set up in the correct way; becoming incompatible with
>> -fno-asynchronous-unwind-tables + -fno-unwind-tables).
>>
>> The other concern I have is the current error message for this behavior
>> from the existing personality function; it'd be good to surface not just
>> "an uncaught exception caused the code to terminate" but that the exception
>> handler had to traverse through code that was not compiled with exceptions
>> in mind (or with Jame's proposal, where we also change noexcept to use
>> this, where we have -fterminate-exceptions + fno-exceptions --> all methods
>> are implied to be noexcept; which might be neater, to be honest.).
>>
>> There should be no compiler-rt side change. __gcc_personality_v0 is in
>>> compiler-rt/libgcc_s
>>> simply because it is used by C :( and it cannot use libsupc++/libc++abi
>>> :)
>>
>> &
>>
>>> There's the additional complication of C code.  Historically, before
>>> -fasynchronous-unwind-tables became a common default, throwing through C
>>> functions would also abort, by default. That's why Glibc builds C functions
>>> that might call user C++ code with -fexceptions (example: qsort) -- that
>>> way it can ensure that unwinding a C++ exception does actually work
>>> properly.
>>
>> Unfortunately, unlike in C++, we don't have a ready-made personality
>>> function we can use for C functions which has "terminate" behavior. The
>>> _gcc_personality_v0 doesn't terminate upon failing to find a LSDA table
>>> entry.
>>
>> This is unfortunate, actually, because compatibility with code being
>> compiled as C and linked with C++ that an exception may pass through is
>> definitely something it'd be good to detect.  The prototype implementation
>> I have using custom personality functions that I have does enable this at
>> the moment by just adding a separate compiler-rt personality function.  I
>> suppose we could also modify __gcc_personality_v0 to handle this case in
>> some way.
>>
>> After doing some prototyping, at least initially I think the following is
>> a reasonable proposal for using  that should meet the initial requirements
>> I'd want for this feature:
>>
>> 1) Adding a new personality routine to libcxxabi (tentatively named
>> "__cxx_noexceptions_terminate_personality_v0") which will exit with a
>> message containing the detail that an exception was passed through code
>> compiled without exception handling, along with exception type (if
>> libcxxabi is compiled without LIBCXXABI_SILENT_TERMINATE; otherwise it
>> would simply invoke std::terminate & exit).
>> 2) Adding a new personality routine to compiler-rt (tentatively named
>> "__noexceptions_terminate_personality_v0") which will exit with a less
>> verbose message, but provide the same 'exit the program with some sort of
>> message' functionality for C code compiled with -fno-exceptions if an
>> exception is passed through it. (E.x. if something is compiled with all
>> methods that are in an extern "C" { ... } block or similar, this would
>> insert the C handler for those methods.)
>> 3) Adding a new flag, -fterminate-exceptions (=strict or =none (the
>> default)), to clang, which when set to 'strict' will add these new
>> personality functions to all methods when -fno-exceptions is also set (and
>> would have no effect otherwise).  I'm opting for an enumeration instead of
>> a boolean flag here as one could imagine wanting to add an intermediate
>> state in the future where it only halts if it would result in cleanups not
>> occurring (but I propose not doing that work in the initial version).  This
>> flag shouldn't change the behavior of code compiled with -fexceptions, and
>> it should default to not affecting the behavior of code compiled with
>> -fno-exceptions.
>>
>> Other notes:
>> - This shouldn't result in changes to functions marked with
>> 'noexcept(true)' unless they are also compiled with -fno-exceptions (in
>> which case they would get the new personality function like all other
>> methods).  The current "add a landing pad to the caller that invokes
>> __clang_call_terminate" behavior would still be present.
>> - I expect that the new personality routine would wind up wanting to call
>> a handler instead of terminating itself (to support the optional
>> demanging).  That would imply also adding a new member
>> "__cxa_noexcept_terminate_handler"  or similar that has its behavior
>> defined behind a compilation flag (like the other handlers in
>> src/cxa_default_handlers.cpp).
>> - I think it's reasonable to defer printing stack information or
>> information about where the exception was thrown/where it encountered a
>> function compiled with -fno-exceptions and this flag until later. (That
>> would potentially be an output message change, though--I don't know what
>> the requirements are around text-output behavior changes. Potentially, I
>> could see just gating that behind further restrictions or a separate
>> compiler flag/etc. since calling out to the symbolizer seems like it might
>> be expensive.)
>> - This seems to have an adverse effect on binary size, but it's less than
>> 5% without optimizations, which is probably OK.
>> - My take is that this sort of feature is likely one that you'd want
>> during test/debug/fuzzing builds, but probably would drop during an
>> optimized build. From that perspective, I'm not as concerned about size
>> bloat or the fact that inlining/optimizations will work less well.
>>
>> Option 2:
>>
>> I'm also going to look at prototyping something like what James is
>> suggesting tomorrow, and I'll write up that as an alternate proposal.
>>
>> It does seem like it'd be a win to clean up the current issue with
>> noexcept functions needing to add landing pads everywhere.  However, one
>> thing I dislike with the current proposal of just leaving the LSDA empty,
>> aside from the fact that it'll be hard to make it work with the C
>> personality function, is that I don't think there's a good way (via just
>> leaving the LSDA missing) to flag he difference between "it's missing from
>> the table because it wasn't compiled in with -fasynchronous-unwind-table or
>> -funwind-table" versus "it was explicitly removed because we want this to
>> be required to halt." I feel like for debugging/etc. purposes, it would
>> probably be good to flag the difference (as the exception would 'seem' like
>> it should be caught to the programmer).
>>
>> Thanks,
>> Everett Maus
>>
>>
>> On Thu, Dec 10, 2020 at 4:05 PM James Y Knight <jyknight at google.com>
>> wrote:
>>
>>>
>>>
>>> On Tue, Dec 8, 2020 at 2:26 PM Everett Maus via llvm-dev <
>>> llvm-dev at lists.llvm.org> wrote:
>>>
>>>> That makes sense.  Really appreciate the feedback, all.
>>>>
>>>> I think the approach I'll look at implementing is probably to:
>>>> - Implement a dedicated 'termination' personality function (in
>>>> compiler-rt?) that does appropriate logging + exit.
>>>>
>>>
>>> I still think we don't actually need to introduce a new personality
>>> function -- just reusing the standard C++ one, instead.
>>>
>>> - Add a flag (-fterminate-exceptions?). This is because this is a very
>>>> clear behavior change, so at least providing an opt-in/opt-out mechanism
>>>> seems important. Possible option: make this an enum, have 'strict' = all
>>>> exceptions crash, 'normal' = exceptions passing through methods that would
>>>> require a cleanup /etc. terminate, none (default) = current behavior where
>>>> things just leak/etc.
>>>>
>>>
>>> I agree we probably do need a flag for this change in behavior, but I
>>> think this flag is going to be very difficult to name and explain, because
>>> there are currently *different* behaviors depending on the target and
>>> other flags you pass. This existing behavior is very inconsistent and
>>> confusing...
>>>
>>> 1. With -fno-asynchronous-unwind-tables -fno-exceptions, no unwind info
>>> is emitted, and therefore, attempting to throw an exception through such a
>>> function already aborts. This is the historically-common behavior.
>>>
>>> 2. With -fasynchronous-unwind-tables -fno-exceptions, unwind info is
>>> emitted for functions, and that unwind info currently specifies that
>>> exceptions can be thrown through such functions transparently.
>>>
>>> To make matters worse, the default of -fasynchronous-unwind-tables is
>>> different per-platform. In Clang, it's enabled for x86-64-linux, and off
>>> for i386-linux and ppc64-linux. And, in GCC, the default value even depends
>>> on other flags, and is customized in some distro's builds of the compiler.
>>> Whether it's on or off for i386-linux depends on whether you specify
>>> -fomit-frame-pointer or not...except that Debian (and maybe other distros?)
>>> have patched the spec file to enable async unwind tables, regardless.
>>>
>>>
>>> - During code generation, when -fno-exceptions is turned on, if
>>>> -fterminate-exceptions was passed, it changes the personality function from
>>>> being not-present to being the dedicated -fno-exceptions termination
>>>> personality function.
>>>>
>>>
>>> We'll also probably want to adjust the inliner, because it otherwise
>>> prohibits inlining between functions with different personality routines
>>> (which will be relevant with cross-TU inlining with LTO).
>>>
>>> Anyways -- what I'd really like to see done here is to BOTH fix the
>>> handling of C++ "noexcept" functions to have minimal overhead, and then
>>> simply use the same mechanism to implement -fno-exceptions. This means we
>>> do in general need to be able to implement this behavior with the
>>> __gxx_personality_v0 personality, because noexcept functions can have
>>> try/catch blocks inside.
>>>
>>> I believe that the current design of noexcept -- which requires explicit
>>> invoke and landingpads saying that they terminate -- should be revisited.
>>> It adds a lot of unnecessary overhead.
>>>
>>> What we need is a way to explicitly declare that we want the
>>> personality-routine's default action to be taken on unwind -- implemented
>>> via leaving a gap in the LSDA table. I think we might want to do this both
>>> as syntax on invoke [e.g. `invoke void @xyz() to label %l unwind default`],
>>> as well as a function attribute declaring that any call in the function
>>> should be treated as having the default action of the personality function.
>>>
>>> "nothrow" would emit this new function attribute in combination with
>>> nounwind. Inlining a function having this attribute into a function not
>>> having the attribute would need to convert `call ...` into `invoke ...
>>> unwind default` -- but since the inliner already has such code, that's not
>>> a big deal.
>>>
>>> Not sure how much binary size balances with other concerns, but it
>>>>> sounds to me that the methods proposed are ones that would result in false
>>>>> positives where unwinding through the frame would have resulted in no
>>>>> action even when compiled with exceptions fully on.
>>>>>
>>>>> Perhaps leaving functions that would otherwise be "transparent" to
>>>>> exception handling alone is already implied?
>>>>>
>>>>
>>>> So I think this is actually not ideal behavior, at least for the use
>>>> case I have in mind.
>>>>
>>>> I think I'd prefer (and the team I partner with would prefer) /any/
>>>> exception passing from code compiled with -fexceptions to code compiled
>>>> with -fno-exceptions to halt & return an error, even if the method wouldn't
>>>> have participated in exception handling (e.x. no classes that need to have
>>>> destructors called, etc.)  I think the most desirable behavior is "the
>>>> program halts if exceptions pass through code compiled without exceptions
>>>> enabled".
>>>>
>>>
>>> I agree that is a desirable behavior.
>>>
>>> There's the additional complication of C code.  Historically, before
>>> -fasynchronous-unwind-tables became a common default, throwing through C
>>> functions would also abort, by default. That's why Glibc builds C functions
>>> that might call user C++ code with -fexceptions (example: qsort) -- that
>>> way it can ensure that unwinding a C++ exception does actually work
>>> properly.
>>>
>>> Unfortunately, unlike in C++, we don't have a ready-made personality
>>> function we can use for C functions which has "terminate" behavior. The
>>> _gcc_personality_v0 doesn't terminate upon failing to find a LSDA table
>>> entry.
>>>
>>
>>
>> --
>> --EJM
>>
>
>
> --
> --EJM
> _______________________________________________
> LLVM Developers mailing list
> llvm-dev at lists.llvm.org
> https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-dev
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20201223/af8c94d1/attachment-0001.html>


More information about the llvm-dev mailing list