This is an archive of the discontinued LLVM Phabricator instance.

[Windows SEH] Fix abnormal-exits in _try
ClosedPublic

Authored by tentzen on Apr 11 2020, 1:14 AM.

Details

Summary

Per Windows SEH Spec, except _leave, all other early exits of a _try (goto/return/continue/break) are considered abnormal exits. In those cases, the first parameter passes to its _finally funclet should be TRUE to indicate an abnormal-termination.

One way to implement abnormal exits in _try is to invoke Windows runtime _local_unwind() (MSVC approach) that will invoke _dtor funclet where abnormal-termination flag is always TRUE when calling _finally. Obviously this approach is less optimal and is complicated to implement in Clang.

Clang today has a NormalCleanupDestSlot mechanism to dispatch multiple exits at the end of _try. Since _leave (or try-end fall-through) is always Indexed with 0 in that NormalCleanupDestSlot, this fix takes the advantage of that mechanism and just passes NormalCleanupDest ID as 1st Arg to _finally.

Diff Detail

Event Timeline

tentzen created this revision.Apr 11 2020, 1:14 AM
Herald added a project: Restricted Project. · View Herald TranscriptApr 11 2020, 1:14 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript

Needs a testcase in clang/test/CodeGen to verify the generated IR.

I haven't looked at this code in a while, but this looks reasonable.

clang/lib/CodeGen/EHScopeStack.h
164

Please add a trailing comma here.

efriedma added inline comments.Apr 11 2020, 2:06 PM
clang/lib/CodeGen/CGException.cpp
1651

Is just truncating the value really correct? What's the possible range of values stored in getNormalCleanupDestSlot()?

tentzen updated this revision to Diff 256836.Apr 11 2020, 11:51 PM
tentzen marked an inline comment as done.

Per Eli's feedbacks:
(1) a test case (windows-seh-abnormal-exit.c) is added under clang\test\CodeGen directory.
(2) an assert is added to catch the extremely rare case that the number of JumpDests in a function is greater than 255. The max. number of JumpDests is 1 (return) + 2*M + N, where M is the number of for/while-loops and N is the number of Goto targets.

tentzen updated this revision to Diff 256840.Apr 12 2020, 1:00 AM
tentzen marked an inline comment as done.

Add option -fms-extensions in test case

Instead of asserting there are less than 256 cleanup destinations, can you emit an icmp against zero, or something like that?

Instead of asserting there are less than 256 cleanup destinations, can you emit an icmp against zero, or something like that?

I did consider that. but it splits the block and induces one compare and one branch in _try exit sequence for something that will not happen in RWC. And it's hard for Optimizer to deal with it. Optimizer can probably can tail-duplicate each path at the expense of code size which I don't think LLVM would do today. Do you really think it's worth to split the block here? thanks

clang/lib/CodeGen/CGException.cpp
1651

Good question!
Usually the number in NormalCleanupDestSlot is single digit (or at most double-digit) , the totoal number of Jump Destinations (return/goto/break/continue/..) in the function.
I thought about widening 1st arg from CHAR to unsigned, but dropped the idea as it seems unnecessary. Yes, someone can write a code to make more than 255 Jumps in a function, but it’s unlikely to happen in real world code. What do you think?
I can go either way..
For sure, we can add an assert to catch it.

I'm not concerned about the performance implications of whatever approach we take here. In the worst case, an "icmp+zext" corresponds to two extra arithmetic instructions; that's not enough to matter. And I expect usually it'll get optimized away.

I'd prefer to avoid exposing our cleanup numbering to user code, if possible. The Microsoft documentation says it returns 0 or 1.

I'm not concerned about the performance implications of whatever approach we take here. In the worst case, an "icmp+zext" corresponds to two extra arithmetic instructions; that's not enough to matter. And I expect usually it'll get optimized away.

I'd prefer to avoid exposing our cleanup numbering to user code, if possible. The Microsoft documentation says it returns 0 or 1.

OK, will do..
thanks

tentzen updated this revision to Diff 256906.Apr 12 2020, 6:36 PM

Per Eli's suggestion,
Use icmp (and zext) to check the JumpDest index, instead of directly passing the Index to _finally().

This revision is now accepted and ready to land.Apr 13 2020, 10:32 AM
rnk added a comment.Apr 13 2020, 12:22 PM

I like the implementation strategy, but I have one corner case, which is probably worth a test.

clang/lib/CodeGen/CGCleanup.cpp
848

Are we sure this is the right set of predicates? It seems like we'll get the wrong flag in cases like:

  __try {
    mayThrow();
    goto out;  // AbnormalTermination should be 1
  } __finally {
    tearDown();
  }
out:
  return 0;

I think these conditions are designed to avoid the switch when there is no normal fallthrough destination (!HasFallthrough). I see clang emits no switch for this C++ input:

$ cat t.cpp
void mayThrow();
struct Dtor {
  ~Dtor();
};
int f() {
  do {
    Dtor o;
    mayThrow();
    break;
  } while (0);
  return 0;
}

$ clang t.cpp  -S -emit-llvm -fno-exceptions -o - | grep switch
# empty
863–864

This flag is set for any cleanup that needs to switch-out to multiple normal destinations. You have named this an "seh abnormal exit", an exit that is abnormal for the purposes of SEH, but I think it would be clearer to give the flag a more general name.

Based on the existing terminology, maybe (F_)HasBranchAfter would be the closest to what we are looking for?

clang/lib/CodeGen/EHScopeStack.h
184

Please add a comment describing the accessor.

tentzen marked 2 inline comments as done.Apr 13 2020, 4:16 PM
tentzen added inline comments.
clang/lib/CodeGen/CGCleanup.cpp
848

Yes, this test passed.

store i32 3, i32* %cleanup.dest.slot, align 4
%0 = call i8* @llvm.localaddress()
%cleanup.dest = load i32, i32* %cleanup.dest.slot, align 4
%1 = icmp ne i32 %cleanup.dest, 0
%2 = zext i1 %1 to i8
call void @"?fin$0@0@Test84@@"(i8 %2, i8* %0)
%cleanup.dest1 = load i32, i32* %cleanup.dest.slot, align 4
switch i32 %cleanup.dest1, label %unreachable [
  i32 3, label %out
]
863–864

my original implementation in https://github.com/tentzen/llvm-project is still using (F_)HasBranchAfters. it's renamed because I'm afraid it may mis-imply it to Scope.getNumBranchAfters().

Yes, Agree, it should not be SEH specific.
how about F_HasExitSwitch?

tentzen updated this revision to Diff 257849.Apr 15 2020, 2:26 PM

Replace F_HasSehAbnormalExits with F_HasExitSwitch

tentzen updated this revision to Diff 257977.Apr 16 2020, 12:44 AM

remade a patch after re-sync

This revision was automatically updated to reflect the committed changes.