From 59ad189f97c4731706e6b5e48ec3c7be28b78606 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Wed, 3 Jan 2018 11:47:42 -0800 Subject: [PATCH 1/6] Implement findinstances command --- commands/FBDebugCommands.py | 88 +++++++++++++++++++++++++++++++++++++ fblldbbase.py | 12 +++++ 2 files changed, 100 insertions(+) diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index 3856c61..57e5d78 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -4,6 +4,8 @@ import fblldbbase as fb import fblldbobjcruntimehelpers as objc +import sys +import os import re def lldbcommands(): @@ -12,6 +14,7 @@ def lldbcommands(): FBFrameworkAddressBreakpointCommand(), FBMethodBreakpointCommand(), FBMemoryWarningCommand(), + FBFindInstancesCommand(), ] class FBWatchInstanceVariableCommand(fb.FBCommand): @@ -184,3 +187,88 @@ def description(self): def run(self, arguments, options): fb.evaluateEffect('[[UIApplication sharedApplication] performSelector:@selector(_performMemoryWarning)]') + + +class FBFindInstancesCommand(fb.FBCommand): + def name(self): + return 'findinstances' + + def description(self): + return """ + Find instances of specified ObjC classes. + + This command scans memory and uses heuristics to identify instances of + Objective-C classes. This includes Swift classes that descend from NSObject. + + Basic examples: + + findinstances UIScrollView + findinstances *UIScrollView + findinstances UIScrollViewDelegate + + These basic searches find instances of the given class or protocol. By + default, subclasses of the class or protocol are included in the results. To + find exact class instances, add a `*` prefix, for example: *UIScrollView. + + Advanced examples: + + # Find views that are either: hidden, invisible, or not in a window + findinstances UIView hidden == true || alpha == 0 || window == nil + # Find views that have either a zero width or zero height + findinstances UIView layer.bounds.#size.width == 0 || layer.bounds.#size.height == 0 + # Find leaf views that have no subviews + findinstances UIView subviews.@count == 0 + # Find dictionaries that have keys that might be passwords or passphrases + findinstances NSDictionary any @allKeys beginswith 'pass' + + These examples make use of a filter. The filter is implemented with + NSPredicate, see its documentaiton for more details. Basic NSPredicate + expressions have relatively predicatable syntax. There are some exceptions + as seen above, see https://github.com/facebook/chisel/wiki/findinstances. + """ + + def run(self, arguments, options): + if not self.loadChiselIfNecessary(): + return + + commands = arguments[0].strip().split(' ', 1) + query = commands[0] + if len(commands) > 1: + # TODO: Escape unescaped double quotes, and escape escapes + predicate = commands[1].strip() + else: + predicate = '' + call = '(void)PrintInstances("{}", "{}")'.format(query, predicate) + fb.evaluateExpressionValue(call) + + def loadChiselIfNecessary(self): + target = lldb.debugger.GetSelectedTarget() + if target.module['Chisel']: + return True + + path = self.chiselLibraryPath() + if not os.path.exists(path): + print 'Chisel library missing: ' + path + return False + + module = fb.evaluateExpressionValue('(void*)dlopen("{}", 2)'.format(path)) + if module.unsigned != 0 or target.module['Chisel']: + return True + + error = fb.evaluateExpressionValue('(char*)dlerror()') + if error.unsigned != 0: + print 'Error loading Chisel: ' + error.summary + else: + print 'Unknown error loading Chisel' + return False + + def chiselLibraryPath(self): + # script os.environ['CHISEL_LIBRARY_PATH'] = '/path/to/custom/Chisel' + path = os.getenv('CHISEL_LIBRARY_PATH') + if path and os.path.exists(path): + return path + + source_path = sys.modules[__name__].__file__ + source_dir = os.path.dirname(source_path) + # ugh: ../.. is to back out of commands/, then back out of libexec/ + return os.path.join(source_dir, '..', '..', 'lib', 'Chisel.framework', 'Chisel') diff --git a/fblldbbase.py b/fblldbbase.py index 40d393a..a9645e7 100755 --- a/fblldbbase.py +++ b/fblldbbase.py @@ -42,7 +42,19 @@ def evaluateExpressionValue(expression, printErrors=True, language=lldb.eLanguag frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame() options = lldb.SBExpressionOptions() options.SetLanguage(language) + + # Allow evaluation that contains a @throw/@catch. + # By default, ObjC @throw will cause evaluation to be aborted. At the time + # of a @throw, it's not known if the exception will be handled by a @catch. + # An exception that's caught, should not cause evaluation to fail. options.SetTrapExceptions(False) + + # Give evaluation more time. + options.SetTimeoutInMicroSeconds(5000000) # 5s + + # Chisel commands are not multithreaded. + options.SetTryAllThreads(False) + value = frame.EvaluateExpression(expression, options) error = value.GetError() From 5e158e0a70f38e381bae243bc73bcc205aeb306b Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Wed, 3 Jan 2018 14:09:07 -0800 Subject: [PATCH 2/6] Improve findinstances argument parsing --- commands/FBDebugCommands.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index 57e5d78..f9e5ff5 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -231,11 +231,14 @@ def run(self, arguments, options): if not self.loadChiselIfNecessary(): return - commands = arguments[0].strip().split(' ', 1) - query = commands[0] - if len(commands) > 1: - # TODO: Escape unescaped double quotes, and escape escapes - predicate = commands[1].strip() + # Unpack the arguments by hand. The input is entirely in arguments[0]. + args = arguments[0].strip().split(' ', 1) + + query = args[0] + if len(args) > 1: + predicate = args[1].strip() + # Escape double quotes and backslashes. + predicate = re.sub('([\\"])', r'\\\1', predicate) else: predicate = '' call = '(void)PrintInstances("{}", "{}")'.format(query, predicate) From 76dfc8aa0c712eac2f7de49316aa7319306ae7f0 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Thu, 4 Jan 2018 10:16:02 -0800 Subject: [PATCH 3/6] Check errno to diagnose dlopen --- commands/FBDebugCommands.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index f9e5ff5..f8c8307 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -259,10 +259,18 @@ def loadChiselIfNecessary(self): return True error = fb.evaluateExpressionValue('(char*)dlerror()') + errno = fb.evaluateExpressionValue('(int)errno') if error.unsigned != 0: print 'Error loading Chisel: ' + error.summary + elif errno.unsigned != 0: + error = fb.evaluateExpressionValue('(char*)strerror((int)errno)') + if error.unsigned != 0: + print 'Error loading Chisel: ' + error.summary + else: + print 'Error loading Chisel (errno {})'.format(errno) else: print 'Unknown error loading Chisel' + return False def chiselLibraryPath(self): From 461143a50fbf4c20f713346e8762a86149567064 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Thu, 4 Jan 2018 12:19:01 -0800 Subject: [PATCH 4/6] Handle errno better; catch codesign errors --- commands/FBDebugCommands.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index f8c8307..fc81600 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -258,12 +258,17 @@ def loadChiselIfNecessary(self): if module.unsigned != 0 or target.module['Chisel']: return True + # `errno` is a macro that expands to a call to __error(). In development, + # lldb was not getting a correct value for `errno`, so `__error()` is used. + errno = fb.evaluateExpressionValue('*(int*)__error()').value error = fb.evaluateExpressionValue('(char*)dlerror()') - errno = fb.evaluateExpressionValue('(int)errno') - if error.unsigned != 0: + if errno == 50: + # KERN_CODESIGN_ERROR from + print 'Error loading Chisel: Code signing failure; Must re-run codesign' + elif error.unsigned != 0: print 'Error loading Chisel: ' + error.summary - elif errno.unsigned != 0: - error = fb.evaluateExpressionValue('(char*)strerror((int)errno)') + elif errno != 0: + error = fb.evaluateExpressionValue('(char*)strerror({})'.format(errno)) if error.unsigned != 0: print 'Error loading Chisel: ' + error.summary else: From 4ae2ba7afbbd84967883c7aa496121508f0d3dc7 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Thu, 4 Jan 2018 12:20:52 -0800 Subject: [PATCH 5/6] Add help when called without args --- commands/FBDebugCommands.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index fc81600..7e4d871 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -235,6 +235,10 @@ def run(self, arguments, options): args = arguments[0].strip().split(' ', 1) query = args[0] + if not query: + print 'Usage: findinstances []; Run `help findinstances`' + return + if len(args) > 1: predicate = args[1].strip() # Escape double quotes and backslashes. From 58659698604a9b7a2cd9eae6d14fac35597c501a Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Thu, 4 Jan 2018 12:24:40 -0800 Subject: [PATCH 6/6] Fix no-argument handling --- commands/FBDebugCommands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commands/FBDebugCommands.py b/commands/FBDebugCommands.py index 7e4d871..c57aa77 100644 --- a/commands/FBDebugCommands.py +++ b/commands/FBDebugCommands.py @@ -231,14 +231,14 @@ def run(self, arguments, options): if not self.loadChiselIfNecessary(): return + if len(arguments) == 0 or not arguments[0].strip(): + print 'Usage: findinstances []; Run `help findinstances`' + return + # Unpack the arguments by hand. The input is entirely in arguments[0]. args = arguments[0].strip().split(' ', 1) query = args[0] - if not query: - print 'Usage: findinstances []; Run `help findinstances`' - return - if len(args) > 1: predicate = args[1].strip() # Escape double quotes and backslashes.