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(); }