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

Hi, all,

The intend of this thread is to complete the support for Windows SEH.

Currently there are two major missing features: Jumping out of a _finally and Hardware exception handling.

The document below is my proposed design and implementation to fully support SEH on LLVM.

I have completely implemented this design on a branch in repo: https://github.com/tentzen/llvm-project.

It now passes MSVC’s in-house SEH suite.

Sorry for this long write-up. For better readability, please read it on https://github.com/tentzen/llvm-project/wiki

Special thanks to Joseph Tremoulet for his earlier comments and suggestions.

Note: I just subscribed llvm-dev, probably not in the list yet. So please reply with my email address (tentzen@microsoft.com) explicitly in To-list.

Thanks,

–Ten

Resending; I accidentally dropped llvm-dev.

-Eli

  • For goto in finally, why are you inventing a completely new mechanism for handling this sort of construct? What makes this different from our existing handling of goto out of catch blocks? Maybe there’s something obvious here I’m missing, but it looks like essentially the same problem, and I don’t see any reason why we can’t use the existing solution.

No, no new mechanism is invented. The design employs the existing mechanism to model the third exception path caused by _local_unwind (in addition to normal execution and exception handling flow). In earlier discussion with Joseph, adding second EH edge to InvokeInst was briefly discussed, but was quickly dropped as it’s clearly a long shot.

The extended model intends to solve the third control-flow that doesn’t seem representable today.

Take case #2 of the first example in wiki page as an example,

the control flowing from normal execution of inner _finlly, passing through outer _finally, and landing in $t10 cannot be represented by LLVM IR.

Or could you elaborate how to achieve it? (Bear with me as I’m new in Clang&LLVM world).

  • …In general, UB means the program can do anything.

Sorry, what is UB?

Right we are not modeling HW exception in control-flow as it’s not necessary.

For C++ code, we don’t care about the value in register, local variable, SSA and so on. All we need is that “live local-objects got dtored properly when HW exception is unwound and handled”.

For C code, only those code under _try construct is affected. Agree that making memory accesses there volatile is sub-optimal. But it should not have correctness issue.

In MSVC, there is one less restricted “write-through” concept for memory access inside a _try. But I think the benefit of it is minor and it’s not worth it as the amount of code directly under _try is very small, and usually is not performance critical code.

  • …I don’t want to add another way for unmodeled control flow to break code.

I would really love to hear (and find a way to improve) if there is any place in this design & implementation which is not sound or robust.

Thanks,

–Ten

Reply inline

  • Take your example, replace “_try” with C++ “try”, replace the “_finally” with “catch(….)” with a “throw;” at the end of the catch block, replace the “_except()” with “catch(…)”, and see what clang currently generates. That seems roughly equivalent to what you’re trying to do. Extending this scheme to encompass try/finally seems like it shouldn’t require new datastructures in clang’s AST, or new entrypoints in the C runtime.
  • When a goto in a _finally occurs, we must “unwind” to the target code, not just “jump” to target label

I’m not sure what you’re trying to say here. In the Microsoft ABI, goto out of a catch block also calls into the unwinder. We have to run any destructors, and return from the funclet (catchret/cleanupret).

  • The call inside a _try is an invoke with EH edge. So it’s perfectly modeled.

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.

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.

-Eli

Unwinding from SEH’s perspective is to invoke outer _finally. Take this simple example below:

volatile int* Fault = 0;

try {

try {

*Fault += 1;

}

__finally {

printf(" inner finally: Counter = %d\n\r", ++Counter);

goto t10;

}

__finally {

printf(" outer finally Counter = %d\n\r", ++Counter);

}

printf(" after outer try_finally: Counter = %d\n\r", Counter);

t10:;

Before the control gets to “t10:”, the outer _finally funclet is invoked by runtime. Detailed steps:

Reply inline.

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]

UHi Ten,

Thanks for the writeup and implementation, nice to meet you.

I wonder if it would be best to try to discuss the features separately. My view is that catching hardware exceptions (/EHa) is critical functionality, but it’s not clear to me if local unwind is truly worth implementing. Having looked at the code briefly, it seemed like a large portion of the complexity comes from local unwind. Today, clang crashes on this small example that jumps out of a __finally block, but the intention was to reject the code and avoid implementing the functionality. Clang does, in fact, emit a warning:
$ clang -c t.cpp

t.cpp:7:7: warning: jump out of __finally block has undefined behavior [-Wjump-seh-finally]
goto lu1;
^

Local unwind, in my view, is the user saying, “I wrote __finally, but actually I decided I wanted to catch the exception, so let’s transfer to normal control flow now.” It seems to me that the user already has a way to express this: __except. I know the mapping isn’t trivial and it’s not exactly the same, but it seems feasible to rewrite most uses of local unwind this way.

Can you estimate the prevalence of local unwind? What percent of __finally blocks in your experience use non-local control flow? I see a lot of value in supporting catching hardware exceptions, but if we can avoid carrying over the complexity of this local unwind feature, it seems to me that future generations of compiler engineers will thank us.

Hi, Reid,

Nice to finally meet you😊.

Thank you for reading through the doc and providing insightful feedbacks.

Yes I definitely can separate these two features if it’s more convenient for everyone.

For now, the local_unwind specific changes can be separated and reviewed between these two commits:

git diff 9b48ea90f4c9ae7ef030719d6c0b49b00861cdde 06c81a4b6262445432a4166627b87bf595f5291b

the -EHa changes can be read :

git diff e943329ba00772f96fbc1fe5dec836cfd0707a38 9b48ea90f4c9ae7ef030719d6c0b49b00861cdde

My reply inline below in [Ten] lines.

–Ten