[LLVMdev] New EH representation for MSVC compatibility

Kaylor, Andrew
Sat, 16 May 2015 00:27:55 -0000

I like the way this sorts out with regard to funclet code generation.  It feels
very natural for Windows EH, though obviously not as natural for non-Windows
targets and I think it is likely to block some optimizations that are currently
possible with those targets.


> If the unwind label is missing, then control leaves the function after the
EH action is completed. If a function is inlined, EH blocks with missing unwind
labels are wired up to the unwind label used by the inlined call site.

Is this saying that a “missing” unwind label corresponds to telling the runtime
to continue the search at the next frame?

Your example looks wrong in this regard, unless I’m misunderstanding it.  It
looks like any exceptions that aren’t caught in that function will lead to a
terminate call.


> Invokes that are reached after a catchblock without following any unwind
edges must transitively unwind to the first catchend block that the catchblock
unwinds to.

I’m not sure I understand this correctly.  In particular, I’m confused about the
roles of resume and catchend.


> %val = cleanupblock <valty> unwind label %nextaction

Why isn’t this a terminator?  It seems like it performs the same sort of role as
catchblock, except presumably it is always entered.  I suppose that’s probably
the answer to my question, but it strikes me as an ambiguity in the scheme.  The
catchblock instruction is more or less a conditional branch whereas the
cleanupblock is more like a label with a hint as to an unconditional branch that
will happen later.  And I guess that’s another thing that bothers me -- a resume
instruction at the end of a catch implementation means something subtly
different than a resume instruction at the end of a cleanup implementation.




From: Reid Kleckner [mailto:rnk at google.com]
Sent: Friday, May 15, 2015 3:38 PM
To: LLVM Developers Mailing List; Bill Wendling; Nick Lewycky; Kaylor, Andrew
Subject: RFC: New EH representation for MSVC compatibility

After a long tale of sorrow and woe, my colleagues and I stand here before you
defeated. The Itanium EH representation is not amenable to implementing
MSVC-compatible exceptions. We need a new representation that preserves
information about how try-catch blocks are nested.

WinEH background
-------------------------------

Skip this if you already know a lot about Windows exceptions. On Windows, every
exceptional action that you can imagine is a function call. Throwing an
exception is a call. Destructor cleanups and finally blocks are calls to
outlined functions that run the cleanup code. Even catching an exception is
implemented as an outlined catch handler function which returns the address of
the basic block at which normal execution should continue.

This is *not* how Itanium landingpads work, where cleanups and catches are
executed after unwinding and clearing old function frames off the stack. The
transition to a landingpad is *not* like a function call, and this is the only
special control transfer used for Itanium EH. In retrospect, having exactly one
kind of control transfer turns out to be a great design simplification. Go
Itanium!

Instead, all MSVC EH personality functions (x86, x64, ARM) cross (C++, SEH) are
implemented with interval tables that express the nesting levels of various
source constructs like destructors, try ranges, catch ranges, etc. When you
rinse your program through LLVM IR today, this structure is what gets lost.

New information
-------------------------

Recently, we have discovered that the tables for __CxxFrameHandler3 have the
additional constraint that the EH states assigned to a catch body must
immediately follow the state numbers assigned to the try body. The natural
scoping rules of C++ make it so that doing this numbering at the source level is
trivial, but once we go to LLVM IR CFG soup, scopes are gone. If you want to
know exactly what corner cases break down, search the bug database and mailing
lists. The explanations are too long for this RFC.


New representation
------------------------------

I propose adding the following new instructions, all of which (except for
resume) are glued to the top of their basic blocks, just like landingpads. They
all have an optional ‘unwind’ label operand, which provides the IR with a
tree-like structure of what EH action to take after this EH action completes.
The unwind label only participates in the formation of the CFG when used in a
catch block, and in other blocks it is considered opaque, personality-specific
information. If the unwind label is missing, then control leaves the function
after the EH action is completed. If a function is inlined, EH blocks with
missing unwind labels are wired up to the unwind label used by the inlined call
site.

The new representation is designed to be able to represent Itanium EH in case we
want to converge on a single EH representation in LLVM and Clang. An IR pass can
convert these actions to landingpads, typeid selector comparisons, and branches,
which means we can phase this representation in on Windows at first and
experiment with it slowly on other platforms. Over time, we can move the
landingpad conversion lower and lower in the stack until it’s moved into
DwarfEHPrepare. We’ll need to support landingpads at least until LLVM 4.0, but
we may want to keep them because they are the natural representation for
Itanium-style EH, and have a relatively low support burden.

resume
-------------

; Old form still works, still means control is leaving the function.
resume <valty> %val
; New form overloaded for intra-frame unwinding or resuming normal execution
resume <valty> %val, label %nextaction
; New form for EH personalities that produce no value
resume void

Now resume takes an optional label operand which is the next EH action to run.
The label must point to a block starting with an EH action. The various EH
action blocks impose personality-specific rules about what the targets of the
resume can be.

catchblock
---------------

%val = catchblock <valty> [i8* @typeid.int<http://typeid.int>, i32
7, i32* %e.addr]
    to label %catch.int<http://catch.int> unwind label %nextaction

The catchblock is a terminator that conditionally selects which block to execute
based on the opaque operands interpreted by the personality function. If the
exception is caught, the ‘to’ block is executed. If unwinding should continue,
the ‘unwind’ block is executed. Because the catchblock is a terminator, no
instructions can be inserted into a catchblock. The MSVC personality function
requires more than just a pointer to RTTI data, so a variable list of operands
is accepted. For an Itanium personality, only one RTTI operand is needed. The
‘unwind’ label of a catchblock must point to a catchend.

