crt: Fix mingw-w64 _invoke_watson implementation

Capture CONTEXT registers at the beginning of the function with explicit
compiler memory barrier to prevent compiler from clobbering registers.

Fix filling of Esp/Rsp and Ebp/Rbp registers.

Add explaining comments for most of the function logic.

Co-authored-by: LIU Hao <lh_mouse@126.com>
Signed-off-by: LIU Hao <lh_mouse@126.com>
diff --git a/mingw-w64-crt/misc/_invoke_watson.c b/mingw-w64-crt/misc/_invoke_watson.c
index a7fa505..6bc1e25 100644
--- a/mingw-w64-crt/misc/_invoke_watson.c
+++ b/mingw-w64-crt/misc/_invoke_watson.c
@@ -7,6 +7,11 @@
 #include <corecrt.h>
 #include <windows.h>
 
+/* keep SEH_INLINE_ASM in sync with mingw-w64-crt/crt/crtexe.c */
+#if defined(__SEH__) && (!defined(__clang__) || __clang_major__ >= 7)
+#define SEH_INLINE_ASM
+#endif
+
 #if defined(__i386__)
 DECLSPEC_NOINLINE /* decrease chance of modifying caller's registers and/or stack frame */
 #endif
@@ -26,28 +31,22 @@
 #endif
 }
 
