# DExTer : Debugging Experience Tester # ~~~~~~ ~ ~~ ~ ~~ # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception from ctypes import * from . import client from . import control from . import symbols from .probe_process import probe_state from .utils import * class STARTUPINFOA(Structure): _fields_ = [ ('cb', c_ulong), ('lpReserved', c_char_p), ('lpDesktop', c_char_p), ('lpTitle', c_char_p), ('dwX', c_ulong), ('dwY', c_ulong), ('dwXSize', c_ulong), ('dwYSize', c_ulong), ('dwXCountChars', c_ulong), ('dwYCountChars', c_ulong), ('dwFillAttribute', c_ulong), ('wShowWindow', c_ushort), ('cbReserved2', c_ushort), ('lpReserved2', c_char_p), ('hStdInput', c_void_p), ('hStdOutput', c_void_p), ('hStdError', c_void_p) ] class PROCESS_INFORMATION(Structure): _fields_ = [ ('hProcess', c_void_p), ('hThread', c_void_p), ('dwProcessId', c_ulong), ('dwThreadId', c_ulong) ] def fetch_local_function_syms(Symbols, prefix): syms = Symbols.get_all_functions() def is_sym_in_src_dir(sym): name, data = sym symdata = Symbols.GetLineByOffset(data.Offset) if symdata is not None: srcfile, line = symdata if prefix in srcfile: return True return False syms = [x for x in syms if is_sym_in_src_dir(x)] return syms def break_on_all_but_main(Control, Symbols, main_offset): mainfile, _ = Symbols.GetLineByOffset(main_offset) prefix = '\\'.join(mainfile.split('\\')[:-1]) for name, rec in fetch_local_function_syms(Symbols, prefix): if name == "main": continue bp = Control.AddBreakpoint2(offset=rec.Offset, enabled=True) # All breakpoints are currently discarded: we just sys.exit for cleanup return def process_creator(binfile): Kernel32 = WinDLL("Kernel32") # Another flavour of process creation startupinfoa = STARTUPINFOA() startupinfoa.cb = sizeof(STARTUPINFOA) startupinfoa.lpReserved = None startupinfoa.lpDesktop = None startupinfoa.lpTitle = None startupinfoa.dwX = 0 startupinfoa.dwY = 0 startupinfoa.dwXSize = 0 startupinfoa.dwYSize = 0 startupinfoa.dwXCountChars = 0 startupinfoa.dwYCountChars = 0 startupinfoa.dwFillAttribute = 0 startupinfoa.dwFlags = 0 startupinfoa.wShowWindow = 0 startupinfoa.cbReserved2 = 0 startupinfoa.lpReserved2 = None startupinfoa.hStdInput = None startupinfoa.hStdOutput = None startupinfoa.hStdError = None processinformation = PROCESS_INFORMATION() # 0x4 below specifies CREATE_SUSPENDED. ret = Kernel32.CreateProcessA(binfile.encode("ascii"), None, None, None, False, 0x4, None, None, byref(startupinfoa), byref(processinformation)) if ret == 0: raise Exception('CreateProcess running {}'.format(binfile)) return processinformation.dwProcessId, processinformation.dwThreadId, processinformation.hProcess, processinformation.hThread def thread_resumer(hProcess, hThread): Kernel32 = WinDLL("Kernel32") # For reasons unclear to me, other suspend-references seem to be opened on # the opened thread. Clear them all. while True: ret = Kernel32.ResumeThread(hThread) if ret <= 0: break if ret < 0: Kernel32.TerminateProcess(hProcess, 1) raise Exception("Couldn't resume process after startup") return def setup_everything(binfile): from . import client from . import symbols Client = client.Client() created_pid, created_tid, hProcess, hThread = process_creator(binfile) # Load lines as well as general symbols sym_opts = Client.Symbols.GetSymbolOptions() sym_opts |= symbols.SymbolOptionFlags.SYMOPT_LOAD_LINES Client.Symbols.SetSymbolOptions(sym_opts) Client.AttachProcess(created_pid) # Need to enter the debugger engine to let it attach properly Client.Control.WaitForEvent(timeout=1) Client.SysObjects.set_current_thread(created_pid, created_tid) Client.Control.Execute("l+t") Client.Control.SetExpressionSyntax(cpp=True) module_name = Client.Symbols.get_exefile_module_name() offset = Client.Symbols.GetOffsetByName("{}!main".format(module_name)) breakpoint = Client.Control.AddBreakpoint2(offset=offset, enabled=True) thread_resumer(hProcess, hThread) Client.Control.SetExecutionStatus(control.DebugStatus.DEBUG_STATUS_GO) # Problem: there is no guarantee that the client will ever reach main, # something else exciting could happen in that time, the host system may # be very loaded, and similar. Wait for some period, say, five seconds, and # abort afterwards: this is a trade-off between spurious timeouts and # completely hanging in the case of a environmental/programming error. res = Client.Control.WaitForEvent(timeout=5000) if res == S_FALSE: Kernel32.TerminateProcess(hProcess, 1) raise Exception("Debuggee did not reach main function in a timely manner") break_on_all_but_main(Client.Control, Client.Symbols, offset) # Set the default action on all exceptions to be "quit and detach". If we # don't, dbgeng will merrily spin at the exception site forever. filts = Client.Control.GetNumberEventFilters() for x in range(filts[0], filts[0] + filts[1]): Client.Control.SetExceptionFilterSecondCommand(x, "qd") return Client, hProcess def step_once(client): client.Control.Execute("p") try: client.Control.WaitForEvent() except Exception as e: if client.Control.GetExecutionStatus() == control.DebugStatus.DEBUG_STATUS_NO_DEBUGGEE: return None # Debuggee has gone away, likely due to an exception. raise e # Could assert here that we're in the "break" state client.Control.GetExecutionStatus() return probe_state(client) def main_loop(client): res = True while res is not None: res = step_once(client) def cleanup(client, hProcess): res = client.DetachProcesses() Kernel32 = WinDLL("Kernel32") Kernel32.TerminateProcess(hProcess, 1)