catchendblock
----------------

catchend unwind label %nextaction

The catchend is a terminator that unconditionally unwinds to the next action. It
is merely a placeholder to help reconstruct which invokes were part of the catch
blocks of a try. Invokes that are reached after a catchblock without following
any unwind edges must transitively unwind to the first catchend block that the
catchblock unwinds to. Executing such an invoke that does not transitively
unwind to the correct catchend block has undefined behavior.

cleanupblock
--------------------

%val = cleanupblock <valty> unwind label %nextaction

This is not a terminator, and control is expected to flow into a resume
instruction which indicates which EH block runs next. If the resume instruction
and the unwind label disagree, behavior is undefined.

terminateblock
----------------------

; for noexcept
terminateblock [void ()* @std.terminate] unwind label %nextaction
; for exception specifications, throw(int)
terminateblock [void ()* @__cxa_unexpected,
@typeid.int<http://typeid.int>, ...] unwind label %nextaction

This is a terminator, and the unwind label is where execution will continue if
the program continues execution. It also has an opaque, personality-specific
list of constant operands interpreted by the backend of LLVM. The convention is
that the first operand is the function to call to end the program, and the rest
determine if the program should end.

sehfilterblock?
------------------

One big hole in the new representation is SEH filter expressions. They present a
major complication because they do not follow a stack discipline. Any EH action
is reachable after an SEH filter runs. Because the CFG is so useless for
optimization purposes, it’s better to outline the filter in the frontend and
assume the filter can run during any potentially throwing function call.

MSVC EH implementation strategy
----------------------------------------------

Skim this if you just need the semantics of the representation above, and not
the implementation details.

The new EH block representation allows WinEHPrepare to get a lot simpler. EH
blocks should now look a lot more familiar, they are single entry, multi-exit
regions of code. This is exactly equivalent to a function, and we can call them
funclets. The plan is to generate code for the parent function first, skipping
all exceptional blocks, and then generate separate MachineFunctions for each
subfunction in turn. I repeat, we can stop doing outlining in IR. This was just
a mistake, because I was afraid of grappling with CodeGen.

WinEHPrepare will have two jobs now:
1. Mark down which basic blocks are reachable from which handler. Duplicate any
blocks that are reachable from two handlers until each block belongs to exactly
one funclet, pruning as many unreachable CFG edges as possible.
2. Demote SSA values that are defined in a funclet and used in another funclet.

The instruction selection pass is the pass that builds MachineFunctions from IR
Functions. This is the pass that will be responsible for the split. It will
maintain information about the offsets of static allocas in
FunctionLoweringInfo, and will throw it away when all funclets have been
generated for this function. This means we don’t need to insert framerecover
calls anymore.

Generating EH state numbers for the TryBlockMap and StateUnwindTable is a matter
of building a tree of EH blocks and invokes. Every unwind edge from an invoke or
an EH block represents that the instruction is a child of the target block. If
the unwind edge is empty, it is a child of the parent function, which is the
root node of the tree. State numbers can be assigned by doing a DFS traversal
where invokes are visited before EH blocks, and EH blocks can be visited in an
arbitrary-but-deterministic order that vaguely corresponds to source order.
Invokes are immediately assigned the current state number. Upon visiting an EH
block, the state number is recorded as the “low” state of the block. All invokes
are assigned this state number. The state number is incremented, and each child
EH block is visited, passing in the state number and producing a new state
number. The final state number is returned to the parent node.

Example IR from Clang
----------------------------------------

The C++:

struct Obj { ~Obj(); };
void f(int);
void foo() noexcept {
  try {
    f(1);
    Obj o;
    f(2);
  } catch (int e) {
    f(3);
    try {
      f(4);
    } catch (...) {
      f(5);
    }
  }
}

The IR for __CxxFrameHandler3:

define void @foo() personality i32 (...)* @__CxxFrameHandler3 {
  %e.addr = alloca i32
  invoke void @f(i32 1)
    to label %cont1 unwind label %maycatch.int<http://maycatch.int>
cont1:
  invoke void @f(i32 2)
    to label %cont2 unwind label %cleanup.Obj
cont2:
  call void @~Obj()
  br label %return
return:
  ret void

cleanup.Obj:
  cleanupblock unwind label %maycatch.int<http://maycatch.int>
  call void @~Obj()
  resume label %maycatch.int<http://maycatch.int>

maycatch.int<http://maycatch.int>:
  catchblock void [i8* @typeid.int<http://typeid.int>, i32 7, i32*
%e.addr]
    to label %catch.int<http://catch.int> unwind label %catchend1
catch.int<http://catch.int>:
  invoke void @f(i32 3)
    to label %cont3 unwind label %catchend1
cont3:
  invoke void @f(i32 4)
    to label %cont4 unwind label %maycatch.all
cont4:
  resume label %return

maycatch.all:
  catchblock void [i8* null, i32 0, i8* null]
    to label %catch.all unwind label %catchend2
catch.all:
  invoke void @f(i32 5)
    to label %cont5 unwind label %catchend2
cont5:
  resume label %cont4

catchend2:
  catchendblock unwind label %catchend1

catchend1:
  catchendblock unwind label %callterminate

callterminate:
  terminateblock [void ()* @std.terminate]
}

From this IR, we can recover the original scoped nesting form that the table
formation requires.

I think that covers it. Feedback welcome. :)
-------------- next part --------------
An HTML attachment was scrubbed...
URL:
<http://lists.llvm.org/pipermail/llvm-dev/attachments/20150516/a5f94411/attachment.html>