Currently, when debugging a program with `lldb-dap` on Windows and using the `integratedTerminal` option, lldb-dap immediatly stops with an `0x80000003` Exception. This is because `ntdll` executes an `int3` breakpoint during process initialization when a debugger is attached. This patch makes `lldb` and `lldb-server` skip the first `int3` after launch when it originates from a system module (the loader's debugger notification). Only that first loader breakpoint is skipped. Any later int3, including `__debugbreak()`, `__builtin_debugtrap()` in the debuggee's own code, still stops the debugger. Fixes https://github.com/llvm/llvm-project/issues/198763 (cherry picked from commit 83530ce356dd55fdc981b5325ca1f5117b4a58f8)
161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
"""
|
|
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,
|
|
)
|
|
|
|
@skipUnlessWindows
|
|
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)
|