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

Fāng-ruì Sòng via llvm-dev llvm-dev at lists.llvm.org
Tue Dec 22 21:30:13 PST 2020


On Tue, Dec 22, 2020 at 9:08 PM Hubert Tong via llvm-dev <
llvm-dev at lists.llvm.org> wrote:
>
> 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.

The default action of the personality (i.e. when LSDA exists but PC belongs
to a gap in the LSDA). For __gxx_personality_v0, it is __cxa_begin_catch +
std::terminate (for a foreign exception the std::get_terminate() handler).

A function attribute is definitely useful - which can give us the GCC
eh_must_not_throw size optimization benefits. I haven't thought a lot about
whether `invoke ... unwind default` (default action for invoke) is needed.
I may be a bit conservative on whether a new personality should be
introduced.

>>
>>
>> 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
>
> _______________________________________________
> 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/20201222/03461035/attachment.html>


More information about the llvm-dev mailing list