| """ | |
| Test that lldb ignores the Windows loader breakpoint when attaching to a | |
| process, but still stops at a genuine breakpoint instruction (an int3) that | |
| lives in the program's own code. | |
| """ | |
| import ctypes | |
| import lldb | |
| from lldbsuite.test.decorators import * | |
| from lldbsuite.test.lldbtest import * | |
| from lldbsuite.test import lldbutil | |
| class WindowsAttachLoaderBreakpointTestCase(TestBase): | |
| def create_suspended_process(self, exe): | |
| """Create ``exe`` suspended and return its (pid, hProcess, hThread). | |
| This mirrors what the lldb-dap runInTerminal launcher does on Windows: | |
| the debuggee is started with CREATE_SUSPENDED so lldb can attach while | |
| the process is still being initialized by the loader. The launcher then | |
| resumes the main thread once the debugger has attached. | |
| """ | |
| from ctypes import wintypes | |
| CREATE_SUSPENDED = 0x00000004 | |
| class STARTUPINFOW(ctypes.Structure): | |
| _fields_ = [ | |
| ("cb", wintypes.DWORD), | |
| ("lpReserved", wintypes.LPWSTR), | |
| ("lpDesktop", wintypes.LPWSTR), | |
| ("lpTitle", wintypes.LPWSTR), | |
| ("dwX", wintypes.DWORD), | |
| ("dwY", wintypes.DWORD), | |
| ("dwXSize", wintypes.DWORD), | |
| ("dwYSize", wintypes.DWORD), | |
| ("dwXCountChars", wintypes.DWORD), | |
| ("dwYCountChars", wintypes.DWORD), | |
| ("dwFillAttribute", wintypes.DWORD), | |
| ("dwFlags", wintypes.DWORD), | |
| ("wShowWindow", wintypes.WORD), | |
| ("cbReserved2", wintypes.WORD), | |
| ("lpReserved2", ctypes.POINTER(ctypes.c_byte)), | |
| ("hStdInput", wintypes.HANDLE), | |
| ("hStdOutput", wintypes.HANDLE), | |
| ("hStdError", wintypes.HANDLE), | |
| ] | |
| class PROCESS_INFORMATION(ctypes.Structure): | |
| _fields_ = [ | |
| ("hProcess", wintypes.HANDLE), | |
| ("hThread", wintypes.HANDLE), | |
| ("dwProcessId", wintypes.DWORD), | |
| ("dwThreadId", wintypes.DWORD), | |
| ] | |
| kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) | |
| kernel32.CreateProcessW.argtypes = [ | |
| wintypes.LPCWSTR, | |
| wintypes.LPWSTR, | |
| ctypes.c_void_p, | |
| ctypes.c_void_p, | |
| wintypes.BOOL, | |
| wintypes.DWORD, | |
| ctypes.c_void_p, | |
| wintypes.LPCWSTR, | |
| ctypes.POINTER(STARTUPINFOW), | |
| ctypes.POINTER(PROCESS_INFORMATION), | |
| ] | |
| kernel32.CreateProcessW.restype = wintypes.BOOL | |
| startupinfo = STARTUPINFOW() | |
| startupinfo.cb = ctypes.sizeof(STARTUPINFOW) | |
| process_information = PROCESS_INFORMATION() | |
| # CreateProcessW may modify the command-line buffer, so it must be | |
| # writable. | |
| command_line = ctypes.create_unicode_buffer('"{}"'.format(exe)) | |
| if not kernel32.CreateProcessW( | |
| exe, | |
| command_line, | |
| None, | |
| None, | |
| False, | |
| CREATE_SUSPENDED, | |
| None, | |
| None, | |
| ctypes.byref(startupinfo), | |
| ctypes.byref(process_information), | |
| ): | |
| raise OSError(ctypes.get_last_error(), "CreateProcessW failed") | |
| return ( | |
| process_information.dwProcessId, | |
| process_information.hProcess, | |
| process_information.hThread, | |
| ) | |
| @requireWindows | |
| def test_attach_ignores_loader_breakpoint(self): | |
| """ | |
| lldb must not report the loader's int3 (raised in a system module while | |
| attaching) as a user-visible stop, but must still stop at the | |
| __builtin_debugtrap() in the program's own code. | |
| """ | |
| self.build() | |
| exe = self.getBuildArtifact("a.out") | |
| pid, hProcess, hThread = self.create_suspended_process(exe) | |
| kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) | |
| kernel32.ResumeThread.argtypes = [ctypes.c_void_p] | |
| kernel32.ResumeThread.restype = ctypes.c_uint | |
| def cleanup(): | |
| kernel32.TerminateProcess(hProcess, 0) | |
| kernel32.CloseHandle(hThread) | |
| kernel32.CloseHandle(hProcess) | |
| self.addTearDownHook(cleanup) | |
| self.dbg.SetAsync(False) | |
| # Attach while the process is suspended and still being initialized. | |
| target = self.dbg.CreateTarget(exe) | |
| self.assertTrue(target, VALID_TARGET) | |
| error = lldb.SBError() | |
| process = target.AttachToProcessWithID(self.dbg.GetListener(), pid, error) | |
| self.assertSuccess(error, "attach to the suspended process") | |
| self.assertState(process.GetState(), lldb.eStateStopped) | |
| self.assertNotEqual( | |
| kernel32.ResumeThread(hThread), 0xFFFFFFFF, "ResumeThread failed" | |
| ) | |
| # Continuing must run past the loader breakpoint (skipped because it is | |
| # raised in a system module) and stop at the program's own | |
| # __builtin_debugtrap(). | |
| process.Continue() | |
| self.assertState(process.GetState(), lldb.eStateStopped) | |
| thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonException) | |
| self.assertIsNotNone( | |
| thread, "the process should stop at the user __builtin_debugtrap()" | |
| ) | |
| function_name = thread.GetFrameAtIndex(0).GetFunctionName() | |
| self.assertIn( | |
| "main", | |
| function_name, | |
| "expected to stop at the program's __builtin_debugtrap(), but " | |
| "stopped in '{}' (a spurious loader breakpoint in a system module " | |
| "was not skipped)".format(function_name), | |
| ) | |
| process.Continue() | |
| self.assertState(process.GetState(), lldb.eStateExited) | |
| self.assertEqual(process.GetExitStatus(), 0) |