[cfe-dev] [Exception Handling] Could we mark __cxa_end_catch as nounwind conditionally?

John McCall via cfe-dev cfe-dev at lists.llvm.org
Mon Aug 30 12:43:22 PDT 2021


On 30 Aug 2021, at 2:59, Fāng-ruì Sòng wrote:
> On 2021-08-30, chuanqi.xcq wrote:
>> Hi all,
>>
>>   Let me introduce about the background:
>>   I find that the compiler couldn't mark `foo` as `nounwind` in the 
>> following example:
>>
>> ```
>> void bar();
>> void foo() {
>>    try {
>>        bar();
>>    } catch(...) {}
>> }
>> ```
>>
>>   From my perspective, it is clear that foo wouldn't throw any 
>> exception. So it is natural to me that the compiler could mark foo as 
>> nounwind as an optimization. But it didn't.
>>   This pattern occurs frequently in C++20 coroutine. So I tried to 
>> handle coroutine specially before in: 
>> https://reviews.llvm.org/D108277.
>>   But the all the reviewers strongly suggested that we should handle 
>> this case generally for all of functions instead of coroutines only.
>>
>>   Then when I looked into the details in IR, I found the reason is 
>> that __cxa_end_catch isn't nounwind.
>>   Here is the IR generated:
>>
>> ```
>> ; Function Attrs: mustprogress uwtable
>> define dso_local void @_Z3foov() local_unnamed_addr #0 personality 
>> i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
>>  invoke void @_Z3barv()
>>          to label %5 unwind label %1
>>
>> 1:                                                ; preds = %0
>>  %2 = landingpad { i8*, i32 }
>>          catch i8* null
>>  %3 = extractvalue { i8*, i32 } %2, 0
>>  %4 = tail call i8* @__cxa_begin_catch(i8* %3) #2 ; nounwind
>>  tail call void @__cxa_end_catch()
>>  br label %5
>>
>> 5:                                                ; preds = %0, %1
>>  ret void
>> }
>> ```
>>
>>    I found that if I marked the call to __cxa_end_catch() as 
>> `nounwind`, the foo could be marked as `nounwind`. So I start to 
>> survey why __cxa_end_catch() isn't 'nounwind'.
>>    First is the comment on __cxa_end_catch() in libcxxabi:
>>
>> ```
>> Upon exit for any reason, a handler must call:
>>    void __cxa_end_catch ();
>>
>> This routine can be called for either a native or foreign exception.
>> For a native exception:
>> * Locates the most recently caught exception and decrements its 
>> handler count.
>> * Removes the exception from the caught exception stack, if the 
>> handler count goes to zero.
>> * If the handler count goes down to zero, and the exception was not 
>> re-thrown
>>  by throw, it locates the primary exception (which may be the same as 
>> the one
>>  it's handling) and decrements its reference count. If that reference 
>> count
>>  goes to zero, the function destroys the exception. In any case, if 
>> the current
>>  exception is a dependent exception, it destroys that.
>>
>> For a foreign exception:
>> * If it has been rethrown, there is nothing to do.
>> * Otherwise delete the exception and pop the catch stack to empty.
>> ```
>>
>>   I am not familiar with exception handling. But from the comment 
>> above, it looks like that __cxa_end_catch wouldn't throw.
>>   Then in  clang::ItaniumCXXABI, I found this:
>>
>> ```
>> A cleanup to call __cxa_end_catch.  In many cases, the caught
>> exception type lets us state definitively that the thrown exception
>> type does not have a destructor.  In particular:
>>  - Catch-alls tell us nothing, so we have to conservatively
>>    assume that the thrown exception might have a destructor.
>>  - Catches by reference behave according to their base types.
>>  - Catches of non-record types will only trigger for exceptions
>>    of non-record types, which never have destructors.
>>  - Catches of record types can trigger for arbitrary subclasses
>>    of the caught type, so we have to assume the actual thrown
>>    exception type might have a throwing destructor, even if the
>>    caught type's destructor is trivial or nothrow.
>> ```
>>
>>     It looks like that __cxa_end_catch would throw only if the 
>> exception caught has an destructor which may throw.
>
> Yes...
>
>>     But I think the situation is rare. First as the comment says, an 
>> exception type doesn't have a destructor usually.
>> Then if it has a destructor, it is also rare that it may throw. 
>> Finally, it is a bad practice to throw from destructor which occurs 
>> in catch block.
>>     So I want to provide an option to tell the compiler whether the 
>> exceptions in current project has a may-throw destructor. In this 
>> way, we could optimize the example in the beginning.
>
> From GCC produced .gcc_except_table, it seems that GCC unconditionally
> assumes that __cxa_begin_catch/__cxa_end_catch do not throw. GCC does
> not emit call site records for the region with __cxa_end_catch.
>
> So I think we should unconditionally assume that 
> __cxa_begin_catch/__cxa_end_catch don't throw as well.
> Sent https://reviews.llvm.org/D108905

That is not how this works.  Here’s the path we need to take.

First, as I mentioned in the review, we need to figure out what the 
language standard expects to happen when the destructor for an exception 
throws at the end of a `catch`.  Currently we are essentially treating 
this as undefined behavior, which I think is highly unlikely to be the 
intent of the standard.

If it’s allowed to unwind, then `__cxa_end_catch` can throw, and so 
the second step is to:
(1) fix the bugs in the language runtimes to handle this situation 
correctly and not leak the exception object, and then
(2) fix GCC to stop unconditionally assuming that `__cxa_end_catch` does 
not throw.
We could then add an opt-in flag to globally assume that exception 
destructors never throw as an optimization if that’s really important 
for getting good code.

If it’s supposed to call `std::terminate`, then we need to come to an 
agreement with the language runtimes about whose responsibility it is to 
ensure that the exception is caught and `std::terminate` is called.  
That means opening an issue with the Itanium C++ ABI.  If we decide 
it’s the compiler’s responsibility to pass a noexcept function as 
the exception destructor, then we need to fix Clang to wrap the 
destructor in a noexcept function when it’s not noexcept.  If we 
decide it’s the runtime’s responsibility, we need to fix the 
runtimes to catch this exception and call `std::terminate`.  In either 
of those casess, `__cxa_end_catch` is known not to throw.  We could also 
decide it’s the compiler’s responsibility to call `std::terminate` 
if `__cxa_end_catch` throws, and if so then we have to do that; however, 
I think that would be a bad outcome.

John.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/cfe-dev/attachments/20210830/3054dfaa/attachment.html>


More information about the cfe-dev mailing list