hilda 3.0.0__py3-none-any.whl → 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hilda/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '3.0.0'
21
- __version_tuple__ = version_tuple = (3, 0, 0)
20
+ __version__ = version = '3.1.0'
21
+ __version_tuple__ = version_tuple = (3, 1, 0)
hilda/breakpoints.py CHANGED
@@ -124,12 +124,30 @@ class HildaBreakpoint:
124
124
  for name in names_to_add:
125
125
  self.lldb_breakpoint.AddName(name)
126
126
 
127
+ @property
128
+ def enabled(self) -> bool:
129
+ """
130
+ Configures whether this breakpoint is enabled or not.
131
+ """
132
+ return self.lldb_breakpoint.IsEnabled()
133
+
134
+ @enabled.setter
135
+ def enabled(self, value: bool) -> None:
136
+ self.lldb_breakpoint.SetEnabled(value)
137
+
127
138
  def __repr__(self) -> str:
128
- return (f'<{self.__class__.__name__} LLDB:{self.lldb_breakpoint} GUARDED:{self.guarded} '
139
+ enabled_repr = 'ENABLED' if self.enabled else 'DISABLED'
140
+ guarded_repr = 'GUARDED' if self.guarded else 'NOT-GUARDED'
141
+ return (f'<{self.__class__.__name__} LLDB:{self.lldb_breakpoint} {enabled_repr} {guarded_repr} '
129
142
  f'CALLBACK:{self.callback}>')
130
143
 
131
144
  def __str__(self) -> str:
132
- result = f'🚨 Breakpoint #{self.id} (guarded: {self.guarded}):\n'
145
+ emoji = '🚨' if self.enabled else '🔕'
146
+ enabled_str = 'enabled' if self.enabled else 'disabled'
147
+ guarded_str = 'guarded' if self.guarded else 'not-guarded'
148
+
149
+ result = f'{emoji} Breakpoint #{self.id} ({enabled_str}, {guarded_str})\n'
150
+
133
151
  if self.description is not None:
134
152
  result += f'\tDescription: {self.description}\n'
135
153
 
@@ -137,10 +155,13 @@ class HildaBreakpoint:
137
155
  result += f'\tWhere: {self.where}\n'
138
156
 
139
157
  # A single breakpoint may be related to several locations (addresses)
158
+ locations = self.locations
159
+ if len(locations) == 0:
160
+ result += f'\tNo locations\n'
140
161
  for location in self.locations:
141
162
  result += f'\tLocation {location}\n'
142
163
 
143
- return result
164
+ return result.strip('\n')
144
165
 
145
166
  def remove(self, remove_guarded: bool = False) -> None:
146
167
  """
@@ -441,8 +462,10 @@ class BreakpointList:
441
462
 
442
463
  def show(self) -> None:
443
464
  """ Show existing breakpoints. """
465
+ if len(self) == 0:
466
+ self._hilda.log_info('No breakpoints')
444
467
  for bp in self:
445
- print(bp)
468
+ self._hilda.log_info(bp)
446
469
 
447
470
  def items(self):
448
471
  """
hilda/decorators.py ADDED
@@ -0,0 +1,45 @@
1
+ from hilda.breakpoints import WhereType
2
+ from hilda.lldb_importer import lldb
3
+ p = lldb.hilda_client
4
+
5
+
6
+ def breakpoint(where: WhereType, enable: bool = False):
7
+ """
8
+ A decorator to define a breakpoint, e.g.,
9
+
10
+ ```
11
+ @breakpoint('symbol_name')
12
+ def my_breakpoint(hilda, *args):
13
+ print('Hit!')
14
+ hilda.cont()
15
+
16
+ my_breakpoint.enabled = True
17
+ ```
18
+ """
19
+ def decorator(callback):
20
+ bp = p.breakpoints.add(where, callback=callback)
21
+ bp.enabled = enable
22
+ return bp
23
+
24
+ return decorator
25
+
26
+
27
+ def watchpoint(where: int, enable: bool = False):
28
+ """
29
+ A decorator to define a watchpoint, e.g.,
30
+
31
+ ```
32
+ @watchpoint(0x11223344)
33
+ def my_watchpoint(hilda, *args):
34
+ print('Hit!')
35
+ hilda.cont()
36
+
37
+ my_watchpoint.enabled = True
38
+ ```
39
+ """
40
+ def decorator(callback):
41
+ wp = p.watchpoints.add(where, callback=callback)
42
+ wp.enabled = enable
43
+ return wp
44
+
45
+ return decorator
hilda/hilda_client.py CHANGED
@@ -42,6 +42,7 @@ from hilda.snippets.mach import CFRunLoopServiceMachPort_hooks
42
42
  from hilda.symbol import Symbol
43
43
  from hilda.symbols_jar import SymbolsJar
44
44
  from hilda.ui.ui_manager import UiManager
45
+ from hilda.watchpoints import WatchpointList
45
46
 
46
47
  lldb.KEYSTONE_SUPPORT = True
47
48
  try:
@@ -128,6 +129,7 @@ class HildaClient:
128
129
  self.process = self.target.GetProcess()
129
130
  self.symbols = SymbolsJar.create(self)
130
131
  self.breakpoints = BreakpointList(self)
132
+ self.watchpoints = WatchpointList(self)
131
133
  self.captured_objects = {}
132
134
  self.registers = Registers(self)
133
135
  self.arch = self.target.GetTriple().split('-')[0]
hilda/launch_lldb.py CHANGED
@@ -61,6 +61,33 @@ class LLDBListenerThread(Thread, ABC):
61
61
  sys.stderr.write(stderr)
62
62
  stderr = self.process.GetSTDERR(1024)
63
63
 
64
+ def _process_potential_watchpoint_event(self) -> None:
65
+ stopped_threads = self._get_stopped_threads(lldb.eStopReasonWatchpoint)
66
+ for thread in stopped_threads:
67
+ watchpoint_id = thread.GetStopReasonDataAtIndex(0)
68
+ frame = thread.GetFrameAtIndex(0)
69
+ if lldb.hilda_client is not None:
70
+ lldb.hilda_client.watchpoints._dispatch_watchpoint_callback(watchpoint_id, thread, frame)
71
+
72
+ def _get_stopped_threads(self, reason: Optional[int] = None) -> list[lldb.SBThread]:
73
+ if reason is None:
74
+ stop_reasons = [
75
+ lldb.eStopReasonSignal, lldb.eStopReasonException,
76
+ lldb.eStopReasonBreakpoint, lldb.eStopReasonWatchpoint,
77
+ lldb.eStopReasonPlanComplete, lldb.eStopReasonTrace,
78
+ ]
79
+ else:
80
+ stop_reasons = [reason]
81
+
82
+ return [thread for thread in self.process if thread.GetStopReason() in stop_reasons]
83
+
84
+ def _set_selected_thread_to_stopped_thread(self) -> None:
85
+ stopped_threads = self._get_stopped_threads()
86
+ if len(stopped_threads) < 1:
87
+ return
88
+ thread = stopped_threads[0]
89
+ self.process.SetSelectedThread(thread)
90
+
64
91
  def run(self):
65
92
  event = lldb.SBEvent()
66
93
  last_state = lldb.eStateStopped
@@ -87,18 +114,8 @@ class LLDBListenerThread(Thread, ABC):
87
114
  logger.debug('Process Continued')
88
115
  elif state == lldb.eStateStopped and last_state == lldb.eStateRunning:
89
116
  logger.debug('Process Stopped')
90
- for thread in self.process:
91
- frame = thread.GetFrameAtIndex(0)
92
- stop_reason = thread.GetStopReason()
93
- logger.debug(f'tid = {hex(thread.GetThreadID())} pc = {frame.GetPC()}')
94
- if stop_reason not in [lldb.eStopReasonSignal, lldb.eStopReasonException,
95
- lldb.eStopReasonBreakpoint,
96
- lldb.eStopReasonWatchpoint, lldb.eStopReasonPlanComplete,
97
- lldb.eStopReasonTrace,
98
- lldb.eStopReasonSignal]:
99
- continue
100
- self.process.SetSelectedThread(thread)
101
- break
117
+ self._set_selected_thread_to_stopped_thread()
118
+ self._process_potential_watchpoint_event()
102
119
 
103
120
  last_state = state
104
121
 
hilda/symbol.py CHANGED
@@ -145,6 +145,9 @@ class Symbol(int):
145
145
  def monitor(self, **args):
146
146
  return self._client.monitor(self, **args)
147
147
 
148
+ def watch(self, **args):
149
+ return self._client.watchpoints.add(self, **args)
150
+
148
151
  def bp(self, callback=None, **args):
149
152
  return self._client.bp(self, callback, **args)
150
153
 
hilda/watchpoints.py ADDED
@@ -0,0 +1,269 @@
1
+ from typing import Callable, Generator, Optional, Union
2
+
3
+ from hilda.lldb_importer import lldb
4
+
5
+
6
+ class HildaWatchpoint:
7
+ """
8
+ Hilda's class representing an LLDB watchpoint, with some optional additional properties
9
+ """
10
+
11
+ def __init__(self, hilda, lldb_watchpoint: lldb.SBWatchpoint, where: Optional[int] = None) -> None:
12
+ """
13
+ Initialize a watchpoint list.
14
+
15
+ :param hilda.hilda_client.HildaClient hilda: Hilda client
16
+ """
17
+ self._hilda = hilda
18
+ self._where = where
19
+ self._callback = None
20
+
21
+ # Actual watchpoint from LLDB API
22
+ self.lldb_watchpoint = lldb_watchpoint
23
+
24
+ @property
25
+ def where(self) -> Optional[int]:
26
+ """
27
+ A value identifying where the watchpoint was set (when it was created).
28
+
29
+ It could be either an address (int) or a Hilda symbol object (Symbol, that inherits from int).
30
+ Note that self.address is similar, but self.where is where the watchpoit was set when it was
31
+ created (using Hilda API), and self.address is the actual address. They should have the same
32
+ value, although self.where may be a Hilda Symbol.
33
+ """
34
+ return self._where
35
+
36
+ @property
37
+ def id(self) -> int:
38
+ """ A number identifying the watchpoint. """
39
+ return self.lldb_watchpoint.GetID()
40
+
41
+ @property
42
+ def callback(self) -> Optional[Callable]:
43
+ """
44
+ A callback that will be executed when the watchpoint is hit.
45
+
46
+ Note that unless the callback explicitly continues (by calling `cont()`), the program will not continue.
47
+ The callback will be invoked as callback(hilda, *args), where hilda is the 'HildaClient'.
48
+ """
49
+ return self._callback
50
+
51
+ @callback.setter
52
+ def callback(self, callback: Optional[Callable]) -> None:
53
+ self._callback = callback
54
+
55
+ @property
56
+ def condition(self) -> Optional[str]:
57
+ """
58
+ An LLDB expression to make this a conditional watchpoint.
59
+ """
60
+ return self.lldb_watchpoint.GetCondition()
61
+
62
+ @condition.setter
63
+ def condition(self, condition: Optional[str]) -> None:
64
+ self.lldb_watchpoint.SetCondition(condition)
65
+
66
+ @property
67
+ def address(self) -> int:
68
+ """
69
+ Get the address this wathpoint watches (also see self.where).
70
+ """
71
+ return self.lldb_watchpoint.GetWatchAddress()
72
+
73
+ @property
74
+ def size(self) -> int:
75
+ """
76
+ Get the size this wathpoint watches.
77
+ """
78
+ return self.lldb_watchpoint.GetWatchSize()
79
+
80
+ @property
81
+ def enabled(self) -> bool:
82
+ """
83
+ Configures whether this watchpoint is enabled or not.
84
+ """
85
+ return self.lldb_watchpoint.IsEnabled()
86
+
87
+ @enabled.setter
88
+ def enabled(self, value: bool) -> None:
89
+ self.lldb_watchpoint.SetEnabled(value)
90
+
91
+ def __repr__(self) -> str:
92
+ return f'<{self.__class__.__name__} LLDB:{self.lldb_watchpoint} CALLBACK:{self.callback}>'
93
+
94
+ def __str__(self) -> str:
95
+ emoji = '🚨' if self.enabled else '🔕'
96
+ enabled_str = 'enabled' if self.enabled else 'disabled'
97
+ result = f'{emoji} Watchpoint #{self.id} ({enabled_str})\n'
98
+
99
+ if self.where is not None:
100
+ result += f'\tWhere: {self.where}\n'
101
+
102
+ return result.strip('\n')
103
+
104
+ def remove(self) -> None:
105
+ """
106
+ Remove the watchpoint.
107
+ """
108
+ self._hilda.watchpoints.remove(self)
109
+
110
+
111
+ class WatchpointList:
112
+ """
113
+ Manager for `HildaWatchpoint` objects, each one wrapping another native LLDB watchpoint.
114
+ """
115
+
116
+ def __init__(self, hilda) -> None:
117
+ """
118
+ Initialize a watchpoint list.
119
+
120
+ :param hilda.hilda_client.HildaClient hilda: Hilda client
121
+ """
122
+ self._hilda = hilda
123
+ self._watchpoints = {}
124
+
125
+ def __contains__(self, id_or_wp: Union[int, HildaWatchpoint]) -> bool:
126
+ return self.get(id_or_wp) is not None
127
+
128
+ def __iter__(self) -> Generator[HildaWatchpoint, None, None]:
129
+ for wp in self._hilda.target.watchpoint_iter():
130
+ yield self[wp.GetID()]
131
+
132
+ def __len__(self) -> int:
133
+ return self._hilda.target.GetNumWatchpoints()
134
+
135
+ def __getitem__(self, id_or_wp: Union[int, HildaWatchpoint]) -> HildaWatchpoint:
136
+ """
137
+ Get a watchpoint by ID (or the watchpoint itself, though it usually makes little sense)
138
+
139
+ :param id_or_wp: Watchpoint's ID (or the watchpoint itself)
140
+ """
141
+ wp = self.get(id_or_wp)
142
+ if wp is None:
143
+ raise KeyError(id_or_wp)
144
+
145
+ return wp
146
+
147
+ def __delitem__(self, id_or_wp: Union[int, HildaWatchpoint]):
148
+ """
149
+ Remove a watchpoint.
150
+
151
+ :param id_or_wp: Watchpoint's ID (or the watchpoint itself)
152
+ """
153
+ self.remove(id_or_wp)
154
+
155
+ def __repr__(self) -> str:
156
+ return repr(dict(self.items()))
157
+
158
+ def __str__(self) -> str:
159
+ return repr(self)
160
+
161
+ def get(self, id_or_wp: Union[int, HildaWatchpoint]) -> Optional[HildaWatchpoint]:
162
+ """
163
+ Get a watchpoint by ID or the watchpoint itself.
164
+
165
+ :param id_or_wp: Watchpoint's ID (or the watchpoint itself)
166
+ :return: `HildaWatchpoint` is one exists, or `None` otherwise
167
+ """
168
+
169
+ if isinstance(id_or_wp, int):
170
+ wp = self._hilda.target.FindWatchpointByID(id_or_wp)
171
+ elif isinstance(id_or_wp, HildaWatchpoint):
172
+ wp = id_or_wp.lldb_watchpoint
173
+ else:
174
+ raise KeyError(f'Watchpoint "{id_or_wp}" could not be found')
175
+
176
+ if not wp.IsValid():
177
+ return None
178
+
179
+ wp_id = wp.GetID()
180
+ if wp_id not in self._watchpoints:
181
+ self._hilda.log_debug(f'Found a watchpoint added outside of the Hilda API {wp}')
182
+ self._watchpoints[wp_id] = HildaWatchpoint(self._hilda, wp)
183
+
184
+ return self._watchpoints[wp_id]
185
+
186
+ def add(self, where: int, size: int = 8, read: bool = True, write: bool = True,
187
+ callback: Optional[Callable] = None, condition: str = None) -> HildaWatchpoint:
188
+ """
189
+ Add a watchpoint.
190
+
191
+ :param where: The address of the watchpoint.
192
+ :param size: The size of the watchpoint (the address span to watch).
193
+ :param read: The watchpoint should monitor reads from memory in the specified address (and size).
194
+ :param write: The watchpoint should monitor writes to memory in the specified address (and size).
195
+ :param callback: A callback that will be executed when the watchpoint is hit.
196
+ Note that unless the callback explicitly continues (by calling cont()), the program will not continue.
197
+ The callback will be invoked as callback(hilda, *args), where hilda is the 'HildaClient'.
198
+ :param condition: An LLDB expression to make this a conditional watchpoint.
199
+ :return: The new watchpoint
200
+ """
201
+
202
+ error = lldb.SBError()
203
+ wp = self._hilda.target.WatchAddress(where, size, read, write, error)
204
+ if not wp.IsValid():
205
+ raise Exception(f'Failed to create watchpoint at {where} ({error})')
206
+
207
+ wp = HildaWatchpoint(self._hilda, wp, where)
208
+ wp.callback = callback
209
+ wp.condition = condition
210
+
211
+ self._watchpoints[wp.id] = wp
212
+
213
+ self._hilda.log_info(f'Watchpoint #{wp.id} has been set')
214
+ return wp
215
+
216
+ def remove(self, id_or_wp: Union[int, HildaWatchpoint]) -> None:
217
+ """
218
+ Remove a watchpoint.
219
+
220
+ :param id_or_wp: Watchpoint's ID (or the watchpoint itself)
221
+ """
222
+ wp = self[id_or_wp]
223
+ watchpoint_id = wp.id
224
+ self._hilda.target.DeleteWatchpoint(watchpoint_id)
225
+ self._hilda.log_debug(f'Watchpoint #{watchpoint_id} has been removed')
226
+
227
+ def clear(self) -> None:
228
+ """
229
+ Remove all watchpoints
230
+ """
231
+ for wp in list(self):
232
+ self.remove(wp)
233
+
234
+ def show(self) -> None:
235
+ """ Show existing watchpoints. """
236
+ if len(self) == 0:
237
+ self._hilda.log_info('No watchpoints')
238
+ for wp in self:
239
+ self._hilda.log_info(wp)
240
+
241
+ def items(self) -> Generator[tuple[int, HildaWatchpoint], None, None]:
242
+ """
243
+ Get a watchpoint ID and watchpoint object tuple for every watchpoint.
244
+ """
245
+ return ((wp.id, wp) for wp in self)
246
+
247
+ def keys(self) -> Generator[int, None, None]:
248
+ """
249
+ Get the watchpoint ID for every watchpoint.
250
+ """
251
+ return (wp.id for wp in self)
252
+
253
+ def values(self) -> Generator[HildaWatchpoint, None, None]:
254
+ """
255
+ Get the watchpoint object for every watchpoint.
256
+ """
257
+ return (wp for wp in self)
258
+
259
+ def _dispatch_watchpoint_callback(self, watchpoint_id: int, _, frame) -> None:
260
+ """
261
+ Route the watchpoint callback the specific watchpoint callback.
262
+ """
263
+ self._hilda._bp_frame = frame
264
+ try:
265
+ callback = self[watchpoint_id].callback
266
+ if callback is not None:
267
+ callback(self._hilda, frame, None, self[watchpoint_id])
268
+ finally:
269
+ self._hilda._bp_frame = None