-/* DECLSPEC_NOINLINE is needed for __builtin_return_address() and __builtin_frame_address() usage */
+/* DECLSPEC_NOINLINE is needed for __builtin_return_address(), __builtin_frame_address() and __builtin_dwarf_cfa() usage */
 DECLSPEC_NOINLINE __MINGW_ATTRIB_NORETURN void __cdecl _invoke_watson(const wchar_t * __UNUSED_PARAM(expression), const wchar_t * __UNUSED_PARAM(function_name), const wchar_t * __UNUSED_PARAM(file_name), unsigned int __UNUSED_PARAM(line_number), uintptr_t __UNUSED_PARAM(reserved))
 {
-    EXCEPTION_RECORD exception_record = { 0, };
-    CONTEXT context = { 0, };
-    EXCEPTION_POINTERS exception_pointers = { &exception_record, &context };
-#if defined(__x86_64__)
-    ULONG64 establisher_frame;
-    ULONG64 image_base;
-    PRUNTIME_FUNCTION function_entry;
-    PVOID handler_data;
-#endif
-
     if (is_fastfail_available())
         __fastfail(FAST_FAIL_INVALID_ARG);
 
+    CONTEXT context; /* do not initialize context (to zeros) to prevent using registers and clobbering their content. */
     /*
      * RtlCaptureContext() is available since Windows XP.
      * UCRT runtime uses inline assemly on 32-bit x86.
      * For compatibility with UCRT do same thing.
      */
 #if defined(__i386__)
+    /* Values for context.Eip, context.Esp and context.Ebp are explicitly
+     * not filled because they are overwritten few lines below.
+     */
     asm volatile(
         "mov %%eax, %0\n\t"
         "mov %%ecx, %1\n\t"
@@ -71,34 +70,92 @@
         "=m" (context.SegEs), "=m" (context.SegFs), "=m" (context.SegGs),
         "=m" (context.EFlags)
       :
-      :
+      : "memory" /* compiler barrier */
     );
+    context.ContextFlags = CONTEXT_CONTROL;
 #else
     RtlCaptureContext(&context);
 #endif
 
+    /* Define and fill all other variables after capturing registers. */
+    EXCEPTION_RECORD exception_record = { 0, };
+    EXCEPTION_POINTERS exception_pointers = { &exception_record, &context };
+#if defined(__x86_64__)
+    ULONG64 image_base;
+    PRUNTIME_FUNCTION function_entry;
+#endif
+
     /* Fill additional platform specific fields of the parent (caller) function into context. */
 #if defined(__i386__)
-    context.ContextFlags = CONTEXT_CONTROL;
+    /*
+     * Current stack frame returned by __builtin_frame_address(0) contains
+     * values of caller function: ebp, eip. Therefore ebp of the called
+     * function is dereferenced first value (index 0).
+     */
     context.Eip = (uintptr_t)__builtin_extract_return_addr(__builtin_return_address(0)); /* msvc uses _ReturnAddress() */
-    context.Esp = (uintptr_t)__builtin_frame_address(0); /* msvc uses _AddressOfReturnAddress() */
-    context.Ebp = *((uintptr_t *)__builtin_frame_address(0)-1); /* msvc uses *((ULONG *)_AddressOfReturnAddress()-1) */
+    context.Esp = (uintptr_t)&expression - 4; /* msvc uses _AddressOfReturnAddress() */
+    context.Ebp = ((uintptr_t *)__builtin_frame_address(0))[0]; /* msvc uses *((ULONG *)_AddressOfReturnAddress()-1) */
 #elif defined(__x86_64__)
+#if defined(SEH_INLINE_ASM)
+    /* FIXME: function_entry is pointer to SEH unwind data table for the current
+     * function, which is generated by linker at the link time. Linker should be
+     * able to provide this pointer at link time and therefore it would not be
+     * required to lookup it at runtime.
+     * TODO: Let linker to resolve function_entry at link time and avoid calling
+     * RtlLookupFunctionEntry() function at all.
+     */
     function_entry = RtlLookupFunctionEntry(context.Rip, &image_base, NULL);
-    if (function_entry)
-        RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, context.Rip, function_entry, &context, &handler_data, &establisher_frame, NULL);
-    context.Rip = (uintptr_t)__builtin_extract_return_addr(__builtin_return_address(0)); /* msvc uses _ReturnAddress() */
-    context.Rsp = (uintptr_t)__builtin_frame_address(0); /* msvc uses _AddressOfReturnAddress()+8 */
-    context.Rbp = *((uintptr_t *)__builtin_frame_address(0)-1); /* not filled filled by msvc */
+#else
+    /* SEH unwind data table is not used, so RtlVirtualUnwind() cannot be called. */
+    function_entry = NULL;
+#endif
+    if (function_entry) {
+        /* If possible use SEH unwind data table to fill the context structure
+         * with data from the parent (caller). This will update at least fields:
+         * context.Rip, context.Rsp, context.Rbp
+         */
+        RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, context.Rip, function_entry, &context, &(PVOID){NULL} /*out: HandlerData*/, &(ULONG64){0} /*out: EstablisherFrame*/, NULL);
+    } else {
+        /* If SEH unwind data table is not available then just fix RIP/RSP/RBP.
+         * For gcc we can use __builtin_dwarf_cfa() which returns pointer to the
+         * original rsp with value before caller issued "call" instruction to
+         * our function. Like on x86, current stack frame returned by
+         * __builtin_frame_address(0) contains values of caller function: rbp,
+         * rip. Therefore rbp of the called function is dereferenced first value.
+         * Note that msvc _AddressOfReturnAddress() is address of the rip value
+         * which should be &(((uintptr_t *)__builtin_dwarf_cfa())[-1]) and
+         * therefore below code for context.Rsp should match the msvc behavior.
+         * For clang we cannot use __builtin_dwarf_cfa() as it returns the rsp
+         * value _after_ the function prologue which allocated space for local
+         * variables. This looks like a bug in clang/llvm, which was reported:
+         * https://github.com/llvm/llvm-project/issues/227606
+         * As a workaround calculate that address from the pointer of 5th
+         * function argument which is passed on the stack above the 32 bytes
+         * of shadow space. __builtin_frame_address() has the same problem, so
+         * it is only used to force rbp to be saved right below the rip value.
+         */
+        context.Rip = (uintptr_t)__builtin_extract_return_addr(__builtin_return_address(0)); /* msvc uses _ReturnAddress() */
+#if defined(__clang__)
+        asm volatile("" : : "r" (__builtin_frame_address(0)));
+        context.Rsp = (uintptr_t)&reserved - 32; /* msvc uses _AddressOfReturnAddress()+8 */
+        context.Rbp = ((uintptr_t *)context.Rsp)[-2]; /* not filled by msvc */
+#else
+        context.Rsp = (uintptr_t)__builtin_dwarf_cfa(); /* msvc uses _AddressOfReturnAddress()+8 */
+        context.Rbp = ((uintptr_t *)__builtin_frame_address(0))[0]; /* not filled by msvc */
+#endif
+    }
 #endif
 
     exception_record.ExceptionCode = STATUS_INVALID_CRUNTIME_PARAMETER;
     exception_record.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
     exception_record.ExceptionAddress = __builtin_extract_return_addr(__builtin_return_address(0)); /* msvc uses _ReturnAddress() */
 
-    /* Remove all filters, trigger exception and terminate the process. */
+    /* Remove top level exception filter and throw unhandled exception.
+     * It will break into debugger or directly start Dr. Watson. */
     SetUnhandledExceptionFilter(NULL);
     UnhandledExceptionFilter(&exception_pointers);
+
+    /* In case debugger or Dr. Watson returned back then terminate the process. */
     TerminateProcess(GetCurrentProcess(), STATUS_INVALID_CRUNTIME_PARAMETER);
     __builtin_unreachable();
 }