
This changes how LLVM constructs certain data structures that relate to exception handling (EH) on Windows. Specifically this changes how IP2State tables for functions are constructed. The purpose of this change is to align LLVM to the requires of the Windows AMD64 ABI, which requires that the IP2State table entries point to the boundaries between instructions. On most Windows platforms (AMD64, ARM64, ARM32, IA64, but *not* x86-32), exception handling works by looking up instruction pointers in lookup tables. These lookup tables are stored in `.xdata` sections in executables. One element of the lookup tables are the `IP2State` tables (Instruction Pointer to State). If a function has any instructions that require cleanup during exception unwinding, then it will have an IP2State table. Each entry in the IP2State table describes a range of bytes in the function's instruction stream, and associates an "EH state number" with that range of instructions. A value of -1 means "the null state", which does not require any code to execute. A value other than -1 is an index into the State table. The entries in the IP2State table contain byte offsets within the instruction stream of the function. The Windows ABI requires that these offsets are aligned to instruction boundaries; they are not permitted to point to a byte that is not the first byte of an instruction. Unfortunately, CALL instructions present a problem during unwinding. CALL instructions push the address of the instruction after the CALL instruction, so that execution can resume after the CALL. If the CALL is the last instruction within an IP2State region, then the return address (on the stack) points to the *next* IP2State region. This means that the unwinder will use the wrong cleanup funclet during unwinding. To fix this problem, compilers should insert a NOP after a CALL instruction, if the CALL instruction is the last instruction within an IP2State region. The NOP is placed within the same IP2State region as the CALL, so that the return address points to the NOP and the unwinder will locate the correct region. This PR modifies LLVM so that it inserts NOP instructions after CALL instructions, when needed. In performance tests, the NOP has no detectable significance. The NOP is rarely inserted, since it is only inserted when the CALL is the last instruction before an IP2State transition or the CALL is the last instruction before the function epilogue. NOP padding is only necessary on Windows AMD64 targets. On ARM64 and ARM32, instructions have a fixed size so the unwinder knows how to "back up" by one instruction. Interaction with Import Call Optimization (ICO): Import Call Optimization (ICO) is a compiler + OS feature on Windows which improves the performance and security of DLL imports. ICO relies on using a specific CALL idiom that can be replaced by the OS DLL loader. This removes a load and indirect CALL and replaces it with a single direct CALL. To achieve this, ICO also inserts NOPs after the CALL instruction. If the end of the CALL is aligned with an EH state transition, we *also* insert a single-byte NOP. **Both forms of NOPs must be preserved.** They cannot be combined into a single larger NOP; nor can the second NOP be removed. This is necessary because, if ICO is active and the call site is modified by the loader, the loader will end up overwriting the NOPs that were inserted for ICO. That means that those NOPs cannot be used for the correct termination of the exception handling region (the IP2State transition), so we still need an additional NOP instruction. The NOPs cannot be combined into a longer NOP (which is ordinarily desirable) because then ICO would split one instruction, producing a malformed instruction after the ICO call.
210 lines
5.6 KiB
C++
210 lines
5.6 KiB
C++
// REQUIRES: x86-registered-target
|
|
|
|
// RUN: %clang_cl -c --target=x86_64-windows-msvc /EHa -O2 /GS- \
|
|
// RUN: -Xclang=-import-call-optimization \
|
|
// RUN: /clang:-S /clang:-o- -- %s 2>&1 \
|
|
// RUN: | FileCheck %s
|
|
|
|
#ifdef __clang__
|
|
#define NO_TAIL __attribute((disable_tail_calls))
|
|
#else
|
|
#define NO_TAIL
|
|
#endif
|
|
|
|
void might_throw();
|
|
void other_func(int x);
|
|
|
|
void does_not_throw() noexcept(true);
|
|
|
|
extern "C" void __declspec(dllimport) some_dll_import();
|
|
|
|
class HasDtor {
|
|
int x;
|
|
char foo[40];
|
|
|
|
public:
|
|
explicit HasDtor(int x);
|
|
~HasDtor();
|
|
};
|
|
|
|
class BadError {
|
|
public:
|
|
int errorCode;
|
|
};
|
|
|
|
void normal_has_regions() {
|
|
// CHECK-LABEL: .def "?normal_has_regions@@YAXXZ"
|
|
// CHECK: .seh_endprologue
|
|
|
|
// <-- state -1 (none)
|
|
{
|
|
HasDtor hd{42};
|
|
|
|
// <-- state goes from -1 to 0
|
|
// because state changes, we expect the HasDtor::HasDtor() call to have a NOP
|
|
// CHECK: call "??0HasDtor@@QEAA@H@Z"
|
|
// CHECK-NEXT: nop
|
|
|
|
might_throw();
|
|
// CHECK: call "?might_throw@@YAXXZ"
|
|
// CHECK-NEXT: nop
|
|
|
|
// <-- state goes from 0 to -1 because we're about to call HasDtor::~HasDtor()
|
|
// CHECK: call "??1HasDtor@@QEAA@XZ"
|
|
// <-- state -1
|
|
}
|
|
|
|
// <-- state -1
|
|
other_func(10);
|
|
// CHECK: call "?other_func@@YAXH@Z"
|
|
// CHECK-NEXT: nop
|
|
// CHECK: .seh_startepilogue
|
|
|
|
// <-- state -1
|
|
}
|
|
|
|
// This tests a tail call to a destructor.
|
|
void case_dtor_arg_empty_body(HasDtor x)
|
|
{
|
|
// CHECK-LABEL: .def "?case_dtor_arg_empty_body@@YAXVHasDtor@@@Z"
|
|
// CHECK: jmp "??1HasDtor@@QEAA@XZ"
|
|
}
|
|
|
|
int case_dtor_arg_empty_with_ret(HasDtor x)
|
|
{
|
|
// CHECK-LABEL: .def "?case_dtor_arg_empty_with_ret@@YAHVHasDtor@@@Z"
|
|
// CHECK: .seh_endprologue
|
|
|
|
// CHECK: call "??1HasDtor@@QEAA@XZ"
|
|
// CHECK-NOT: nop
|
|
|
|
// The call to HasDtor::~HasDtor() should NOT have a NOP because the
|
|
// following "mov eax, 100" instruction is in the same EH state.
|
|
|
|
return 100;
|
|
|
|
// CHECK: mov eax, 100
|
|
// CHECK: .seh_startepilogue
|
|
// CHECK: .seh_endepilogue
|
|
// CHECK: .seh_endproc
|
|
}
|
|
|
|
int case_noexcept_dtor(HasDtor x) noexcept(true)
|
|
{
|
|
// CHECK: .def "?case_noexcept_dtor@@YAHVHasDtor@@@Z"
|
|
// CHECK: call "??1HasDtor@@QEAA@XZ"
|
|
// CHECK-NEXT: mov eax, 100
|
|
// CHECK: .seh_startepilogue
|
|
return 100;
|
|
}
|
|
|
|
void case_except_simple_call() NO_TAIL
|
|
{
|
|
does_not_throw();
|
|
}
|
|
// CHECK-LABEL: .def "?case_except_simple_call@@YAXXZ"
|
|
// CHECK: .seh_endprologue
|
|
// CHECK-NEXT: call "?does_not_throw@@YAXXZ"
|
|
// CHECK-NEXT: nop
|
|
// CHECK-NEXT: .seh_startepilogue
|
|
// CHECK: .seh_endproc
|
|
|
|
void case_noexcept_simple_call() noexcept(true) NO_TAIL
|
|
{
|
|
does_not_throw();
|
|
}
|
|
// CHECK-LABEL: .def "?case_noexcept_simple_call@@YAXXZ"
|
|
// CHECK: .seh_endprologue
|
|
// CHECK-NEXT: call "?does_not_throw@@YAXXZ"
|
|
// CHECK-NEXT: nop
|
|
// CHECK-NEXT: .seh_startepilogue
|
|
// CHECK: .seh_endepilogue
|
|
// CHECK-NEXT: ret
|
|
// CHECK-NEXT: .seh_endproc
|
|
|
|
// This tests that the destructor is called right before SEH_BeginEpilogue,
|
|
// but in a function that has a return value. Loading the return value
|
|
// counts as a real instruction, so there is no need for a NOP after the
|
|
// dtor call.
|
|
int case_dtor_arg_calls_no_throw(HasDtor x)
|
|
{
|
|
does_not_throw(); // no NOP expected
|
|
return 100;
|
|
}
|
|
// CHECK-LABEL: .def "?case_dtor_arg_calls_no_throw@@YAHVHasDtor@@@Z"
|
|
// CHECK: .seh_endprologue
|
|
// CHECK: "?does_not_throw@@YAXXZ"
|
|
// CHECK-NEXT: nop
|
|
// CHECK: "??1HasDtor@@QEAA@XZ"
|
|
// CHECK-NEXT: mov eax, 100
|
|
// CHECK: .seh_startepilogue
|
|
// CHECK: .seh_endproc
|
|
|
|
// Check the behavior of CALLs that are at the end of MBBs. If a CALL is within
|
|
// a non-null EH state (state -1) and is at the end of an MBB, then we expect
|
|
// to find an EH_LABEL after the CALL. This causes us to insert a NOP, which
|
|
// is the desired result.
|
|
void case_dtor_runs_after_join(int x) {
|
|
// CHECK-LABEL: .def "?case_dtor_runs_after_join@@YAXH@Z"
|
|
// CHECK: .seh_endprologue
|
|
|
|
// <-- EH state -1
|
|
|
|
// ctor call does not need a NOP, because it has real instructions after it
|
|
HasDtor hd{42};
|
|
// CHECK: call "??0HasDtor@@QEAA@H@Z"
|
|
// CHECK-NEXT: nop
|
|
// CHECK: test
|
|
|
|
// <-- EH state transition from -1 0
|
|
if (x) {
|
|
might_throw(); // <-- NOP expected (at end of BB w/ EH_LABEL)
|
|
// CHECK: call "?might_throw@@YAXXZ"
|
|
// CHECK-NEXT: nop
|
|
} else {
|
|
other_func(10); // <-- NOP expected (at end of BB w/ EH_LABEL)
|
|
// CHECK: call "?other_func@@YAXH@Z"
|
|
// CHECK-NEXT: nop
|
|
}
|
|
does_not_throw();
|
|
// <-- EH state transition 0 to -1
|
|
// ~HasDtor() runs
|
|
|
|
// CHECK: .seh_endproc
|
|
|
|
// CHECK: "$ip2state$?case_dtor_runs_after_join@@YAXH@Z":
|
|
// CHECK-NEXT: .long [[func_begin:.Lfunc_begin([0-9]+)@IMGREL]]
|
|
// CHECK-NEXT: .long -1
|
|
// CHECK-NEXT: .long [[tmp1:.Ltmp([0-9]+)]]@IMGREL
|
|
// CHECK-NEXT: .long 0
|
|
// CHECK-NEXT: .long [[tmp2:.Ltmp([0-9]+)]]@IMGREL
|
|
// CHECK-NEXT: .long -1
|
|
}
|
|
|
|
|
|
// Check the behavior of NOP padding around tail calls.
|
|
// We do not expect to insert NOPs around tail calls.
|
|
// However, the first call (to other_func()) does get a NOP
|
|
// because it comes before .seh_startepilogue.
|
|
void case_tail_call_no_eh(bool b) {
|
|
// tail call; no NOP padding after JMP
|
|
if (b) {
|
|
does_not_throw();
|
|
// <-- no NOP here
|
|
return;
|
|
}
|
|
|
|
other_func(20);
|
|
// <-- NOP does get inserted here
|
|
}
|
|
// CHECK-LABEL: .def "?case_tail_call_no_eh@@YAX_N@Z"
|
|
// CHECK: test
|
|
// CHECK-NEXT: je .LBB
|
|
// CHECK: jmp "?does_not_throw@@YAXXZ"
|
|
// CHECK-SAME: TAILCALL
|
|
// CHECK-NEXT: .LBB
|
|
// CHECK-NEXT: mov ecx, 20
|
|
// CHECK-NEXT: jmp "?other_func@@YAXH@Z"
|
|
// CHECK-SAME: TAILCALL
|
|
// CHECK-NEXT: # -- End function
|