HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
debugger_win32_impl.hpp
1// Copyright Take Vos 2021-2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
8#include "../macros.hpp"
9#include "../win32/win32.hpp"
10#include "console_win32.hpp"
11#include "exception.hpp"
12#include "debugger_intf.hpp"
13#include "debugger_utils.hpp"
14#include "misc.hpp"
15#include <exception>
16#include <print>
17#include <cstdio>
18#include <chrono>
19#include <bit>
20#include <format>
21#include <thread>
22#include <filesystem>
23
24hi_export_module(hikogui.utility.debugger : impl);
25
26hi_warning_push();
27// C6320: Exception-filter expression is the constant EXCEPTION_EXECUTE_HANDLER.
28// This might mask exceptions that were not intended to be handled.
29hi_warning_ignore_msvc(6320);
30
31hi_export namespace hi {
32inline namespace v1 {
33
34namespace detail {
35
37 DWORD dwSize;
38 DWORD dwProcessorArchitecture;
39 DWORD dwThreadID;
40 DWORD dwReserved0;
41 ULONG64 lpExceptionAddress;
42 ULONG64 lpExceptionRecord;
43 ULONG64 lpContextRecord;
44};
45
46inline JIT_DEBUG_INFO jit_debug_info = {};
47
48inline HANDLE jit_debug_handle = NULL;
49
58inline bool launch_jit_debugger() noexcept
59{
60 using namespace std::literals;
61
62 auto debugger_enabled = win32_RegGetValue<std::string>(
63 HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug", "Auto");
64 if (not debugger_enabled or *debugger_enabled != "1") {
65 // JIT debugger was not configured or disabled.
66 return false;
67 }
68
69 auto debugger = win32_RegGetValue<std::string>(
70 HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug", "Debugger");
71 if (not debugger) {
72 // JIT debugger was not configured.
73 return false;
74 }
75
76 auto executable_name = win32_GetModuleFileName();
77 if (not executable_name) {
78 // Could not get executable name.
79 return false;
80 }
81
82 auto executable_is_excluded = win32_RegGetValue<uint32_t>(
83 HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug\\AutoExclusionList", executable_name->filename().string());
84 if (executable_is_excluded and *executable_is_excluded == 1) {
85 // This executable was excluded.
86 return false;
87 }
88
89 auto const num_arguments = count(*debugger, "%ld") + count(*debugger, "%p");
90 auto const cmd_line_fmt = replace(replace(*debugger, "%ld", "{}"), "%p", "{:x}");
91
92 auto const process_id = GetCurrentProcessId();
93
94 if (num_arguments >= 2 and jit_debug_handle == NULL) {
95 if (auto handle = win32_CreateEvent()) {
96 jit_debug_handle = *handle;
97
98 } else {
99 set_debug_message("Could not create event object for JIT debugger.");
101 }
102 }
103
104 auto cmd_line = [&] {
105 try {
106 if (num_arguments == 1) {
107 return std::vformat(cmd_line_fmt, std::make_format_args(process_id));
108
109 } else if (num_arguments == 2) {
110 auto const jit_debug_handle_ = win32_HANDLE_to_int(jit_debug_handle);
111 return std::vformat(cmd_line_fmt, std::make_format_args(process_id, jit_debug_handle_));
112
113 } else if (num_arguments == 3) {
114 auto const jit_debug_handle_ = win32_HANDLE_to_int(jit_debug_handle);
115 auto const jit_debug_info_ = std::bit_cast<uintptr_t>(&jit_debug_info);
116 return std::vformat(cmd_line_fmt, std::make_format_args(process_id, jit_debug_handle_, jit_debug_info_));
117
118 } else {
119 set_debug_message("JIT debugger accepts an invalid number of arguments.");
121 }
122 } catch (...) {
124 }
125 }();
126
127 // Start debugger process
128 auto startup_info = STARTUPINFOW{};
129 startup_info.cb = sizeof(STARTUPINFOW);
130
131 auto process_info = win32_CreateProcess(
132 std::nullopt, // application name
133 cmd_line, // command line
134 nullptr, // process attributes
135 nullptr, // thread attributes
136 false, // inherit handles
137 0, // creation flags
138 nullptr, // environment
139 std::nullopt, // current directory
140 startup_info); // process info
141
142 if (not process_info) {
143 set_debug_message("Could not executed JIT debugger.");
145 }
146
147 auto const debugger_is_attached = [&] {
148 auto end_time = std::chrono::utc_clock::time_point::max();
149 while (std::chrono::utc_clock::now() < end_time) {
150 if (IsDebuggerPresent()) {
151 // The debugger is attached.
152 return true;
153 }
154
155 // Block until the JIT debug selection application returns.
156 if (end_time == std::chrono::utc_clock::time_point::max()) {
157 auto exit_code = win32_GetExitCodeProcess(process_info->hProcess);
158
159 if (exit_code and *exit_code == 0) {
160 // The user selected a debugger, we will wait upto 60 second.
161 end_time = std::chrono::utc_clock::now() + 60s;
162
163 } else if (exit_code and *exit_code != 0) {
164 // User pressed "cancel".
165 return false;
166
167 } else if (exit_code.error() != win32_error::status_pending) {
168 // The JIT debug process has not yet exited.
169 set_debug_message("GetExitCodeProcess() return unknown error");
171 }
172 }
173
175 }
176
177 set_debug_message("Debugger did not attach within 60s after being selected.");
179 }();
180
181 // Close handles created by CreateProcess.
182 CloseHandle(process_info->hThread);
183 CloseHandle(process_info->hProcess);
184
185 return debugger_is_attached;
186}
187
188[[nodiscard]] constexpr std::string to_string(EXCEPTION_POINTERS const &ep) noexcept
189{
190 auto r = std::string{};
191
192 // clang-format off
193 switch (ep.ExceptionRecord->ExceptionCode) {
194 case static_cast<DWORD>(STATUS_ASSERTION_FAILURE): r += "Assertion Failure"; break;
195 case static_cast<DWORD>(EXCEPTION_ACCESS_VIOLATION): r += "Access Violation"; break;
196 case static_cast<DWORD>(EXCEPTION_ARRAY_BOUNDS_EXCEEDED): r += "Array Bounds Exceeded"; break;
197 case static_cast<DWORD>(EXCEPTION_BREAKPOINT): r += "Breakpoint"; break;
198 case static_cast<DWORD>(EXCEPTION_DATATYPE_MISALIGNMENT): r += "Datatype Misalignment"; break;
199 case static_cast<DWORD>(EXCEPTION_FLT_DENORMAL_OPERAND): r += "Floating Point Denormal Operand"; break;
200 case static_cast<DWORD>(EXCEPTION_FLT_DIVIDE_BY_ZERO): r += "Floating Point Divide by Zero"; break;
201 case static_cast<DWORD>(EXCEPTION_FLT_INEXACT_RESULT): r += "Floating Point Inexact Result"; break;
202 case static_cast<DWORD>(EXCEPTION_FLT_INVALID_OPERATION): r += "Floating Point Invalid Operation"; break;
203 case static_cast<DWORD>(EXCEPTION_FLT_OVERFLOW): r += "Floating Point Overflow"; break;
204 case static_cast<DWORD>(EXCEPTION_FLT_STACK_CHECK): r += "Floating Point Stack Check"; break;
205 case static_cast<DWORD>(EXCEPTION_FLT_UNDERFLOW): r += "Floating Point Underflow"; break;
206 case static_cast<DWORD>(EXCEPTION_ILLEGAL_INSTRUCTION): r += "Illegal Instruction"; break;
207 case static_cast<DWORD>(EXCEPTION_IN_PAGE_ERROR): r += "In Page Error"; break;
208 case static_cast<DWORD>(EXCEPTION_INT_DIVIDE_BY_ZERO): r += "Integer Divide By Zero"; break;
209 case static_cast<DWORD>(EXCEPTION_INT_OVERFLOW): r += "Integer Overflow"; break;
210 case static_cast<DWORD>(EXCEPTION_INVALID_DISPOSITION): r += "Invalid Disposition"; break;
211 case static_cast<DWORD>(EXCEPTION_NONCONTINUABLE_EXCEPTION): r += "Non-continuable Exception"; break;
212 case static_cast<DWORD>(EXCEPTION_PRIV_INSTRUCTION): r += "Priviledged Instruction"; break;
213 case static_cast<DWORD>(EXCEPTION_SINGLE_STEP): r += "Single Step"; break;
214 case static_cast<DWORD>(EXCEPTION_STACK_OVERFLOW): r += "Stack Overflow"; break;
215 default: r += "Unknown Operating System Exception"; break;
216 }
217 // clang-format on
218
219 return r;
220}
221
222[[nodiscard]] constexpr bool is_debugable_exception(EXCEPTION_POINTERS const &ep) noexcept
223{
224 // clang-format off
225 switch (ep.ExceptionRecord->ExceptionCode) {
226 case static_cast<DWORD>(STATUS_ASSERTION_FAILURE): return true;
227 case static_cast<DWORD>(EXCEPTION_ACCESS_VIOLATION): return true;
228 case static_cast<DWORD>(EXCEPTION_ARRAY_BOUNDS_EXCEEDED): return true;
229 case static_cast<DWORD>(EXCEPTION_BREAKPOINT): return true;
230 case static_cast<DWORD>(EXCEPTION_DATATYPE_MISALIGNMENT): return true;
231 case static_cast<DWORD>(EXCEPTION_FLT_DENORMAL_OPERAND): return true;
232 case static_cast<DWORD>(EXCEPTION_FLT_DIVIDE_BY_ZERO): return true;
233 case static_cast<DWORD>(EXCEPTION_FLT_INEXACT_RESULT): return true;
234 case static_cast<DWORD>(EXCEPTION_FLT_INVALID_OPERATION): return true;
235 case static_cast<DWORD>(EXCEPTION_FLT_OVERFLOW): return true;
236 case static_cast<DWORD>(EXCEPTION_FLT_STACK_CHECK): return true;
237 case static_cast<DWORD>(EXCEPTION_FLT_UNDERFLOW): return true;
238 case static_cast<DWORD>(EXCEPTION_ILLEGAL_INSTRUCTION): return true;
239 case static_cast<DWORD>(EXCEPTION_IN_PAGE_ERROR): return true;
240 case static_cast<DWORD>(EXCEPTION_INT_DIVIDE_BY_ZERO): return true;
241 case static_cast<DWORD>(EXCEPTION_INT_OVERFLOW): return true;
242 case static_cast<DWORD>(EXCEPTION_INVALID_DISPOSITION): return true;
243 case static_cast<DWORD>(EXCEPTION_NONCONTINUABLE_EXCEPTION): return true;
244 case static_cast<DWORD>(EXCEPTION_PRIV_INSTRUCTION): return true;
245 case static_cast<DWORD>(EXCEPTION_SINGLE_STEP): return false;
246 case static_cast<DWORD>(EXCEPTION_STACK_OVERFLOW): return true;
247 default: return false;
248 }
249 // clang-format on
250}
251
252inline LONG exception_handler(EXCEPTION_POINTERS *p) noexcept
253{
254 if (not is_debugable_exception(*p)) {
255 return EXCEPTION_CONTINUE_SEARCH;
256 }
257
258 // Fill in information about the exception so that the JIT debugger can handle it.
259 jit_debug_info.dwSize = sizeof(JIT_DEBUG_INFO);
260#if HI_PROCESSOR == HI_CPU_X86
261 jit_debug_info.dwProcessorArchitecture = PROCESSOR_ARCHITECTURE_INTEL;
262#elif HI_PROCESSOR == HI_CPU_X86_64
263 jit_debug_info.dwProcessorArchitecture = PROCESSOR_ARCHITECTURE_AMD64;
264#elif HI_PROCESSOR == HI_CPU_ARM
265 jit_debug_info.dwProcessorArchitecture = PROCESSOR_ARCHITECTURE_ARM;
266#elif HI_PROCESSOR == HI_CPU_ARM64
267 jit_debug_info.dwProcessorArchitecture = PROCESSOR_ARCHITECTURE_ARM64;
268#else
269#error "Not implemented."
270#endif
271 jit_debug_info.dwThreadID = GetCurrentThreadId();
272 jit_debug_info.dwReserved0 = 0;
273 jit_debug_info.lpExceptionAddress = std::bit_cast<ULONG64>(p->ExceptionRecord->ExceptionAddress);
274 jit_debug_info.lpExceptionRecord = std::bit_cast<ULONG64>(&p->ExceptionRecord);
275 jit_debug_info.lpContextRecord = std::bit_cast<ULONG64>(p->ContextRecord);
276
277 if (IsDebuggerPresent()) {
278 // This is normally not reached.
279 // But if the debugger is present, just do what normally is done.
280 return EXCEPTION_CONTINUE_SEARCH;
281
282 } else if (launch_jit_debugger()) {
283 // The user selected a debugger.
284 // The instruction that caused the exception will be executed again.
285
286 // Clear the message set by a hi_assert_abort().
287 ::hi::set_debug_message(nullptr);
288 return EXCEPTION_CONTINUE_EXECUTION;
289
290 } else if (p->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) {
291 // A break-point without a terminate message will simply continue.
292 // No debugger attached, advance the instruction pointer to not get into a loop.
293#if HI_PROCESSOR == HI_CPU_X86_64
294 // The breakpoint instruction is 0xCC (int 3), advance the instruction pointer.
295 p->ContextRecord->Rip++;
296#elif HI_PROCESSOR == HI_CPU_X86
297 // The breakpoint instruction is 0xCC (int 3), advance the instruction pointer.
298 p->ContextRecord->Eip++;
299#else
300#error "Not implemented."
301#endif
302 return EXCEPTION_CONTINUE_EXECUTION;
303
304 } else {
305 if (not (p->ExceptionRecord->ExceptionCode == STATUS_ASSERTION_FAILURE and has_debug_message())) {
306 // The exception was not caused by a hi_assert_abort().
307 auto exception_str = to_string(*p);
308 set_debug_message(exception_str.c_str());
309 }
310
311 // If we reach this point we already tried opening the JIT debugger,
312 // std::abort() should not.
313 _set_abort_behavior(0, _CALL_REPORTFAULT);
315
316 // A EXCEPTION_CONTINUE_SEARCH does not cause std::terminate() to be called.
317 }
318}
319
320} // namespace detail
321
322inline void enable_debugger() noexcept
323{
324 // Disable error messages from the Windows CRT on std::terminate().
325 _CrtSetReportMode(_CRT_WARN, 0);
326 _CrtSetReportMode(_CRT_ERROR, 0);
327 _CrtSetReportMode(_CRT_ASSERT, 0);
328
329 // Install a handler for __debugbreak() / int 3 (0xCC).
330 // This handler will request the user if it wants a debugger to be
331 // attached to the application.
332 AddVectoredExceptionHandler(0, detail::exception_handler);
333}
334
335} // namespace v1
336} // namespace hi::inline v1
337
338hi_warning_pop();
Rules for working with win32 headers.
The HikoGUI namespace.
Definition array_generic.hpp:20
void enable_debugger() noexcept
Enable the JIT debugger to be attached.
Definition debugger_generic_impl.hpp:17
uint32_t win32_HANDLE_to_int(HANDLE handle) noexcept
Convert a HANDLE to a 32-bit unsigned integer.
Definition utility.hpp:25
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Definition debugger_win32_impl.hpp:36
T count(T... args)
T replace(T... args)
T sleep_for(T... args)
T terminate(T... args)
T to_string(T... args)