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 process_creator(binfile):
73  Kernel32 = WinDLL("Kernel32")
74
75  # Another flavour of process creation
76  startupinfoa = STARTUPINFOA()
77  startupinfoa.cb = sizeof(STARTUPINFOA)
78  startupinfoa.lpReserved = None
79  startupinfoa.lpDesktop = None
80  startupinfoa.lpTitle = None
81  startupinfoa.dwX = 0
82  startupinfoa.dwY = 0
83  startupinfoa.dwXSize = 0
84  startupinfoa.dwYSize = 0
85  startupinfoa.dwXCountChars = 0
86  startupinfoa.dwYCountChars = 0
87  startupinfoa.dwFillAttribute = 0
88  startupinfoa.dwFlags = 0
89  startupinfoa.wShowWindow = 0
90  startupinfoa.cbReserved2 = 0
91  startupinfoa.lpReserved2 = None
92  startupinfoa.hStdInput = None
93  startupinfoa.hStdOutput = None
94  startupinfoa.hStdError = None
95  processinformation = PROCESS_INFORMATION()
96
97  # 0x4 below specifies CREATE_SUSPENDED.
98  ret = Kernel32.CreateProcessA(binfile.encode("ascii"), None, None, None, False, 0x4, None, None, byref(startupinfoa), byref(processinformation))
99  if ret == 0:
100    raise Exception('CreateProcess running {}'.format(binfile))
101
102  return processinformation.dwProcessId, processinformation.dwThreadId, processinformation.hProcess, processinformation.hThread
103
104def thread_resumer(hProcess, hThread):
105  Kernel32 = WinDLL("Kernel32")
106
107  # For reasons unclear to me, other suspend-references seem to be opened on
108  # the opened thread. Clear them all.
109  while True:
110    ret = Kernel32.ResumeThread(hThread)
111    if ret <= 0:
112      break
113  if ret < 0:
114    Kernel32.TerminateProcess(hProcess, 1)
115    raise Exception("Couldn't resume process after startup")
116
117  return
118
119def setup_everything(binfile):
120  from . import client
121  from . import symbols
122  Client = client.Client()
123
124  created_pid, created_tid, hProcess, hThread = process_creator(binfile)
125
126  # Load lines as well as general symbols
127  sym_opts = Client.Symbols.GetSymbolOptions()
128  sym_opts |= symbols.SymbolOptionFlags.SYMOPT_LOAD_LINES
129  Client.Symbols.SetSymbolOptions(sym_opts)
130
131  Client.AttachProcess(created_pid)
132
133  # Need to enter the debugger engine to let it attach properly
134  Client.Control.WaitForEvent(timeout=1)
135  Client.SysObjects.set_current_thread(created_pid, created_tid)
136  Client.Control.Execute("l+t")
137  Client.Control.SetExpressionSyntax(cpp=True)
138
139  module_name = Client.Symbols.get_exefile_module_name()
140  offset = Client.Symbols.GetOffsetByName("{}!main".format(module_name))
141  breakpoint = Client.Control.AddBreakpoint2(offset=offset, enabled=True)
142  thread_resumer(hProcess, hThread)
143  Client.Control.SetExecutionStatus(control.DebugStatus.DEBUG_STATUS_GO)
144
145  # Problem: there is no guarantee that the client will ever reach main,
146  # something else exciting could happen in that time, the host system may
147  # be very loaded, and similar. Wait for some period, say, five seconds, and
148  # abort afterwards: this is a trade-off between spurious timeouts and
149  # completely hanging in the case of a environmental/programming error.
150  res = Client.Control.WaitForEvent(timeout=5000)
151  if res == S_FALSE:
152    Kernel32.TerminateProcess(hProcess, 1)
153    raise Exception("Debuggee did not reach main function in a timely manner")
154
155  break_on_all_but_main(Client.Control, Client.Symbols, offset)
156
157  # Set the default action on all exceptions to be "quit and detach". If we
158  # don't, dbgeng will merrily spin at the exception site forever.
159  filts = Client.Control.GetNumberEventFilters()
160  for x in range(filts[0], filts[0] + filts[1]):
161    Client.Control.SetExceptionFilterSecondCommand(x, "qd")
162
163  return Client, hProcess
164
165def step_once(client):
166  client.Control.Execute("p")
167  try:
168    client.Control.WaitForEvent()
169  except Exception as e:
170    if client.Control.GetExecutionStatus() == control.DebugStatus.DEBUG_STATUS_NO_DEBUGGEE:
171      return None # Debuggee has gone away, likely due to an exception.
172    raise e
173  # Could assert here that we're in the "break" state
174  client.Control.GetExecutionStatus()
175  return probe_state(client)
176
177def main_loop(client):
178  res = True
179  while res is not None:
180    res = step_once(client)
181
182def cleanup(client, hProcess):
183  res = client.DetachProcesses()
184  Kernel32 = WinDLL("Kernel32")
185  Kernel32.TerminateProcess(hProcess, 1)
186