commands/FBDebugCommands.py (394 lines of code) (raw):
#!/usr/bin/python
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# Can be removed when Python 2 support is removed.
from __future__ import print_function
import os
import re
import sys
import fbchisellldbbase as fb
import fbchisellldbobjcruntimehelpers as objc
import lldb
def lldbcommands():
return [
FBWatchInstanceVariableCommand(),
FBFrameworkAddressBreakpointCommand(),
FBMethodBreakpointCommand(),
FBMemoryWarningCommand(),
FBFindInstancesCommand(),
FBMethodBreakpointEnableCommand(),
FBMethodBreakpointDisableCommand(),
FBHeapFromCommand(),
FBSequenceCommand(),
]
class FBWatchInstanceVariableCommand(fb.FBCommand):
def name(self):
return "wivar"
def description(self):
return "Set a watchpoint for an object's instance variable."
def args(self):
return [
fb.FBCommandArgument(
arg="object", type="id", help="Object expression to be evaluated."
),
fb.FBCommandArgument(
arg="ivarName", help="Name of the instance variable to watch."
),
]
def run(self, arguments, options):
commandForObject, ivarName = arguments
objectAddress = int(fb.evaluateObjectExpression(commandForObject), 0)
ivarOffsetCommand = '(ptrdiff_t)ivar_getOffset((void*)object_getInstanceVariable((id){}, "{}", 0))'.format(
objectAddress, ivarName
)
ivarOffset = int(fb.evaluateExpression(ivarOffsetCommand), 0)
# A multi-statement command allows for variables scoped to the command,
# not permanent in the session like $variables.
ivarSizeCommand = (
"unsigned int size = 0;"
'char *typeEncoding = (char *)ivar_getTypeEncoding((void*)class_getInstanceVariable((Class)object_getClass((id){}), "{}"));'
"(char *)NSGetSizeAndAlignment(typeEncoding, &size, 0);"
"size"
).format(objectAddress, ivarName)
ivarSize = int(fb.evaluateExpression(ivarSizeCommand), 0)
error = lldb.SBError()
watchpoint = lldb.debugger.GetSelectedTarget().WatchAddress(
objectAddress + ivarOffset, ivarSize, False, True, error
)
if error.Success():
print(
"Remember to delete the watchpoint using: watchpoint delete {}".format(
watchpoint.GetID()
)
)
else:
print("Could not create the watchpoint: {}".format(error.GetCString()))
class FBFrameworkAddressBreakpointCommand(fb.FBCommand):
def name(self):
return "binside"
def description(self):
return "Set a breakpoint for a relative address within the framework/library that's currently running. This does the work of finding the offset for the framework/library and sliding your address accordingly."
def args(self):
return [
fb.FBCommandArgument(
arg="address",
type="string",
help="Address within the currently running framework to set a breakpoint on.",
)
]
def run(self, arguments, options):
library_address = int(arguments[0], 0)
address = int(
lldb.debugger.GetSelectedTarget()
.GetProcess()
.GetSelectedThread()
.GetSelectedFrame()
.GetModule()
.ResolveFileAddress(library_address)
)
lldb.debugger.HandleCommand("breakpoint set --address {}".format(address))
class FBMethodBreakpointCommand(fb.FBCommand):
def name(self):
return "bmessage"
def description(self):
return "Set a breakpoint for a selector on a class, even if the class itself doesn't override that selector. It walks the hierarchy until it finds a class that does implement the selector and sets a conditional breakpoint there."
def args(self):
return [
fb.FBCommandArgument(
arg="expression",
type="string",
help='Expression to set a breakpoint on, e.g. "-[MyView setFrame:]", "+[MyView awesomeClassMethod]" or "-[0xabcd1234 setFrame:]"',
)
]
def run(self, arguments, options):
expression = arguments[0]
methodPattern = re.compile(
r"""
(?P<scope>[-+])?
\[
(?P<target>.*?)
(?P<category>\(.+\))?
\s+
(?P<selector>.*)
\]
""",
re.VERBOSE,
)
match = methodPattern.match(expression)
if not match:
print("Failed to parse expression. Do you even Objective-C?!")
return
expressionForSelf = objc.functionPreambleExpressionForSelf()
if not expressionForSelf:
arch = objc.currentArch()
print(
"Your architecture, {}, is truly fantastic. However, I don't currently support it.".format(
arch
)
)
return
methodTypeCharacter = match.group("scope")
classNameOrExpression = match.group("target")
category = match.group("category")
selector = match.group("selector")
methodIsClassMethod = methodTypeCharacter == "+"
if methodIsClassMethod:
methodTypeCharacter = "\+"
else:
# The default is instance method, and methodTypeCharacter
# may not actually be '-'.
methodTypeCharacter = "-"
targetIsClass = False
targetObject = fb.evaluateObjectExpression(
"({})".format(classNameOrExpression), False
)
if not targetObject:
# If the expression didn't yield anything then it's likely a class.
# Assume it is. We will check again that the class does actually
# exist anyway.
targetIsClass = True
targetObject = fb.evaluateObjectExpression(
"[{} class]".format(classNameOrExpression), False
)
targetClass = fb.evaluateObjectExpression(
"[{} class]".format(targetObject), False
)
if not targetClass or int(targetClass, 0) == 0:
print(
'Couldn\'t find a class from the expression "{}". Did you typo?'.format(
classNameOrExpression
)
)
return
if methodIsClassMethod:
targetClass = objc.object_getClass(targetClass)
found = False
nextClass = targetClass
while not found and int(nextClass, 0) > 0:
if classItselfImplementsSelector(nextClass, selector):
found = True
else:
nextClass = objc.class_getSuperclass(nextClass)
if not found:
print(
"There doesn't seem to be an implementation of {} in the class hierarchy. Made a boo boo with the selector name?".format(
selector
)
)
return
breakpointClassName = objc.class_getName(nextClass)
formattedCategory = category if category else ""
breakpointFullName = "{}[{}{} {}]".format(
methodTypeCharacter, breakpointClassName, formattedCategory, selector
)
if targetIsClass:
breakpointCondition = "(void*)object_getClass({}) == {}".format(
expressionForSelf, targetClass
)
else:
breakpointCondition = "(void*){} == {}".format(
expressionForSelf, targetObject
)
print(
"Setting a breakpoint at {} with condition {}".format(
breakpointFullName, breakpointCondition
)
)
if category:
lldb.debugger.HandleCommand(
'breakpoint set --skip-prologue false --fullname "{}" --condition "{}"'.format(
breakpointFullName, breakpointCondition
)
)
else:
breakpointPattern = r"{}\[{}(\(.+\))? {}\]".format(
methodTypeCharacter, breakpointClassName, selector
)
lldb.debugger.HandleCommand(
'breakpoint set --skip-prologue false --func-regex "{}" --condition "{}"'.format(
breakpointPattern, breakpointCondition
)
)
def classItselfImplementsSelector(klass, selector):
thisMethod = objc.class_getInstanceMethod(klass, selector)
if thisMethod == 0:
return False
superklass = objc.class_getSuperclass(klass)
superMethod = objc.class_getInstanceMethod(superklass, selector)
if thisMethod == superMethod:
return False
else:
return True
class FBMemoryWarningCommand(fb.FBCommand):
def name(self):
return "mwarning"
def description(self):
return "simulate a memory warning"
def run(self, arguments, options):
fb.evaluateEffect(
"[[UIApplication sharedApplication] performSelector:@selector(_performMemoryWarning)]"
)
def switchBreakpointState(expression, on):
expression_pattern = re.compile(r"{}".format(expression), re.I)
target = lldb.debugger.GetSelectedTarget()
for breakpoint in target.breakpoint_iter():
if breakpoint.IsEnabled() != on and (
expression_pattern.search(str(breakpoint))
):
print(str(breakpoint))
breakpoint.SetEnabled(on)
for location in breakpoint:
if location.IsEnabled() != on and (
expression_pattern.search(str(location))
or expression == hex(location.GetLoadAddress())
):
print(str(location))
location.SetEnabled(on)
class FBMethodBreakpointEnableCommand(fb.FBCommand):
def name(self):
return "benable"
def description(self):
return """
Enable a set of breakpoints for a regular expression
Examples:
* benable ***address***
benable 0x0000000104514dfc
benable 0x183e23564
#use `benable *filename*` to switch all breakpoints in this file to `enable`
benable SUNNetService.m
#use `benable ***module(AppName)***` to switch all breakpoints in this module to `enable`
benable UIKit
benable Foundation
"""
def args(self):
return [
fb.FBCommandArgument(
arg="expression", type="string", help="Expression to enable breakpoint"
)
]
def run(self, arguments, options):
expression = arguments[0]
switchBreakpointState(expression, True)
class FBMethodBreakpointDisableCommand(fb.FBCommand):
def name(self):
return "bdisable"
def description(self):
return """
Disable a set of breakpoints for a regular expression
Examples:
* bdisable ***address***
bdisable 0x0000000104514dfc
bdisable 0x183e23564
#use `bdisable *filename*` to switch all breakpoints in this file to `disable`
bdisable SUNNetService.m
#use `bdisable ***module(AppName)***` to switch all breakpoints in this module to `disable`
bdisable UIKit
bdisable Foundation
"""
def args(self):
return [
fb.FBCommandArgument(
arg="expression", type="string", help="Expression to disable breakpoint"
)
]
def run(self, arguments, options):
expression = arguments[0]
switchBreakpointState(expression, False)
class FBFindInstancesCommand(fb.FBCommand):
def name(self):
return "findinstances"
def args(self):
return [
fb.FBCommandArgument(arg="type", help="Class or protocol name"),
fb.FBCommandArgument(
arg="query",
default=" ", # space is a hack to mark optional
help="Query expression, uses NSPredicate syntax",
),
]
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 lex(self, commandLine):
# Can't use default shlex splitting because it strips quotes, which results
# in invalid NSPredicate syntax. Split the input into type and rest (query).
return commandLine.split(" ", 1)
def run(self, arguments, options):
if not self.loadChiselIfNecessary():
return
if len(arguments) == 0 or not arguments[0].strip():
print(
"Usage: findinstances <classOrProtocol> [<predicate>]; Run `help findinstances`"
)
return
query = arguments[0]
predicate = arguments[1].strip()
# Escape double quotes and backslashes.
predicate = re.sub('([\\"])', r"\\\1", predicate)
call = '(void)PrintInstances("{}", "{}")'.format(query, predicate)
fb.evaluateExpressionValue(call)
def loadChiselIfNecessary(self):
target = lldb.debugger.GetSelectedTarget()
symbol_contexts = target.FindSymbols("PrintInstances", lldb.eSymbolTypeCode)
if any(ctx.symbol.IsValid() for ctx in symbol_contexts):
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
# `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()")
if errno == 50:
# KERN_CODESIGN_ERROR from <mach/kern_return.h>
print("Error loading Chisel: Code signing failure; Must re-run codesign")
elif error.unsigned != 0:
print("Error loading Chisel: " + error.summary)
elif errno != 0:
error = fb.evaluateExpressionValue("(char*)strerror({})".format(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):
# 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")
class FBHeapFromCommand(fb.FBCommand):
def name(self):
return "heapfrom"
def description(self):
return "Show all nested heap pointers contained within a given variable."
def run(self, arguments, options):
# This command is like `expression --synthetic-type false`,
# except only showing nested heap references.
var = self.context.frame.var(arguments[0])
if not var or not var.IsValid():
self.result.SetError('No variable named "{}"'.format(arguments[0]))
return
# Use the actual underlying structure of the variable,
# not the human friendly (synthetic) one.
root = var.GetNonSyntheticValue()
# Traversal of SBValue tree to get leaf nodes, which is where heap
# pointers will be.
leafs = []
queue = [root]
while queue:
node = queue.pop(0)
if node.num_children == 0:
leafs.append(node)
else:
queue += [node.GetChildAtIndex(i) for i in range(node.num_children)]
pointers = {}
for node in leafs:
# Assumption: an addr that has no value means a pointer.
if node.addr and not node.value:
pointers[node.load_addr] = node.path
options = lldb.SBExpressionOptions()
options.SetLanguage(lldb.eLanguageTypeC)
def isHeap(addr):
lookup = "(int)malloc_size({})".format(addr)
return self.context.frame.EvaluateExpression(lookup, options).unsigned != 0
allocations = (addr for addr in pointers if isHeap(addr))
for addr in allocations:
print(
"0x{addr:x} {path}".format(addr=addr, path=pointers[addr]),
file=self.result,
)
if not allocations:
print("No heap addresses found", file=self.result)
class FBSequenceCommand(fb.FBCommand):
def name(self):
return "sequence"
def description(self):
return "Run commands in sequence, stopping on any error."
def lex(self, commandLine):
return [command.strip() for command in commandLine.split(";")]
def run(self, arguments, options):
# arguments contains the raw command first, followed by the split commands.
if len(arguments) == 1:
return
commands = filter(None, arguments[1:])
interpreter = lldb.debugger.GetCommandInterpreter()
# Complete one command before running the next one in the sequence. Disable
# async to do this. Also, save the current async value to restore it later.
asyncFlag = lldb.debugger.GetAsync()
lldb.debugger.SetAsync(False)
for command in commands[:-1]:
success = self.run_command(interpreter, command)
if not success:
lldb.debugger.SetAsync(asyncFlag)
return
# Restore original async value.
lldb.debugger.SetAsync(asyncFlag)
# If the last command is `continue`, call Continue() on the process
# instead. This is done because HandleCommand('continue') has strange
# behavior, while calling Continue() works as expected.
last = commands[-1]
if self.is_continue(interpreter, last):
self.context.process.Continue()
else:
self.run_command(interpreter, last)
def run_command(self, interpreter, command):
ret = lldb.SBCommandReturnObject()
interpreter.HandleCommand(command, ret)
if ret.GetOutput():
print(ret.GetOutput().strip(), file=self.result)
if ret.Succeeded():
return True
self.result.SetError(ret.GetError())
self.result.SetStatus(ret.GetStatus())
return False
def is_continue(self, interpreter, command):
ret = lldb.SBCommandReturnObject()
interpreter.ResolveCommand(command, ret)
return ret.GetOutput() == "process continue"