Files
llvm-tms34010/lldb/test/API/attach/TestWindowsAttachBreakpoint.py
Charles Zablit 5e7b435ebd [lldb][Windows] ignore loader breakpoints in system modules (#208233)
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)
2026-07-24 08:33:53 +02:00

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)