hilda 1.4.2__py3-none-any.whl → 2.0.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/__main__.py +5 -1
- hilda/_version.py +2 -2
- hilda/cli.py +89 -0
- hilda/exceptions.py +9 -1
- hilda/hilda_client.py +101 -118
- hilda/ipython_extensions/events.py +53 -0
- hilda/ipython_extensions/keybindings.py +22 -0
- hilda/ipython_extensions/magics.py +40 -0
- hilda/launch_lldb.py +177 -73
- hilda/lldb_entrypoint.py +2 -2
- hilda/lldb_importer.py +23 -0
- hilda/objective_c_class.py +4 -1
- hilda/objective_c_symbol.py +2 -0
- hilda/snippets/boringssl.py +1 -1
- hilda/snippets/dyld.py +1 -1
- hilda/snippets/fs_utils.py +1 -1
- hilda/snippets/mach/CFRunLoopServiceMachPort_hooks.py +1 -2
- hilda/snippets/macho/all_image_infos.py +1 -1
- hilda/snippets/macho/image_info.py +1 -1
- hilda/snippets/macho/macho_load_commands.py +1 -1
- hilda/snippets/remotepairingd.py +1 -1
- hilda/snippets/xpc.py +1 -1
- hilda/symbol.py +1 -1
- hilda/symbols_jar.py +8 -4
- {hilda-1.4.2.dist-info → hilda-2.0.0.dist-info}/METADATA +82 -28
- hilda-2.0.0.dist-info/RECORD +54 -0
- hilda-1.4.2.dist-info/RECORD +0 -49
- {hilda-1.4.2.dist-info → hilda-2.0.0.dist-info}/LICENSE +0 -0
- {hilda-1.4.2.dist-info → hilda-2.0.0.dist-info}/WHEEL +0 -0
- {hilda-1.4.2.dist-info → hilda-2.0.0.dist-info}/entry_points.txt +0 -0
- {hilda-1.4.2.dist-info → hilda-2.0.0.dist-info}/top_level.txt +0 -0
hilda/__main__.py
CHANGED
hilda/_version.py
CHANGED
hilda/cli.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import coloredlogs
|
|
7
|
+
|
|
8
|
+
from hilda import launch_lldb
|
|
9
|
+
from hilda._version import version
|
|
10
|
+
|
|
11
|
+
DEFAULT_HILDA_PORT = 1234
|
|
12
|
+
|
|
13
|
+
coloredlogs.install(level=logging.DEBUG)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def cli():
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
startup_files_option = click.option('-f', '--startup_files', multiple=True, default=None, help='Files to run on start')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_envp(ctx: click.Context, param: click.Parameter, value: List[str]) -> List[str]:
|
|
25
|
+
env_list = []
|
|
26
|
+
for item in value:
|
|
27
|
+
try:
|
|
28
|
+
key, val = item.split('=', 1)
|
|
29
|
+
env_list.append(f'{key}={val}')
|
|
30
|
+
except ValueError:
|
|
31
|
+
raise click.BadParameter(f'Invalid format for --envp: {item}. Expected KEY=VALUE.')
|
|
32
|
+
return env_list
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@cli.command('remote')
|
|
36
|
+
@click.argument('hostname', default='localhost')
|
|
37
|
+
@click.argument('port', type=click.INT, default=DEFAULT_HILDA_PORT)
|
|
38
|
+
@startup_files_option
|
|
39
|
+
def remote(hostname: str, port: int, startup_files: Optional[List[str]] = None) -> None:
|
|
40
|
+
""" Connect to remote debugserver at given address """
|
|
41
|
+
launch_lldb.remote(hostname, port, startup_files)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cli.command('attach')
|
|
45
|
+
@click.option('-n', '--name', help='process name to attach')
|
|
46
|
+
@click.option('-p', '--pid', type=click.INT, help='pid to attach')
|
|
47
|
+
@startup_files_option
|
|
48
|
+
def attach(name: str, pid: int, startup_files: Optional[List[str]] = None) -> None:
|
|
49
|
+
""" Attach to given process and start a lldb shell """
|
|
50
|
+
launch_lldb.attach(name=name, pid=pid, startup_files=startup_files)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@cli.command('launch')
|
|
54
|
+
@click.argument('exec_path')
|
|
55
|
+
@click.option('--argv', multiple=True, help='Command line arguments to pass to the process')
|
|
56
|
+
@click.option('--envp', multiple=True, callback=parse_envp, help='Environment variables in the form KEY=VALUE')
|
|
57
|
+
@click.option('--stdin', type=Path, help='Redirect stdin from this file path')
|
|
58
|
+
@click.option('--stdout', type=Path, help='Redirect stdout to this file path')
|
|
59
|
+
@click.option('--stderr', type=Path, help='Redirect stderr to this file path')
|
|
60
|
+
@click.option('--cwd', type=Path, help='Set the working directory for the process')
|
|
61
|
+
@click.option('--flags', type=click.INT, default=0, help='Launch flags (bitmask)')
|
|
62
|
+
@click.option('--stop-at-entry', is_flag=True, help='Stop the process at the entry point')
|
|
63
|
+
@startup_files_option
|
|
64
|
+
def launch(exec_path: str, argv: Optional[List] = None, envp: Optional[Mapping] = None,
|
|
65
|
+
stdin: Optional[Path] = None,
|
|
66
|
+
stdout: Optional[Path] = None, stderr: Optional[Path] = None, cwd: Optional[Path] = None,
|
|
67
|
+
flags: Optional[int] = 0, stop_at_entry: Optional[bool] = False,
|
|
68
|
+
startup_files: Optional[List[str]] = None) -> None:
|
|
69
|
+
""" Attach to given process and start a lldb shell """
|
|
70
|
+
if not argv:
|
|
71
|
+
argv = None
|
|
72
|
+
if not envp:
|
|
73
|
+
envp = None
|
|
74
|
+
launch_lldb.launch(exec_path, argv, envp, stdin, stdout, stderr, cwd, flags, stop_at_entry,
|
|
75
|
+
startup_files)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@cli.command('bare')
|
|
79
|
+
def cli_bare():
|
|
80
|
+
""" Just start a lldb shell """
|
|
81
|
+
commands = [f'command script import {Path(__file__).resolve().parent / "lldb_entrypoint.py"}']
|
|
82
|
+
commands = '\n'.join(commands)
|
|
83
|
+
launch_lldb.execute(f'lldb --one-line "{commands}"')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@cli.command('version')
|
|
87
|
+
def cli_version():
|
|
88
|
+
"""Show the version information."""
|
|
89
|
+
click.echo(version)
|
hilda/exceptions.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
__all__ = ['HildaException', 'SymbolAbsentError', 'EvaluatingExpressionError', 'CreatingObjectiveCSymbolError',
|
|
2
2
|
'ConvertingToNsObjectError', 'ConvertingFromNSObjectError', 'DisableJetsamMemoryChecksError',
|
|
3
3
|
'GettingObjectiveCClassError', 'AccessingRegisterError', 'AccessingMemoryError',
|
|
4
|
-
'BrokenLocalSymbolsJarError', 'AddingLldbSymbolError']
|
|
4
|
+
'BrokenLocalSymbolsJarError', 'AddingLldbSymbolError', 'LLDBException']
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class HildaException(Exception):
|
|
@@ -9,6 +9,14 @@ class HildaException(Exception):
|
|
|
9
9
|
pass
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class LLDBException(Exception):
|
|
13
|
+
""" A domain exception for lldb errors. """
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.message = message
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
class SymbolAbsentError(HildaException):
|
|
13
21
|
""" Raise when trying to get a symbol that doesn't exist. """
|
|
14
22
|
pass
|
hilda/hilda_client.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import ast
|
|
2
1
|
import base64
|
|
3
2
|
import builtins
|
|
4
3
|
import importlib
|
|
@@ -8,18 +7,19 @@ import logging
|
|
|
8
7
|
import os
|
|
9
8
|
import pickle
|
|
10
9
|
import struct
|
|
10
|
+
import sys
|
|
11
11
|
import time
|
|
12
12
|
import typing
|
|
13
13
|
from collections import namedtuple
|
|
14
14
|
from contextlib import contextmanager, suppress
|
|
15
|
+
from dataclasses import dataclass, field
|
|
15
16
|
from datetime import datetime, timezone
|
|
16
|
-
from functools import cached_property
|
|
17
|
+
from functools import cached_property, wraps
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any, Callable, List, Optional, Union
|
|
19
20
|
|
|
20
21
|
import hexdump
|
|
21
22
|
import IPython
|
|
22
|
-
import lldb
|
|
23
23
|
from humanfriendly import prompts
|
|
24
24
|
from humanfriendly.terminal.html import html_to_ansi
|
|
25
25
|
from IPython.core.magic import register_line_magic # noqa: F401
|
|
@@ -34,7 +34,7 @@ from hilda.common import CfSerializable
|
|
|
34
34
|
from hilda.exceptions import AccessingMemoryError, AccessingRegisterError, AddingLldbSymbolError, \
|
|
35
35
|
BrokenLocalSymbolsJarError, ConvertingFromNSObjectError, ConvertingToNsObjectError, CreatingObjectiveCSymbolError, \
|
|
36
36
|
DisableJetsamMemoryChecksError, EvaluatingExpressionError, HildaException, SymbolAbsentError
|
|
37
|
-
from hilda.
|
|
37
|
+
from hilda.lldb_importer import lldb
|
|
38
38
|
from hilda.objective_c_symbol import ObjectiveCSymbol
|
|
39
39
|
from hilda.registers import Registers
|
|
40
40
|
from hilda.snippets.mach import CFRunLoopServiceMachPort_hooks
|
|
@@ -49,48 +49,77 @@ except ImportError:
|
|
|
49
49
|
lldb.KEYSTONE_SUPPORT = False
|
|
50
50
|
print('failed to import keystone. disabling some features')
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
hilda_art = f.read()
|
|
52
|
+
hilda_art = Path(__file__).resolve().parent.joinpath('hilda_ascii_art.html').read_text()
|
|
54
53
|
|
|
55
54
|
GREETING = f"""
|
|
56
55
|
{hilda_art}
|
|
57
56
|
|
|
58
57
|
<b>Hilda has been successfully loaded! 😎
|
|
59
|
-
|
|
58
|
+
Usage:
|
|
59
|
+
<span style="color: magenta">p</span> Global to access all features.
|
|
60
|
+
<span style="color: magenta">F7</span> Step Into.
|
|
61
|
+
<span style="color: magenta">F8</span> Step Over.
|
|
62
|
+
<span style="color: magenta">F9</span> Continue.
|
|
63
|
+
<span style="color: magenta">F10</span> Stop.
|
|
64
|
+
|
|
60
65
|
Have a nice flight ✈️! Starting an IPython shell...
|
|
61
66
|
"""
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
p.log_error("Error: className is required.")
|
|
74
|
-
return
|
|
75
|
-
try:
|
|
76
|
-
local_ns[className] = p.objc_get_class(className)
|
|
77
|
-
p.log_info(f'{className} class loaded successfully')
|
|
78
|
-
except Exception:
|
|
79
|
-
p.log_error(f'Error loading class {className}')
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@register_line_magic
|
|
83
|
-
@needs_local_scope
|
|
84
|
-
def fbp(line, local_ns=None):
|
|
85
|
-
p = local_ns['p']
|
|
86
|
-
module_name, address = shlex.split(line.strip())
|
|
87
|
-
address = int(address, 16)
|
|
88
|
-
p.file_symbol(address, module_name).bp()
|
|
89
|
-
"""
|
|
68
|
+
|
|
69
|
+
def disable_logs() -> None:
|
|
70
|
+
logging.getLogger('asyncio').disabled = True
|
|
71
|
+
logging.getLogger('parso.cache').disabled = True
|
|
72
|
+
logging.getLogger('parso.cache.pickle').disabled = True
|
|
73
|
+
logging.getLogger('parso.python.diff').disabled = True
|
|
74
|
+
logging.getLogger('humanfriendly.prompts').disabled = True
|
|
75
|
+
logging.getLogger('blib2to3.pgen2.driver').disabled = True
|
|
76
|
+
logging.getLogger('hilda.launch_lldb').disabled = True
|
|
77
|
+
|
|
90
78
|
|
|
91
79
|
SerializableSymbol = namedtuple('SerializableSymbol', 'address type_ filename')
|
|
92
80
|
|
|
93
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class Configs:
|
|
84
|
+
""" Configuration settings for evaluation and monitoring. """
|
|
85
|
+
evaluation_unwind_on_error: bool = field(default=False,
|
|
86
|
+
metadata={'doc': 'Whether to unwind on error during evaluation.'})
|
|
87
|
+
evaluation_ignore_breakpoints: bool = field(default=False,
|
|
88
|
+
metadata={'doc': 'Whether to ignore breakpoints during evaluation.'})
|
|
89
|
+
nsobject_exclusion: bool = field(default=False, metadata={
|
|
90
|
+
'doc': 'Whether to exclude NSObject during evaluation - reduce ipython autocomplete results.'})
|
|
91
|
+
objc_verbose_monitor: bool = field(default=False, metadata={
|
|
92
|
+
'doc': 'When set to True, using monitor() will automatically print objc methods arguments.'})
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
return self.__str__()
|
|
96
|
+
|
|
97
|
+
def __str__(self):
|
|
98
|
+
config_str = 'Configuration settings:\n'
|
|
99
|
+
max_len = max(len(field_name) for field_name in self.__dataclass_fields__) + 2
|
|
100
|
+
|
|
101
|
+
for field_name, field_info in self.__dataclass_fields__.items():
|
|
102
|
+
value = getattr(self, field_name)
|
|
103
|
+
doc = field_info.metadata.get('doc', 'No docstring available')
|
|
104
|
+
config_str += f'\t{field_name.ljust(max_len)}: {str(value).ljust(5)} | {doc}\n'
|
|
105
|
+
|
|
106
|
+
return config_str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def stop_is_needed(func: Callable):
|
|
110
|
+
"""Decorator to check if the process must be stopped before proceeding."""
|
|
111
|
+
|
|
112
|
+
@wraps(func)
|
|
113
|
+
def wrapper(self, *args, **kwargs):
|
|
114
|
+
is_running = self.process.GetState() == lldb.eStateRunning
|
|
115
|
+
if is_running:
|
|
116
|
+
self.logger.error(f'Cannot {func.__name__.replace("_", "-")}: Process must be stopped first.')
|
|
117
|
+
return
|
|
118
|
+
return func(self, *args, **kwargs)
|
|
119
|
+
|
|
120
|
+
return wrapper
|
|
121
|
+
|
|
122
|
+
|
|
94
123
|
class HildaClient:
|
|
95
124
|
Breakpoint = namedtuple('Breakpoint', 'address options forced callback')
|
|
96
125
|
|
|
@@ -108,13 +137,7 @@ class HildaClient:
|
|
|
108
137
|
self.registers = Registers(self)
|
|
109
138
|
self.arch = self.target.GetTriple().split('-')[0]
|
|
110
139
|
self.ui_manager = UiManager(self)
|
|
111
|
-
|
|
112
|
-
# within hilda
|
|
113
|
-
self._evaluation_unwind_on_error = True
|
|
114
|
-
|
|
115
|
-
# should ignore breakpoints while evaluation
|
|
116
|
-
self._evaluation_ignore_breakpoints = True
|
|
117
|
-
|
|
140
|
+
self.configs = Configs()
|
|
118
141
|
self._dynamic_env_loaded = False
|
|
119
142
|
self._symbols_loaded = False
|
|
120
143
|
|
|
@@ -144,7 +167,7 @@ class HildaClient:
|
|
|
144
167
|
# convert FDs into int
|
|
145
168
|
return {int(k): v for k, v in result.items()}
|
|
146
169
|
|
|
147
|
-
def bt(self, should_print=
|
|
170
|
+
def bt(self, should_print: bool = False, depth: Optional[int] = None) -> List[Union[str, lldb.SBFrame]]:
|
|
148
171
|
""" Print an improved backtrace. """
|
|
149
172
|
backtrace = []
|
|
150
173
|
for i, frame in enumerate(self.thread.frames):
|
|
@@ -251,6 +274,7 @@ class HildaClient:
|
|
|
251
274
|
globals()['symbols'] = self.symbols
|
|
252
275
|
self._symbols_loaded = True
|
|
253
276
|
|
|
277
|
+
@stop_is_needed
|
|
254
278
|
def poke(self, address, buf: bytes):
|
|
255
279
|
"""
|
|
256
280
|
Write data at given address
|
|
@@ -265,6 +289,7 @@ class HildaClient:
|
|
|
265
289
|
|
|
266
290
|
return retval
|
|
267
291
|
|
|
292
|
+
@stop_is_needed
|
|
268
293
|
def poke_text(self, address: int, code: str) -> int:
|
|
269
294
|
"""
|
|
270
295
|
Write instructions to address.
|
|
@@ -276,6 +301,7 @@ class HildaClient:
|
|
|
276
301
|
bytecode, count = self._ks.asm(code, as_bytes=True)
|
|
277
302
|
return self.poke(address, bytecode)
|
|
278
303
|
|
|
304
|
+
@stop_is_needed
|
|
279
305
|
def peek(self, address, size: int) -> bytes:
|
|
280
306
|
"""
|
|
281
307
|
Read data at given address
|
|
@@ -294,6 +320,7 @@ class HildaClient:
|
|
|
294
320
|
|
|
295
321
|
return retval
|
|
296
322
|
|
|
323
|
+
@stop_is_needed
|
|
297
324
|
def peek_str(self, address: Symbol) -> str:
|
|
298
325
|
"""
|
|
299
326
|
Peek a buffer till null termination
|
|
@@ -302,7 +329,7 @@ class HildaClient:
|
|
|
302
329
|
"""
|
|
303
330
|
return address.po('char *')[1:-1] # strip the ""
|
|
304
331
|
|
|
305
|
-
def stop(self):
|
|
332
|
+
def stop(self, *args) -> None:
|
|
306
333
|
""" Stop process. """
|
|
307
334
|
self.debugger.SetAsync(False)
|
|
308
335
|
|
|
@@ -313,8 +340,10 @@ class HildaClient:
|
|
|
313
340
|
|
|
314
341
|
if not self.process.Stop().Success():
|
|
315
342
|
self.log_critical('failed to stop process')
|
|
343
|
+
else:
|
|
344
|
+
self.log_info('Process Stopped')
|
|
316
345
|
|
|
317
|
-
def cont(self):
|
|
346
|
+
def cont(self, *args) -> None:
|
|
318
347
|
""" Continue process. """
|
|
319
348
|
is_running = self.process.GetState() == lldb.eStateRunning
|
|
320
349
|
|
|
@@ -328,6 +357,8 @@ class HildaClient:
|
|
|
328
357
|
|
|
329
358
|
if not self.process.Continue().Success():
|
|
330
359
|
self.log_critical('failed to continue process')
|
|
360
|
+
else:
|
|
361
|
+
self.log_info('Process Continued')
|
|
331
362
|
|
|
332
363
|
def detach(self):
|
|
333
364
|
"""
|
|
@@ -338,8 +369,12 @@ class HildaClient:
|
|
|
338
369
|
"""
|
|
339
370
|
if not self.process.Detach().Success():
|
|
340
371
|
self.log_critical('failed to detach')
|
|
372
|
+
else:
|
|
373
|
+
self.log_info('Process Detached')
|
|
341
374
|
|
|
342
|
-
|
|
375
|
+
@stop_is_needed
|
|
376
|
+
def disass(self, address: int, buf: bytes, flavor: str = 'intel',
|
|
377
|
+
should_print: bool = False) -> lldb.SBInstructionList:
|
|
343
378
|
"""
|
|
344
379
|
Print disassembly from a given address
|
|
345
380
|
:param flavor:
|
|
@@ -528,14 +563,16 @@ class HildaClient:
|
|
|
528
563
|
self.thread.StepOutOfFrame(self.frame)
|
|
529
564
|
self._bp_frame = None
|
|
530
565
|
|
|
531
|
-
|
|
566
|
+
@stop_is_needed
|
|
567
|
+
def step_into(self, *args):
|
|
532
568
|
""" Step into current instruction. """
|
|
533
569
|
with self.sync_mode():
|
|
534
570
|
self.thread.StepInto()
|
|
535
571
|
if self.ui_manager.active:
|
|
536
572
|
self.ui_manager.show()
|
|
537
573
|
|
|
538
|
-
|
|
574
|
+
@stop_is_needed
|
|
575
|
+
def step_over(self, *args):
|
|
539
576
|
""" Step over current instruction. """
|
|
540
577
|
with self.sync_mode():
|
|
541
578
|
self.thread.StepOver()
|
|
@@ -821,9 +858,9 @@ class HildaClient:
|
|
|
821
858
|
formatted_expression = str(expression)
|
|
822
859
|
|
|
823
860
|
options = lldb.SBExpressionOptions()
|
|
824
|
-
options.SetIgnoreBreakpoints(self.
|
|
861
|
+
options.SetIgnoreBreakpoints(self.configs.evaluation_ignore_breakpoints)
|
|
825
862
|
options.SetTryAllThreads(True)
|
|
826
|
-
options.SetUnwindOnError(self.
|
|
863
|
+
options.SetUnwindOnError(self.configs.evaluation_unwind_on_error)
|
|
827
864
|
|
|
828
865
|
e = self.frame.EvaluateExpression(formatted_expression, options)
|
|
829
866
|
|
|
@@ -847,35 +884,6 @@ class HildaClient:
|
|
|
847
884
|
spec.loader.exec_module(m)
|
|
848
885
|
return m
|
|
849
886
|
|
|
850
|
-
def set_evaluation_unwind(self, value: bool):
|
|
851
|
-
"""
|
|
852
|
-
Set whether LLDB will attempt to unwind the stack whenever an expression evaluation error occurs.
|
|
853
|
-
|
|
854
|
-
Use unwind() to restore when an error is raised in this case.
|
|
855
|
-
"""
|
|
856
|
-
self._evaluation_unwind_on_error = value
|
|
857
|
-
|
|
858
|
-
def get_evaluation_unwind(self) -> bool:
|
|
859
|
-
"""
|
|
860
|
-
Get evaluation unwind state.
|
|
861
|
-
|
|
862
|
-
When this value is True, LLDB will attempt unwinding the stack on evaluation errors.
|
|
863
|
-
Otherwise, the stack frame will remain the same on errors to help you investigate the error.
|
|
864
|
-
"""
|
|
865
|
-
return self._evaluation_unwind_on_error
|
|
866
|
-
|
|
867
|
-
def set_evaluation_ignore_breakpoints(self, value: bool):
|
|
868
|
-
"""
|
|
869
|
-
Set whether to ignore breakpoints while evaluating expressions
|
|
870
|
-
"""
|
|
871
|
-
self._evaluation_ignore_breakpoints = value
|
|
872
|
-
|
|
873
|
-
def get_evaluation_ignore_breakpoints(self) -> bool:
|
|
874
|
-
"""
|
|
875
|
-
Get evaluation "ignore-breakpoints" state.
|
|
876
|
-
"""
|
|
877
|
-
return self._evaluation_ignore_breakpoints
|
|
878
|
-
|
|
879
887
|
def unwind(self) -> bool:
|
|
880
888
|
""" Unwind the stack (useful when get_evaluation_unwind() == False) """
|
|
881
889
|
return self.thread.UnwindInnermostExpression().Success()
|
|
@@ -1016,28 +1024,32 @@ class HildaClient:
|
|
|
1016
1024
|
|
|
1017
1025
|
return value
|
|
1018
1026
|
|
|
1019
|
-
def interact(self, additional_namespace: typing.Mapping = None
|
|
1027
|
+
def interact(self, additional_namespace: Optional[typing.Mapping] = None,
|
|
1028
|
+
startup_files: Optional[List[str]] = None) -> None:
|
|
1020
1029
|
""" Start an interactive Hilda shell """
|
|
1021
1030
|
if not self._dynamic_env_loaded:
|
|
1022
1031
|
self.init_dynamic_environment()
|
|
1023
1032
|
print('\n')
|
|
1024
1033
|
self.log_info(html_to_ansi(GREETING))
|
|
1034
|
+
ipython_config = Config()
|
|
1035
|
+
ipython_config.IPCompleter.use_jedi = True
|
|
1036
|
+
ipython_config.BaseIPythonApplication.profile = 'hilda'
|
|
1037
|
+
ipython_config.InteractiveShellApp.extensions = ['hilda.ipython_extensions.magics',
|
|
1038
|
+
'hilda.ipython_extensions.events',
|
|
1039
|
+
'hilda.ipython_extensions.keybindings']
|
|
1040
|
+
ipython_config.InteractiveShellApp.exec_lines = ["disable_logs()"]
|
|
1041
|
+
if startup_files is not None:
|
|
1042
|
+
ipython_config.InteractiveShellApp.exec_files = startup_files
|
|
1043
|
+
self.log_debug(f'Startup files - {startup_files}')
|
|
1025
1044
|
|
|
1026
|
-
config = Config()
|
|
1027
|
-
config.IPCompleter.use_jedi = True
|
|
1028
|
-
config.InteractiveShellApp.exec_lines = [
|
|
1029
|
-
"""disable_logs()""",
|
|
1030
|
-
"""IPython.get_ipython().events.register('pre_run_cell', self._ipython_run_cell_hook)""",
|
|
1031
|
-
MAGIC_FUNCTIONS,
|
|
1032
|
-
]
|
|
1033
|
-
config.BaseIPythonApplication.profile = 'hilda'
|
|
1034
1045
|
namespace = globals()
|
|
1035
1046
|
namespace.update(locals())
|
|
1036
1047
|
namespace['p'] = self
|
|
1037
1048
|
if additional_namespace is not None:
|
|
1038
1049
|
namespace.update(additional_namespace)
|
|
1039
|
-
|
|
1040
|
-
IPython.start_ipython(config=
|
|
1050
|
+
sys.argv = ['a']
|
|
1051
|
+
IPython.start_ipython(config=ipython_config, user_ns=namespace)
|
|
1052
|
+
self.detach()
|
|
1041
1053
|
|
|
1042
1054
|
@staticmethod
|
|
1043
1055
|
def _add_global(name: str, value: Any, reserved_names=None):
|
|
@@ -1108,35 +1120,6 @@ class HildaClient:
|
|
|
1108
1120
|
|
|
1109
1121
|
return f'((intptr_t(*)({args_type}))({address}))({args_conv})'
|
|
1110
1122
|
|
|
1111
|
-
def _ipython_run_cell_hook(self, info):
|
|
1112
|
-
"""
|
|
1113
|
-
Enable lazy loading for symbols
|
|
1114
|
-
:param info: IPython's CellInfo object
|
|
1115
|
-
"""
|
|
1116
|
-
if info.raw_cell[0] in ['!', '%'] or info.raw_cell.endswith('?'):
|
|
1117
|
-
return
|
|
1118
|
-
|
|
1119
|
-
for node in ast.walk(ast.parse(info.raw_cell)):
|
|
1120
|
-
if not isinstance(node, ast.Name):
|
|
1121
|
-
# we are only interested in names
|
|
1122
|
-
continue
|
|
1123
|
-
|
|
1124
|
-
if node.id in locals() or node.id in globals() or node.id in dir(builtins):
|
|
1125
|
-
# That are undefined
|
|
1126
|
-
continue
|
|
1127
|
-
|
|
1128
|
-
if not hasattr(SymbolsJar, node.id):
|
|
1129
|
-
# ignore SymbolsJar properties
|
|
1130
|
-
try:
|
|
1131
|
-
symbol = getattr(self.symbols, node.id)
|
|
1132
|
-
except SymbolAbsentError:
|
|
1133
|
-
pass
|
|
1134
|
-
else:
|
|
1135
|
-
self._add_global(
|
|
1136
|
-
node.id,
|
|
1137
|
-
symbol if symbol.type_ != lldb.eSymbolTypeObjCMetaClass else self.objc_get_class(node.id)
|
|
1138
|
-
)
|
|
1139
|
-
|
|
1140
1123
|
@staticmethod
|
|
1141
1124
|
def _std_string(value):
|
|
1142
1125
|
if struct.unpack("b", (value + 23).peek(1))[0] >= 0:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import builtins
|
|
3
|
+
|
|
4
|
+
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
|
5
|
+
|
|
6
|
+
from hilda.exceptions import EvaluatingExpressionError, SymbolAbsentError
|
|
7
|
+
from hilda.hilda_client import HildaClient
|
|
8
|
+
from hilda.lldb_importer import lldb
|
|
9
|
+
from hilda.symbols_jar import SymbolsJar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HIEvents:
|
|
13
|
+
def __init__(self, ip: TerminalInteractiveShell):
|
|
14
|
+
self.shell = ip
|
|
15
|
+
self.hilda_client: HildaClient = self.shell.user_ns['p']
|
|
16
|
+
|
|
17
|
+
def pre_run_cell(self, info):
|
|
18
|
+
"""
|
|
19
|
+
Enable lazy loading for symbols
|
|
20
|
+
:param info: IPython's CellInfo object
|
|
21
|
+
"""
|
|
22
|
+
if info.raw_cell[0] in ['!', '%'] or info.raw_cell.endswith('?'):
|
|
23
|
+
return
|
|
24
|
+
for node in ast.walk(ast.parse(info.raw_cell)):
|
|
25
|
+
if not isinstance(node, ast.Name):
|
|
26
|
+
# we are only interested in names
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if node.id in locals() or node.id in globals() or node.id in dir(builtins):
|
|
30
|
+
# That are undefined
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if not hasattr(SymbolsJar, node.id):
|
|
34
|
+
# ignore SymbolsJar properties
|
|
35
|
+
try:
|
|
36
|
+
symbol = getattr(self.hilda_client.symbols, node.id)
|
|
37
|
+
except SymbolAbsentError:
|
|
38
|
+
pass
|
|
39
|
+
else:
|
|
40
|
+
try:
|
|
41
|
+
self.hilda_client._add_global(
|
|
42
|
+
node.id,
|
|
43
|
+
symbol if symbol.type_ != lldb.eSymbolTypeObjCMetaClass else self.hilda_client.objc_get_class(
|
|
44
|
+
node.id)
|
|
45
|
+
)
|
|
46
|
+
except EvaluatingExpressionError:
|
|
47
|
+
self.hilda_client.log_warning(
|
|
48
|
+
f'Process is running. Pause execution in order to resolve "{node.id}"')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_ipython_extension(ip: TerminalInteractiveShell):
|
|
52
|
+
hie = HIEvents(ip)
|
|
53
|
+
ip.events.register('pre_run_cell', hie.pre_run_cell)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from prompt_toolkit.enums import DEFAULT_BUFFER
|
|
2
|
+
from prompt_toolkit.filters import EmacsInsertMode, HasFocus, HasSelection, ViInsertMode
|
|
3
|
+
from prompt_toolkit.keys import Keys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_ipython_extension(ipython):
|
|
7
|
+
def register_keybindings():
|
|
8
|
+
hilda = ipython.user_ns['p']
|
|
9
|
+
keys_mapping = {Keys.F7: hilda.step_into,
|
|
10
|
+
Keys.F8: hilda.step_over,
|
|
11
|
+
Keys.F9: hilda.cont,
|
|
12
|
+
Keys.F10: hilda.stop}
|
|
13
|
+
|
|
14
|
+
insert_mode = ViInsertMode() | EmacsInsertMode()
|
|
15
|
+
registry = ipython.pt_app.key_bindings
|
|
16
|
+
|
|
17
|
+
for key, callback in keys_mapping.items():
|
|
18
|
+
registry.add_binding(key, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode))(
|
|
19
|
+
callback)
|
|
20
|
+
|
|
21
|
+
register_keybindings()
|
|
22
|
+
ipython.events.register('shell_initialized', register_keybindings)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
|
|
3
|
+
from IPython.core.magic import Magics, line_magic, magics_class, needs_local_scope
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@magics_class
|
|
7
|
+
class HIMagics(Magics):
|
|
8
|
+
|
|
9
|
+
@line_magic
|
|
10
|
+
@needs_local_scope
|
|
11
|
+
def objc(self, line, local_ns=None):
|
|
12
|
+
"""Load an Objective-C class by name into the IPython session."""
|
|
13
|
+
p = local_ns.get('p')
|
|
14
|
+
class_name = line.strip()
|
|
15
|
+
if not class_name:
|
|
16
|
+
p.log_error("Error: className is required.")
|
|
17
|
+
return
|
|
18
|
+
try:
|
|
19
|
+
local_ns[class_name] = p.objc_get_class(class_name)
|
|
20
|
+
p.log_info(f'{class_name} class loaded successfully')
|
|
21
|
+
except Exception as e:
|
|
22
|
+
p.log_error(f'Error loading class {class_name}: {str(e)}')
|
|
23
|
+
|
|
24
|
+
@line_magic
|
|
25
|
+
@needs_local_scope
|
|
26
|
+
def fbp(self, line, local_ns=None):
|
|
27
|
+
"""Set a file breakpoint in the debugger."""
|
|
28
|
+
p = local_ns.get('p')
|
|
29
|
+
try:
|
|
30
|
+
module_name, address = shlex.split(line.strip())
|
|
31
|
+
address = int(address, 16)
|
|
32
|
+
p.file_symbol(address, module_name).bp()
|
|
33
|
+
except ValueError as ve:
|
|
34
|
+
p.log_error(f"Error parsing arguments: {str(ve)}")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
p.log_error(f"Error setting breakpoint: {str(e)}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_ipython_extension(ipython):
|
|
40
|
+
ipython.register_magics(HIMagics)
|