def symbols_jar(self) -> SymbolsJar: """ Get a SymbolsJar object for quick operations on all methods """ jar = SymbolsJar.create(self._client) for m in self.methods: jar[f'[{self.name} {m.name}]'] = m.address return jar
def __init__(self, debugger: lldb.SBDebugger): self.logger = logging.getLogger(__name__) self.endianness = '<' self.debugger = debugger self.target = debugger.GetSelectedTarget() self.process = self.target.GetProcess() self.symbols = SymbolsJar() self.symbols.set_hilda_client(self) self.breakpoints = {} self.captured_objects = {} self.registers = Registers(self) self._dynamic_env_loaded = False self._symbols_loaded = False # the frame called within the context of the hit BP self._bp_frame = None self._add_global('symbols', self.symbols, []) self._add_global('registers', self.registers, []) self.log_info(f'Target: {self.target}') self.log_info(f'Process: {self.process}')
def inject(self, filename): """ Inject a single library into currently running process :param filename: :return: module object """ module = self.target.FindModule( lldb.SBFileSpec(os.path.basename(filename), False)) if module.file.basename is not None: self.log_warning(f'file {filename} has already been loaded') injected = SymbolsJar() handle = self.symbols.dlopen(filename, 10) # RTLD_GLOBAL|RTLD_NOW if handle == 0: self.log_critical(f'failed to inject: {filename}') module = self.target.FindModule( lldb.SBFileSpec(os.path.basename(filename), False)) for symbol in module.symbols: load_addr = symbol.addr.GetLoadAddress(self.target) if load_addr == 0xffffffffffffffff: # skip those not having a real address continue name = symbol.name type_ = symbol.GetType() if name in ('<redacted>', ) or ( type_ not in (lldb.eSymbolTypeCode, lldb.eSymbolTypeData, lldb.eSymbolTypeObjCMetaClass)): # ignore unnamed symbols and those which are not: data, code or objc classes continue injected[name] = self.symbol(load_addr) return injected
class HildaClient(metaclass=CommandsMeta): Breakpoint = namedtuple('Breakpoint', 'address options forced') RETVAL_BIT_COUNT = 64 def __init__(self, debugger: lldb.SBDebugger): self.logger = logging.getLogger(__name__) self.endianness = '<' self.debugger = debugger self.target = debugger.GetSelectedTarget() self.process = self.target.GetProcess() self.symbols = SymbolsJar() self.symbols.set_hilda_client(self) self.breakpoints = {} self.captured_objects = {} self.registers = Registers(self) self._dynamic_env_loaded = False self._symbols_loaded = False # the frame called within the context of the hit BP self._bp_frame = None self._add_global('symbols', self.symbols, []) self._add_global('registers', self.registers, []) self.log_info(f'Target: {self.target}') self.log_info(f'Process: {self.process}') @command() def hd(self, buf): """ Print an hexdump of given buffer :param buf: buffer to print in hexdump form """ print(hexdump.hexdump(buf)) @command() def lsof(self) -> dict: """ Get dictionary of all open FDs :return: Mapping between open FDs and their paths """ with open(os.path.join(Path(__file__).resolve().parent, 'lsof.m'), 'r') as f: result = json.loads(self.po(f.read())) # convert FDs into int return {int(k): v for k, v in result.items()} @command() def bt(self): """ Print an improved backtrace. """ for i, frame in enumerate(self.thread.frames): row = '' row += html_to_ansi( f'<span style="color: cyan">0x{frame.addr.GetFileAddress():x}</span> ' ) row += str(frame) if i == 0: # first line row += ' 👈' print(row) @command() def disable_jetsam_memory_checks(self): """ Disable jetsam memory checks, prevent raising: `error: Execution was interrupted, reason: EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=15 MB, unused=0x0).` when evaluating expression. """ # 6 is for MEMORYSTATUS_CMD_SET_JETSAM_TASK_LIMIT result = self.symbols.memorystatus_control(6, self.process.GetProcessID(), 0, 0, 0) if result: raise DisableJetsamMemoryChecksError() @command() def symbol(self, address): """ Get symbol object for a given address :param address: :return: Hilda's symbol object """ return Symbol.create(address, self) @command() def objc_symbol(self, address) -> ObjectiveCSymbol: """ Get objc symbol wrapper for given address :param address: :return: Hilda's objc symbol object """ try: return ObjectiveCSymbol.create(int(address), self) except HildaException as e: raise CreatingObjectiveCSymbolError from e @command() def inject(self, filename): """ Inject a single library into currently running process :param filename: :return: module object """ module = self.target.FindModule( lldb.SBFileSpec(os.path.basename(filename), False)) if module.file.basename is not None: self.log_warning(f'file {filename} has already been loaded') injected = SymbolsJar() handle = self.symbols.dlopen(filename, 10) # RTLD_GLOBAL|RTLD_NOW if handle == 0: self.log_critical(f'failed to inject: {filename}') module = self.target.FindModule( lldb.SBFileSpec(os.path.basename(filename), False)) for symbol in module.symbols: load_addr = symbol.addr.GetLoadAddress(self.target) if load_addr == 0xffffffffffffffff: # skip those not having a real address continue name = symbol.name type_ = symbol.GetType() if name in ('<redacted>', ) or ( type_ not in (lldb.eSymbolTypeCode, lldb.eSymbolTypeData, lldb.eSymbolTypeObjCMetaClass)): # ignore unnamed symbols and those which are not: data, code or objc classes continue injected[name] = self.symbol(load_addr) return injected @command() def rebind_symbols(self, image_range=None, filename_expr=''): """ Reparse all loaded images symbols :param image_range: index range for images to load in the form of [start, end] :param filename_expr: filter only images containing given expression """ self.log_debug('mapping symbols') self._symbols_loaded = False for i, module in enumerate(tqdm(self.target.modules)): filename = module.file.basename if filename_expr not in filename: continue if image_range is not None and (i < image_range[0] or i > image_range[1]): continue for symbol in module: with suppress(AddingLldbSymbolError): self.add_lldb_symbol(symbol) globals()['symbols'] = self.symbols self._symbols_loaded = True @command() def poke(self, address, buf: bytes): """ Write data at given address :param address: :param buf: """ err = lldb.SBError() retval = self.process.WriteMemory(address, buf, err) if not err.Success(): try: self.log_critical(str(err)) except HildaException as e: raise AccessingMemoryError from e return retval @command() def peek(self, address, size) -> bytes: """ Read data at given address :param address: :param size: :return: """ err = lldb.SBError() retval = self.process.ReadMemory(address, int(size), err) if not err.Success(): try: self.log_critical(str(err)) except HildaException as e: raise AccessingMemoryError from e return retval @command() def peek_str(self, address, encoding=None): """ Peek a buffer till null termination :param address: :param encoding: character encoding. if None, bytes is returned :return: """ if hasattr(self.symbols, 'strlen'): # always prefer using native strlen buf = self.peek(address, self.symbols.strlen(address)) else: buf = self.peek(address, 1) while buf[-1] != 0: buf += self.peek(address + len(buf), 1) # remove null terminator buf = buf[:-1] if encoding is not None: buf = str(buf, encoding) return buf @command() def stop(self): """ Stop process. """ self.debugger.SetAsync(False) is_running = self.process.GetState() == lldb.eStateRunning if not is_running: self.log_debug('already stopped') return if not self.process.Stop().Success(): self.log_critical('failed to stop process') @command() def cont(self): """ Continue process. """ is_running = self.process.GetState() == lldb.eStateRunning if is_running: self.log_debug('already running') return # bugfix: the debugger may become in sync state, so we make sure # it isn't before trying to continue self.debugger.SetAsync(True) if not self.process.Continue().Success(): self.log_critical('failed to continue process') @command() def detach(self): """ Detach from process. Useful in order to exit gracefully so process doesn't get killed while you exit """ if not self.process.Detach().Success(): self.log_critical('failed to detach') @command() def disass(self, address, buf, should_print=True) -> lldb.SBInstructionList: """ Print disassembly from a given address :param address: :param buf: :param should_print: :return: """ inst = self.target.GetInstructions( lldb.SBAddress(address, self.target), buf) if should_print: print(inst) return inst @command() def file_symbol(self, address) -> Symbol: """ Calculate symbol address without ASLR :param address: address as can be seen originally in Mach-O """ return self.symbol( self.target.ResolveFileAddress(address).GetLoadAddress( self.target)) @command() def get_register(self, name) -> Symbol: """ Get value for register by its name :param name: :return: """ register = self.frame.register[name.lower()] if register is None: raise AccessingRegisterError() return self.symbol(register.unsigned) @command() def set_register(self, name, value): """ Set value for register by its name :param name: :param value: :return: """ register = self.frame.register[name.lower()] if register is None: raise AccessingRegisterError() register.value = hex(value) @command() def objc_call(self, obj, selector, *params): """ Simulate a call to an objc selector :param obj: obj to pass into `objc_msgSend` :param selector: selector to execute :param params: any other additional parameters the selector requires :return: invocation returned value """ # On object `obj` args = self._serialize_call_params([obj]) # Call selector (by its uid) args.append( self._generate_call_expression( self.symbols.sel_getUid, self._serialize_call_params([selector]))) # With params args.extend(self._serialize_call_params(params)) call_expression = self._generate_call_expression( self.symbols.objc_msgSend, args) with self.stopped(): return self.evaluate_expression(call_expression) @command() def call(self, address, argv: list = None): """ Call function at given address with given parameters :param address: :param argv: parameter list :return: function's return value """ if argv is None: argv = [] call_expression = self._generate_call_expression( address, self._serialize_call_params(argv)) with self.stopped(): return self.evaluate_expression(call_expression) @command() def monitor(self, address, **options) -> lldb.SBBreakpoint: """ Monitor every time a given address is called The following options are available: regs={reg1: format} will print register values Available formats: x: hex s: string cf: use CFCopyDescription() to get more informative description of the object po: use LLDB po command For example: regs={'x0': 'x'} -> x0 will be printed in HEX format retval=True print function's return value stop=True force a stop at every hit bt=True print backtrace cmd=[cmd1, cmd2] run several LLDB commands, one by another force_return=value force a return from function with the specified value name=some_value use `some_name` instead of the symbol name automatically extracted from the calling frame :param address: :param options: :return: """ def callback(hilda, frame, bp_loc, options): def format_value(format, value): formatters = { 'x': lambda value: f'0x{int(value):x}', 's': lambda value: value.peek_str(), 'cf': lambda value: value.cf_description, 'po': lambda value: value.po(), } if format in formatters: return formatters[format](value) else: return f'{value:x} (unsupported format)' bp = bp_loc.GetBreakpoint() symbol = frame.GetSymbol() symbol_address = symbol.addr.GetLoadAddress(hilda.target) symbol_name = symbol.GetName() name = symbol_name if options.get('name', False): name = options['name'] log_message = f'🚨 #{bp.id} 0x{symbol_address:x} {name}' if 'regs' in options: log_message += '\nregs:' for name, format in options['regs'].items(): value = hilda.symbol(frame.FindRegister(name).unsigned) log_message += f'\n\t{name} = {format_value(format, value)}' if options.get('force_return', False): hilda.force_return(options['force_return']) log_message += f'\nforced return: {options["force_return"]}' if options.get('bt', False): # bugfix: for callstacks from xpc events hilda.finish() hilda.bt() if options.get('retval', False): # return from function hilda.finish() value = hilda.get_register('x0') log_message += f'\nreturned: {format_value(options["retval"], value)}' hilda.log_info(log_message) for cmd in options.get('cmd', []): hilda.lldb_handle_command(cmd) if not options.get('stop', False): hilda.cont() return self.bp(address, callback, **options) @command() def finish(self): """ Run current frame till its end. """ with self.sync_mode(): self.thread.StepOutOfFrame(self.frame) self._bp_frame = None @command() def step_into(self): """ Step into current instruction. """ with self.sync_mode(): self.thread.StepInto() @command() def step_over(self): """ Step over current instruction. """ with self.sync_mode(): self.thread.StepOver() @command() def remove_all_hilda_breakpoints(self, remove_forced=False): """ Remove all breakpoints created by Hilda :param remove_forced: include removed of "forced" breakpoints """ breakpoints = list(self.breakpoints.items()) for bp_id, bp in breakpoints: if remove_forced or not bp.forced: self.remove_hilda_breakpoint(bp_id) @command() def remove_hilda_breakpoint(self, bp_id): """ Remove a single breakpoint placed by Hilda :param bp_id: Breakpoint's ID """ self.target.BreakpointDelete(bp_id) del self.breakpoints[bp_id] self.log_info(f'BP #{bp_id} has been removed') @command() def force_return(self, value=0): """ Prematurely return from a stack frame, short-circuiting exection of newer frames and optionally yielding a specified value. :param value: :return: """ self.finish() self.set_register('x0', value) @command() def proc_info(self): """ Print information about currently running mapped process. """ print(self.process) @command() def print_proc_entitlements(self): """ Get the plist embedded inside the process' __LINKEDIT section. """ linkedit_section = self.target.modules[0].FindSection('__LINKEDIT') linkedit_data = self.symbol( linkedit_section.GetLoadAddress(self.target)).peek( linkedit_section.size) # just look for the xml start inside the __LINKEDIT section. should be good enough since wer'e not # expecting any other XML there entitlements = str( linkedit_data[linkedit_data.find(b'<?xml'):].split(b'\xfa', 1)[0], 'utf8') print(highlight(entitlements, XmlLexer(), TerminalTrueColorFormatter())) @command() def bp(self, address, callback=None, forced=False, **options) -> lldb.SBBreakpoint: """ Add a breakpoint :param address: :param callback: callback(hilda, *args) to be called :param forced: whether the breakpoint should be protected frm usual removal. :param options: :return: """ if address in [bp.address for bp in self.breakpoints.values()]: if prompts.prompt_for_confirmation( 'A breakpoint already exist in given location. ' 'Would you like to delete the previous one?', True): breakpoints = list(self.breakpoints.items()) for bp_id, bp in breakpoints: if address == bp.address: self.remove_hilda_breakpoint(bp_id) bp = self.target.BreakpointCreateByAddress(address) setattr(bp, 'hilda', self) # add into Hilda's internal list of breakpoints self.breakpoints[bp.id] = HildaClient.Breakpoint(address=address, options=options, forced=forced) if callback is not None: callback_source = '' callback_source_lines = inspect.getsource(callback).split('\n') def_offset = callback_source_lines[0].index('def ') for line in callback_source_lines: line = line.replace('\t', ' ') callback_source += line[def_offset:] + '\n' callback_source += f'\n' callback_source += f'lldb.hilda_client._bp_frame = frame\n' callback_source += f'{callback.__name__}(lldb.hilda_client, frame, bp_loc, {repr(options)})\n' callback_source += f'lldb.hilda_client._bp_frame = None\n' err = bp.SetScriptCallbackBody(callback_source) if not err.Success(): self.log_critical( f'failed to set breakpoint script body: {err}') self.log_info(f'Breakpoint #{bp.id} has been set') return bp @command() def show_hilda_breakpoints(self): """ Show existing breakpoints created by Hilda. """ for bp_id, bp in self.breakpoints.items(): print(f'🚨 Breakpoint #{bp_id}: Forced: {bp.forced}') print(f'\tAddress: 0x{bp.address:x}') print(f'\tOptions: {bp.options}') @command() def show_commands(self): """ Show available commands. """ for command_name, command_func in self.commands: doc = docstring_parser.parse(command_func.__doc__) print(f'👾 {command_name} - {doc.short_description}') if doc.long_description: print(textwrap.indent(doc.long_description, ' ')) @command() def save(self, filename=None): """ Save loaded symbols map (for loading later using the load() command) :param filename: optional filename for where to store """ if filename is None: filename = self._get_saved_state_filename() self.log_info(f'saving current state info: {filename}') with open(filename, 'wb') as f: symbols_copy = {} for k, v in self.symbols.items(): # converting the symbols into serializable objects symbols_copy[k] = SerializableSymbol(address=int(v), type_=v.type_, filename=v.filename) pickle.dump(symbols_copy, f) @command() def load(self, filename=None): """ Load an existing symbols map (previously saved by the save() command) :param filename: filename to load from """ if filename is None: filename = self._get_saved_state_filename() self.log_info(f'loading current state from: {filename}') with open(filename, 'rb') as f: symbols_copy = pickle.load(f) for k, v in tqdm(symbols_copy.items()): self.symbols[k] = self.symbol(v.address) # perform sanity test for symbol rand if self.symbols.rand() == 0 and self.symbols.rand() == 0: # rand returning 0 twice means the loaded file is probably outdated raise BrokenLocalSymbolsJarError() # assuming the first main image will always change self.rebind_symbols(image_range=[0, 0]) self.init_dynamic_environment() self._symbols_loaded = True @command() def po(self, expression, cast=None): """ Print given object using LLDB's po command Can also run big chunks of native code: po('NSMutableString *s = [NSMutableString string]; [s appendString:@"abc"]; [s description]') :param expression: either a symbol or string the execute :param cast: object type :raise EvaluatingExpressionError: LLDB failed to evaluate the expression :return: LLDB's po output """ casted_expression = '' if cast is not None: casted_expression += '(%s)' % cast casted_expression += f'0x{expression:x}' if isinstance( expression, int) else str(expression) res = lldb.SBCommandReturnObject() self.debugger.GetCommandInterpreter().HandleCommand( f'expression -i 0 -lobjc -O -- {casted_expression}', res) if not res.Succeeded(): raise EvaluatingExpressionError(res.GetError()) return res.GetOutput().strip() @command() def globalize_symbols(self): """ Make all symbols in python's global scope """ reserved_names = list(globals().keys()) + dir(builtins) for name, value in tqdm(self.symbols.items()): if ':' not in name \ and '[' not in name \ and '<' not in name \ and '(' not in name \ and '.' not in name: self._add_global(name, value, reserved_names) @command() def lldb_handle_command(self, cmd): """ Execute an LLDB command For example: lldb_handle_command('register read') :param cmd: """ self.debugger.HandleCommand(cmd) @command() def objc_get_class(self, name) -> objective_c_class.Class: """ Get ObjC class object :param name: :return: """ return objective_c_class.Class.from_class_name(self, name) @command() def CFSTR(self, s): """ Create CFStringRef object from given string :param s: given string :return: """ return self.cf(s) @command() def cf(self, data) -> Symbol: """ Create CFObject from given data :param data: Data representing the CFObject, must by JSON serializable :return: Pointer to a CFObject """ try: json_data = json.dumps(data) except TypeError as e: raise ConvertingToCfObjectError from e return self.evaluate_expression(''' @import Foundation; NSString *s = @"{{\\"root\\": {} }}"; NSData *jsonData = [s dataUsingEncoding:NSUTF8StringEncoding]; NSError *error; NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error: &error]; [jsonObject objectForKey:@"root"]; '''.format(json_data.replace('"', r'\"'))) @command() def evaluate_expression(self, expression) -> Symbol: """ Wrapper for LLDB's EvaluateExpression. Used for quick code snippets. Feel free to use local variables inside the expression using format string. For example: currentDevice = objc_get_class('UIDevice').currentDevice evaluate_expression(f'[[{currentDevice} systemName] hasPrefix:@"2"]') :param expression: :return: returned symbol """ # prepending a prefix so LLDB knows to return an int type if isinstance(expression, int): formatted_expression = f'(intptr_t)0x{expression:x}' else: formatted_expression = str(expression) options = lldb.SBExpressionOptions() options.SetIgnoreBreakpoints(True) options.SetTryAllThreads(True) e = self.target.EvaluateExpression(formatted_expression) if not e.error.Success(): raise EvaluatingExpressionError(str(e.error)) return self.symbol(e.unsigned) @property def thread(self): """ Current active thread. """ if self._bp_frame is not None: return self._bp_frame.GetThread() return self.process.GetSelectedThread() @property def frame(self): """ Current active frame. """ if self._bp_frame is not None: return self._bp_frame return self.thread.GetSelectedFrame() @contextmanager def stopped(self): """ Context-Manager for execution while process is stopped. """ is_running = self.process.GetState() == lldb.eStateRunning if is_running: self.stop() try: yield finally: if is_running: self.cont() @contextmanager def safe_malloc(self, size): """ Context-Manager for allocating a block of memory which is freed afterwards :param size: :return: """ block = self.symbols.malloc(size) if block == 0: raise IOError(f'failed to allocate memory of size: {size} bytes') try: yield block finally: self.symbols.free(block) @contextmanager def sync_mode(self): """ Context-Manager for execution while LLDB is in sync mode. """ is_async = self.debugger.GetAsync() self.debugger.SetAsync(False) try: yield finally: self.debugger.SetAsync(is_async) def init_dynamic_environment(self): """ Init session-scoped process dynamic dependencies. """ self.log_debug('init dynamic environment') self._dynamic_env_loaded = True self.log_debug('disable mach_msg receive errors') try: CFRunLoopServiceMachPort_hooks.disable_mach_msg_errors(self) except SymbolAbsentError: self.log_warning('failed to disable mach_msg errors') objc_code = """ @import ObjectiveC; @import Foundation; """ try: self.po(objc_code) except EvaluatingExpressionError: # first time is expected to fail. bug in LLDB? pass def log_warning(self, message): """ Log at warning level """ self.logger.warning(message) def log_debug(self, message): """ Log at debug level """ self.logger.debug(message) def log_error(self, message): """ Log at error level """ self.logger.error(message) def log_critical(self, message): """ Log at critical level """ self.logger.critical(message) raise HildaException(message) def log_info(self, message): """ Log at info level """ self.logger.info(message) def add_lldb_symbol(self, symbol: lldb.SBSymbol) -> Symbol: """ Convert an LLDB symbol into Hilda's symbol object and insert into `symbols` global :param symbol: LLDB symbol :return: converted symbol :raise AddingLldbSymbolError: Hilda failed to convert the LLDB symbol. """ load_addr = symbol.addr.GetLoadAddress(self.target) if load_addr == 0xffffffffffffffff: # skip those not having a real address raise AddingLldbSymbolError() name = symbol.name type_ = symbol.GetType() if name in ('<redacted>', ) or (type_ not in ( lldb.eSymbolTypeCode, lldb.eSymbolTypeRuntime, lldb.eSymbolTypeData, lldb.eSymbolTypeObjCMetaClass)): # ignore unnamed symbols and those which are not in a really used type raise AddingLldbSymbolError() value = self.symbol(load_addr) # add it into symbols global self.symbols[name] = value self.symbols[f'{name}{{{value.filename}}}'] = value return value def interactive(self): """ Start an interactive Hilda shell """ if not self._dynamic_env_loaded: self.init_dynamic_environment() self._globalize_commands() print('\n') self.log_info(html_to_ansi(GREETING)) c = Config() c.IPCompleter.use_jedi = False c.InteractiveShellApp.exec_lines = [ '''IPython.get_ipython().events.register('pre_run_cell', self._ipython_run_cell_hook)''' ] namespace = globals() namespace.update(locals()) IPython.start_ipython(config=c, user_ns=namespace) @staticmethod def is_objc_type(symbol: Symbol) -> bool: """ Test if a given symbol represents an objc object :param symbol: :return: """ # Tagged pointers are ObjC objects if symbol & OBJC_TAG_MASK == OBJC_TAG_MASK: return True # Class are not ObjC objects for mask, value in ISA_MAGICS: if symbol & mask == value: return False try: with symbol.change_item_size(8): isa = symbol[0] except HildaException: return False for mask, value in ISA_MAGICS: if isa & mask == value: return True return False @staticmethod def _add_global(name, value, reserved_names=None): if reserved_names is None or name not in reserved_names: # don't override existing symbols globals()[name] = value @staticmethod def _get_saved_state_filename(): return '/tmp/cache.hilda' def _serialize_call_params(self, argv): args_conv = [] for arg in argv: if isinstance(arg, str) or isinstance(arg, bytes): if isinstance(arg, str): arg = arg.encode() arg = ''.join([f'\\x{b:02x}' for b in arg]) args_conv.append(f'(intptr_t)"{arg}"') elif isinstance(arg, int) or isinstance(arg, Symbol): args_conv.append(f'0x{int(arg):x}') else: raise NotImplementedError('cannot serialize argument') return args_conv @staticmethod def _generate_call_expression(address, params): args_type = ','.join(['intptr_t'] * len(params)) args_conv = ','.join(params) return f'((intptr_t(*)({args_type}))({address}))({args_conv})' def _globalize_commands(self): """ Make all command available in global scope. """ reserved_names = list(globals().keys()) + dir(builtins) for command_name, function in self.commands: command_func = partial(function, self) command_func.__doc__ = function.__doc__ self._add_global(command_name, command_func, reserved_names) def _ipython_run_cell_hook(self, info): """ Enable lazy loading for symbols :param info: IPython's CellInfo object """ for node in ast.walk(ast.parse(info.raw_cell)): if not isinstance(node, ast.Name): # we are only intereseted in names continue if node.id in locals() or node.id in globals() or node.id in dir( builtins): # That are undefined continue try: symbol = getattr(self.symbols, node.id) except SymbolAbsentError: pass else: self._add_global( node.id, symbol if symbol.type_ != lldb.eSymbolTypeObjCMetaClass else self.objc_get_class(node.id))