1# DExTer : Debugging Experience Tester
2# ~~~~~~   ~         ~~         ~   ~~
3#
4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5# See https://llvm.org/LICENSE.txt for license information.
6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7
8from ctypes import *
9
10from . import client
11from . import control
12from . import symbols
13from .probe_process import probe_state
14from .utils import *
15
16class STARTUPINFOA(Structure):
17  _fields_ = [
18      ('cb', c_ulong),
19      ('lpReserved', c_char_p),
20      ('lpDesktop', c_char_p),
21      ('lpTitle', c_char_p),
22      ('dwX', c_ulong),
23      ('dwY', c_ulong),
24      ('dwXSize', c_ulong),
25      ('dwYSize', c_ulong),
26      ('dwXCountChars', c_ulong),
27      ('dwYCountChars', c_ulong),
28      ('dwFillAttribute', c_ulong),
29      ('wShowWindow', c_ushort),
30      ('cbReserved2', c_ushort),
31      ('lpReserved2', c_char_p),
32      ('hStdInput', c_void_p),
33      ('hStdOutput', c_void_p),
34      ('hStdError', c_void_p)
35    ]
36
37class PROCESS_INFORMATION(Structure):
38  _fields_ = [
39      ('hProcess', c_void_p),
40      ('hThread', c_void_p),
41      ('dwProcessId', c_ulong),
42      ('dwThreadId', c_ulong)
43    ]
44
45def fetch_local_function_syms(Symbols, prefix):
46  syms = Symbols.get_all_functions()
47
48  def is_sym_in_src_dir(sym):
49    name, data = sym
50    symdata = Symbols.GetLineByOffset(data.Offset)
51    if symdata is not None:
52      srcfile, line = symdata
53      if prefix in srcfile:
54        return True
55    return False
56
57  syms = [x for x in syms if is_sym_in_src_dir(x)]
58  return syms
59
60def break_on_all_but_main(Control, Symbols, main_offset):
61  mainfile, _ = Symbols.GetLineByOffset(main_offset)
62  prefix = '\\'.join(mainfile.split('\\')[:-1])
63
64  for name, rec in fetch_local_function_syms(Symbols, prefix):
65    if name == "main":
66      continue
67    bp = Control.AddBreakpoint2(offset=rec.Offset, enabled=True)
68
69  # All breakpoints are currently discarded: we just sys.exit for cleanup
70  return
71
72def setup_everything(binfile):
73  from . import client
74  from . import symbols
75  Client = client.Client()
76
77  Client.Control.SetEngineOptions(0x20) # DEBUG_ENGOPT_INITIAL_BREAK
78
79  Client.CreateProcessAndAttach2(binfile)
80
81  # Load lines as well as general symbols
82  sym_opts = Client.Symbols.GetSymbolOptions()
83  sym_opts |= symbols.SymbolOptionFlags.SYMOPT_LOAD_LINES
84  Client.Symbols.SetSymbolOptions(sym_opts)
85
86  # Need to enter the debugger engine to let it attach properly.
87  res = Client.Control.WaitForEvent(timeout=1000)
88  if res == S_FALSE:
89    # The debugee apparently didn't do anything at all. Rather than risk
90    # hanging, bail out at this point.
91    client.TerminateProcesses()
92    raise Exception("Debuggee did not start in a timely manner")
93
94  # Enable line stepping.
95  Client.Control.Execute("l+t")
96  # Enable C++ expression interpretation.
97  Client.Control.SetExpressionSyntax(cpp=True)
98
99  # We've requested to break into the process at the earliest opportunity,
100  # and WaitForEvent'ing means we should have reached that break state.
101  # Now set a breakpoint on the main symbol, and "go" until we reach it.
102  module_name = Client.Symbols.get_exefile_module_name()
103  offset = Client.Symbols.GetOffsetByName("{}!main".format(module_name))
104  breakpoint = Client.Control.AddBreakpoint2(offset=offset, enabled=True)
105  Client.Control.SetExecutionStatus(control.DebugStatus.DEBUG_STATUS_GO)
106
107  # Problem: there is no guarantee that the client will ever reach main,
108  # something else exciting could happen in that time, the host system may
109  # be very loaded, and similar. Wait for some period, say, five seconds, and
110  # abort afterwards: this is a trade-off between spurious timeouts and
111  # completely hanging in the case of a environmental/programming error.
112  res = Client.Control.WaitForEvent(timeout=5000)
113  if res == S_FALSE:
114    client.TerminateProcesses()
115    raise Exception("Debuggee did not reach main function in a timely manner")
116
117  break_on_all_but_main(Client.Control, Client.Symbols, offset)
118
119  # Set the default action on all exceptions to be "quit and detach". If we
120  # don't, dbgeng will merrily spin at the exception site forever.
121  filts = Client.Control.GetNumberEventFilters()
122  for x in range(filts[0], filts[0] + filts[1]):
123    Client.Control.SetExceptionFilterSecondCommand(x, "qd")
124
125  return Client
126
127def step_once(client):
128  client.Control.Execute("p")
129  try:
130    client.Control.WaitForEvent()
131  except Exception as e:
132    if client.Control.GetExecutionStatus() == control.DebugStatus.DEBUG_STATUS_NO_DEBUGGEE:
133      return None # Debuggee has gone away, likely due to an exception.
134    raise e
135  # Could assert here that we're in the "break" state
136  client.Control.GetExecutionStatus()
137  return probe_state(client)
138
139def main_loop(client):
140  res = True
141  while res is not None:
142    res = step_once(client)
143
144def cleanup(client):
145  client.TerminateProcesses()
146