[llvm-dev] [RFC] [Windows SEH] Local_Unwind (Jumping out of a _finally) and -EHa (Hardware Exception Handling)

Ten Tzen via llvm-dev llvm-dev at lists.llvm.org
Fri Apr 3 00:49:12 PDT 2020


First of all, I suggest you take some time to read through the first example in my wiki and understand the expected behavior of jumping-out-of-finally in SEH.
It will make our communication much easier.

More reply inline below; Started with [Ten]

From: Eli Friedman <efriedma at quicinc.com>
Sent: Thursday, April 2, 2020 8:48 PM
To: Ten Tzen <tentzen at microsoft.com>; llvm-dev <llvm-dev at lists.llvm.org>
Cc: Aaron Smith <aaron.smith at microsoft.com>
Subject: [EXTERNAL] RE: [llvm-dev] [RFC] [Windows SEH] Local_Unwind (Jumping out of a _finally) and -EHa (Hardware Exception Handling)

Reply inline.

From: Ten Tzen <tentzen at microsoft.com<mailto:tentzen at microsoft.com>>
Sent: Thursday, April 2, 2020 6:01 PM
To: Eli Friedman <efriedma at quicinc.com<mailto:efriedma at quicinc.com>>; llvm-dev <llvm-dev at lists.llvm.org<mailto:llvm-dev at lists.llvm.org>>
Cc: aaron.smith at microsoft.com<mailto:aaron.smith at microsoft.com>
Subject: [EXT] RE: [llvm-dev] [RFC] [Windows SEH] Local_Unwind (Jumping out of a _finally) and -EHa (Hardware Exception Handling)


Unwinding from SEH's perspective is to invoke outer _finally.

For C++ code, At the end of inner catch-handler, control directly passes back to t10:.

If you have local variables with destructors, it doesn't.  The destructors have to run first.

If you have a local variable with a destructor, clang emits two calls to the destructor: one along the normal path, and one along the unwind path.   The goto jumps to the "normal" path destructor call, it calls the destructor, and then the code jumps from there to the final destination.
[Ten]  right, that is fine. But the code never needs to invoke the outer catch-handler.  For SEH case, a goto out of a _finally needs to invoke outer _finally before landing at target label.
Currently, we do the same thing for SEH: there's a normal path and an unwind path.  We outline the code into a separate function, and call it from both paths.  This is essentially identical to what we do for try/catch.  There isn't any obvious reason we can't extend this to handle goto the same way.  In fact, clang already supports goto across a finally block:

[Ten] that is different.  Another way to look at it; a catch-handler is the end of unwind process (assuming no rethrow).  When it's done, the execution continues from the end of try or from the goto target.  The _finally handler in SEH is different. Before it reaches the except handler, a _finally handler is just ONE step of the unwind process.  Hence, if there is a goto out of in a finally, the remaining (outer) _finally handlers must be invoked to complete the unwinding.

void f(int a);
void f(int ex, int lu, int lu2, int lu3) {
__try {
  __try {
      f(ex);
  } __except (ex){
      if (lu3) goto T;
      f(lu);
  }
} __finally {
    f(lu);
}
T:;
}

(If the goto itself is in a finally block, it currently doesn't work, but that's a relatively minor detail.)

[Ten] it's totally different.  The except handler is NOT a funclet(). It's demoted back to host function. As such, the goto from the except handler to label T is visible to Optimizer.  A _finally is a funclet, i.e., a separate function.  A goto from a separate function is not visible to Optimizer and its effect is not modeled in IR today.

This is not the same as what MSVC implements, but it isn't obviously wrong.  If you're going to write a bunch of new code to implement something else, you need to justify it.

[Ten] the proposed design is not the same as MSVC either.  It's the model developed to leverage and fit with LLVM EH framework.


?  If you call a nounwind function, the invoke will be transformed to a plain call.  And we're likely to infer nounwind in many cases (for example, functions that don't call any other functions).  There isn't any way to stop this currently; I guess we could add one.

For -EHa where HW exception must be handled, nounwind-attribute is ignored (or reset) for callees directly inside a _try.

In other words, you need to mark the calls "volatile".  (You could try to track the region that's inside the try block for transforms that care, but that's more complicated for no benefit.)

[Ten] No, we can just remove the nounwind attribute for call-sites inside a _try, or add one more nohardware-exception-attribute to distinguish them.

Also, even if you block directly removing the unwind edge, passes like IPSCCP could still prove that the edge isn't feasible and reason based on that.  So you really need to block all interprocedural transforms, not just ones that mess with the unwind edge.

[Ten] Yes good point. More work may be still needed to make IPO optimization be aware of Hardware-exception and -EHa.


?  I'm sort of unhappy with the fact that this is theoretically unsound, but maybe the extra effort isn't worthwhile, as long as it doesn't impact any transforms we realistically perform.  How much extra effort it would be sort of depends on what conclusion we reach for the "undefined behavior" part of this, which is really the part I'm more concerned about.

Which part (-EHa or Local_unwind) is theoretically unsound to you?  Could you be more specific what UB problem could arise in this design?

The unsoundness is the possibility of optimizations introducing local variables, which I mentioned before.  The resulting variables won't use volatile load/store operations, so they won't be properly preserved.  Actually, thinking about it a bit more, I'm not sure it's completely theoretical; you could run into trouble with constant hoisting.

[Ten] The purpose of preserving user variables is that when HW exception occurs, users' filter & handler can read the correct and up-to-date global states (recorded in Local/Global/Heap).  Compiler temps need not be preserved.  Not sure I understand const-hoisting problem. Please be more specific.

The UB problem is what I outlined in my very first reply.  You need to define some way that isn't UB to trigger an exception, or else handling the resulting exception is formally meaningless.  If the behavior is undefined, it doesn't matter what happens at that point.

[Ten] A hardware exception occurs mostly due to users' bug, like the dereference of a null pointer or divided-by-0.  When it happens today without -EHa, it causes an unhandled exception fault because users cannot handle it.   With -EHa, user can either handle it or gracefully terminate the program.  Does this answer your question?   Sorry, I don't understand what you are asking for about UB.


-Eli
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20200403/b5bb7bc8/attachment.html>


More information about the llvm-dev mailing list