1. 脚本桥接之选项和参数
创建自定义调试命令时,通常需要根据提供给命令的选项或参数稍微调整功能。一个自定义的LLDB
命令只能用一种方式来完成一项工作,那就太无趣了。
下面我们将探索如何将可选参数和必传参数传递给自定义命令,以更改自定义LLDB
脚本中的功能或逻辑。我们将继续使用在前一篇文章中创建的bar
命令。通过添加逻辑来处理脚本中的选项来丰富bar
命令,令其具有处理以下可选参数的逻辑:
- 非正则表达式搜索:使用
-n
或--non_regex
选项,使得bar
命令使用非正则表达式进行断点搜索。此选项不接受任何其他参数。 - 按模块筛选:使用
-m
或--module
选项,将只搜索特定模块中的断点。此选项需要一个指定模块名称的附加参数。 - 按条件停止:使用
-c
或--condition
选项,bar
命令将在当前函数执行完时判断给定条件。如果为真,将停止执行。如果为假,则将继续执行。此选项需要一个附加参数,且该参数是一条字符串代码,执行完后返回Objective-C BOOL类型。
我们将使用RWDevCon
项目。这个应用程序是RWDevcon会议的配套应用程序。每年都有一个传统,就是在雷·温德里奇生气之前,看看你能摸多少次他的肩膀。这里用到的项目是从84167c68
这个提交派生出来。可以在这里获得更新的版本:https://github.com/raywenderlich/RWDevCon-App。
不需要探索任何源代码。借助bar
命令,将能够使用智能断点查询探索我们感兴趣不同的项目。但在能够做到这一点之前,先来谈谈如何使这个bar
命令更加强大。
Python的optparse模块
我们拥有Python
及其模块的全部功能,可以随意使用。Python 2.7
附带的三个有用的模块,在解析选项和参数时非常值得研究:getopt
、optparse
和argparse
。
getopt
是一种底层的操作。optparse
正逐渐退出历史舞台,因为它在Python 2.7
之后就被弃用了。不幸的是,argparse
主要设计为与Python
的sys.argv
一起使用。然而,Python LLDB
命令脚本无法使用sys.argv
。这意味着optparse
将是我们唯一的选项。Facebook
的Chisel
、苹果自己定制的LLDB
脚本都使用这个模块。所以,它实际上是解析参数的标准方式。
optparse
模块将允许我们定义OptionParser
类型的实例,OptionParser
是一个负责解析所有参数的类。要使这个类工作,需要声明我们的命令支持哪些参数和选项。因为可选参数可能接受,也可能不接受该特定选项的附加值。比如
some_command woot -b 34 -a "hello world"
这个命令名为some_command
。但是传递给这个命令的参数和选项是什么?如果没有为解析器提供任何上下文,则此语句是不明确的。解析器不知道-b
或-a
选项是否应该接受该选项的参数。
例如,解析器可能认为这个命令传递了三个参数:[woot
,34
,hello world
],还有两个选项-b
,-a
,没有参数。但是,如果解析器希望-b
和-a
接受参数。那么解析器将为您提供参数[woot
]、-b
选项的34
和-a
的hello world
。
添加不带参数的选项
你需要告诉你的解析器需要什么参数。我们先来添加第一个选项,该选项将改变bar
命令的功能,以便在不使用正则表达式的情况下应用SBBreakpoint
,而使用普通表达式。
此参数最终将由布尔值表示,因此此选项不需要参数。此选项的存在与否是确定布尔值所需要的所有信息。如果这个参数存在,那么它为真;否则为假。
值得注意的是,一些脚本作者会设计一个鼓励你设置布尔值的选项。该选项显式传入布尔值。如果未提供该选项,则默认为True
或False
。
//比如这个命令
some_command -f
//上面的命令等价于
some_command -f1
那并不是我的风格。但是如果你希望更多的人使用这个脚本,那么可能需要考虑这个设计。因为它为用户提供了更明确的意图。
打开BreakAfterRegex.py
,并导入optparse
和shlex
模块。optparse
是刚刚介绍的模块,包含OptionParser
类,用于分析命令的任何额外输入。shlex
模块有一个很好的Python
函数,方便地分割为命令提供的参数,同时保持字符串参数的完整性。
import shlex
command = '"hello world" "2nd parameter" 34' shlex.split(command)
['hello world', '2nd parameter', '34']
在BreakAfterRegex.py的最底部创建以下方法:
def generateOptionParser():
usage = "usage: %prog [options] breakpoint_query\n" +\
"Use 'bar -h' for option desc"
#1
parser = optparse.OptionParser(usage=usage, prog='bar')
#2
parser.add_option("-n", "--non_regex",
#3
action="store_true",
#4
default=False,
#5
dest="non_regex",
#6
help="Use a non-regex breakpoint instead")
#7
return parser
- 创建
OptionParser
实例并为其提供usage
参数和prog
参数。如果使用错了,给解析器一个不知道如何处理的参数,就会显示用法。prog
选项用于处理函数的名字。
我们需要设置这个参数。因为它解决了一个奇怪的小问题,允许我们在运行-h
或--help
选项时,可以获取自定义命令的所有支持选项。如果prog
不在其中,-h
命令将无法正常工作。 - 在解析器中添加
--non_regex
或-n
参数。 -
action
参数,指明了如果提供了该参数的行为。store_true
意思是,如果提供了该参数,那么解析器就保存True
。 - 参数默值为
False
。即如果未提供此选项,则保存值为False
。 -
dest
参数,指明了OptionParser
解析输入时的属性名称non_regex
。例如command_args = shlex.split(command) (options, args) = parser.parse_args(command_args) options.non_regex
parse_args
方法生成一个Python
元组,其中包含一个选项列表和一个参数列表。options
变量将包含non_regex
属性。 -
help
参数,将提供帮助文档。可以使用--help
选项获取所有参数及其信息。例如,如果在bar
命令中正确设置了此选项,则只需键入bar -h
即可查看所有选项及其操作的列表。 - 将返回
OptionParser
的实例。
来到breakAfterRegex
函数的开头。删除以下两行:
target = debugger.GetSelectedTarget()
breakpoint = target.BreakpointCreateByRegex(command)
然后在删除的地方加入:
'''Creates a regular expression breakpoint and adds it. Once the breakpoint is hit, control will step out of the current function and print the return value. Useful for stopping on getter/accessor/initialization methods
'''
#1
command = command.replace('\\', '\\\\')
#2
command_args = shlex.split(command, posix=False)
#3
parser = generateOptionParser()
#4
try:
#5
(options, args) = parser.parse_args(command_args)
except:
result.SetError(parser.usage)
return
target = debugger.GetSelectedTarget()
#6
clean_command = shlex.split(args[0])[0]
#7
if options.non_regex:
breakpoint = target.BreakpointCreateByName(clean_command)
else:
breakpoint = target.BreakpointCreateByRegex(clean_command)
- 我们输入在终端输入
\'
时,实际表示'
。我们需要对命令中的\
再转义一次。 - 传递到自定义
LLDB
脚本中的命令参数是一个字符串,包含所有输入参数。我们把这个变量传递到shlex.split
方法中获得字符串列表。posix=False
有助于处理包含的特殊字符(如破折号);否则,OptionParse
将错误地假设这是一个传入的选项。这非常重要。因为
Objective-C
在实例方法中有破折号,所以我们不希望破折号被错误地解释为一个选项! - 使用
generateOptionParser
函数,创建一个解析器来处理命令的输入。 - 解析输入很可能出错。
Python
通常的错误处理方法是抛出异常。如果optparse
发现一个错误,它就会抛出这个错误。如果在脚本中没有捕捉异常,LLDB
将退出。因此,将解析包含在try-except
块中,防止LLDB
因输入错误而退出。 - 将command_args变量传递给
OptionParser
类的parse_args
方法,并将接收一个元组作为返回值。这个元组由两个值组成:options
可选参数,它包含所有的选项参数(目前只有non_regex
选项);args
必选参数,这些参数由解析器解析的任何其他输入组成。 - 获取第一个捕获的参数(断点查询),并将其分配给一个名为
clean_command
的变量。还记得第2条中提到的posix=False
吗?该逻辑将保持捕获的参数周围的引号,从而保持精确的语法。如果没有posix=False
,您可以只使用args[0]
,但是如果不能在正则表达式搜索中使用转义\
,将丧失正则表达式中的很多功能。 - 检查
options.non_regex
的布尔值。如果为True
,则在SBTarget
中执行BreakpointCreateByName
方法以实现非正则表达式断点。如果non_regex
为False
(可能是默认值),则脚本将使用正则表达式搜索。所以,我们需要做的只是将-n
添加到bar
命令的输入中,就可以使non_regex
为True
。
测试一下
创建一个符号断点。符号为getenv
,添加两个命令br dis 1
和bar -n "-[NSUserDefaults(NSUserDefaults) objectForKey:]"
,勾选自动继续。
我们在getenv
C函数上创建了一个符号断点。在LLDB
中,如果想在自己的代码开始执行之前设置断点,或者在逆向别人的app之前设置断点,那么这是hook
任何逻辑的好地方。
我不喜欢使用main
,因为许多可执行文件都包含main
函数,并且主可执行文件的main
符号可能在可执行文件的生产版本中被剥离。但我们知道getenv
肯定会命中,而且会在我们的代码开始运行之前被命中。
第一个动作是去掉getenv
断点。我们不是在删除它,只是在禁用它。之所以使用1
,是因为这是我们的第一个断点,其ID
为1
。
在NSUSerDefaults
的objectForKey:
方法上创建一个非正则表达式断点。我们希望这个方法返回一个id
或nil
,所以让我们看看这个RWDevCon
应用程序正在读取(或写入)NSUserDefaults
什么东西。
运行这个app。如果没有深入研究这个应用程序,可能会得到很多nil
。意味着这个方法肯定在被这个应用程序中的某些代码读取。
添加带参数的选项
我们来添加下一个选项--module
,用于指定在哪个模块进行正则表达式的查询。
在BreakAfterRegex.py
脚本中,回到generationParser
函数,在返回parser
之前添加以下代码:
parser.add_option("-m", "--module",
action="store",
default=None,
dest="module",
help="Filter a breakpoint by only searching within a specified Module")
回到breakAfterRegex
函数将下面两行替换
if options.non_regex:
breakpoint = target.BreakpointCreateByName(clean_command)
else:
breakpoint = target.BreakpointCreateByRegex(clean_command)
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
if options.non_regex:
breakpoint = target.BreakpointCreateByName(clean_command, options.module)
else:
breakpoint = target.BreakpointCreateByRegex(clean_command, options.module)
我们来看看到底可以传入那些参数。
(lldb) script help (lldb.SBTarget.BreakpointCreateByRegex)
Help on function BreakpointCreateByRegex in module lldb:
BreakpointCreateByRegex(self, *args)
BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex, str const * module_name=None) -> SBBreakpoint
BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex) -> SBBreakpoint
BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex, lldb::LanguageType symbol_language, SBFileSpecList module_list, SBFileSpecList comp_unit_list) -> SBBreakpoint
我们现在使用的便是这个函数。
BreakpointCreateByRegex(SBTarget self, str const * symbol_name_regex, str const * module_name=None) -> SBBreakpoint
注意最后一个参数:module_name=None
。这是一个可选参数,意味着如果不提供参数,模块名默认值为None
。当OptionParser
实例解析选项时,怎么都可以将options.module
提供给BreakpointCreateByRegex方法。因为
options.module的默认值将为
None`,与不使用额外参数效果相同。
我们来试试。我们的第二个动作改为bar @objc.*.init -m RWDevCon
。在所有继承自OC对象的Swift对象初始化的地方加上了一个断点。我们限制这个断点只查询RWDevCon模块。
运行一下。我们会发现很多__ObjC.NSEntityDescription
的命中。这意味着这个项目使用了Swift写了很多CoreData
逻辑。清除屏幕,并点击包含研讨会(即没有午餐或派对日期)的项目。在控制台,我们得到了一个继承自OC对象的Swift对象列表。搜索名为Person的类。
将地址复制到剪贴板中。在粘贴到地址之前,我们看一下Person
类实现的所有方法。methods Person
命令将列出OC运行时知道的Person
类实现的所有方法。这个类实现的Swift方法仍然有可能是OC运行时不知道的。
(lldb) methods Person
<Person: 0x105a35460>:
in Person:
Properties:
@property (nonatomic, copy) NSString* first; (@dynamic first;)
@property (nonatomic, copy) NSString* last; (@dynamic last;)
@property (nonatomic, copy) NSString* bio; (@dynamic bio;)
@property (nonatomic, copy) NSString* twitter; (@dynamic twitter;)
@property (nonatomic, copy) NSString* identifier; (@dynamic identifier;)
@property (nonatomic) BOOL active; (@dynamic active;)
@property (nonatomic, retain) NSSet* sessions; (@dynamic sessions;)
Instance Methods:
- (id) initWithEntity:(id)arg1 insertIntoManagedObjectContext:(id)arg2; (0x1059fe0a0)
(NSManagedObject ...)
向断点回调函数传递参数
下面我们创建-c
或--condition
参数的解析。在generateOptionParser
返回值之前加入:
parser.add_option("-c", "--condition",
action="store",
default=None,
dest="condition",
help="Only stop if the expression matches True. Can reference return value through 'obj'. Obj-C only.")
那么我们怎么把这个参数传递给回调函数breakpointHandler
呢?答案是我们将使用Python字典来传递这个选项。另外,断点的好处是,不管创建或删除多少个断点,每个断点在每次运行会话中都有一个唯一的标识ID
。可以将断点ID
设置为键,并将该断点的选项设置为值。来到BreakAfterRegex.py
的顶部,并在import
语句的正下方添加以下逻辑:
#1
class BarOptions(object):
#2
optdict = {}
#3
@staticmethod
def addOptions(options, breakpoint):
key = str(breakpoint.GetID())
BarOptions.optdict[key] = options
- 声明一个名为
BarOptions
的类,该类继承自object
。可以把object
看作是Python
中的NSObject
。
2。声明一个名为optdict
的类变量。如果要声明实例变量,它必须在init
函数中。因为只使用这个类变量,所以不会为这个类设置任何初始化方法。 - 声明一个名为
addOptions
的类方法。通过断点的ID
作为键来保存options
。
来到breakAfterRegex
并在回调函数上面加入:
BarOptions.addOptions(options, breakpoint)
在BreakAfterRegex.py
的最下面加入新的函数。
def evaluateCondition(debugger, condition):
'''Returns True or False based upon the supplied condition. You can reference the NSObject through "obj"'''
#1
res = lldb.SBCommandReturnObject()
interpreter = debugger.GetCommandInterpreter()
target = debugger.GetSelectedTarget()
#2
expression = 'expression -lobjc -O -- id obj = ((id){}); ((BOOL) {})'.format(getRegisterString(target), condition)
interpreter.HandleCommand(expression, res)
#3
if res.GetError():
print(condition)
print('*' * 80 + '\n' + res.GetError() + '\ncondition:' + condition)
return False
elif res.HasResult():
#4
retval = res.GetOutput()
#5
if 'YES' in retval:
return True
#6
return False
- 创建一个
SBCommandReturnObject
来处理传递过来的condition
参数。 - 创建并执行传入的自定义表达式。
注意:我们声明了实例变量
obj
,并将其从返回寄存器强制转换为类型id
。这样,我们就可以方便地将返回值引用为obj
,而不是硬件特定的寄存器。再将提供的表达式返回值转换为Objective-C BOOL
,它将返回YES
或NO
输出。 - 如果返回值包含错误,则打印出错误。
注意:如果函数返回
True
,SBBreakpoint
回调函数断点处理程序将停止执行。如果返回的不是True
(即False
、None
或no return
),则执行不会停止。 - 把结果赋值给
retval
变量。 - 将输出与预期结果进行比较。如果表达式的计算结果为
YES
,则暂停执行。 - 如果执行返回
NO
,则通过返回False
继续执行。
最后breakpointHandler
函数,在thread.StepOut()
下面添加:
#1
key = str(bp_loc.GetBreakpoint().GetID())
#2
options = BarOptions.optdict[key]
#3
if options.condition:
#4
condition = shlex.split(options.condition)[0]
#5
return evaluateCondition(debugger, condition)
-
bp_loc
是SBBreakpointLocation
类型。这个类允许您通过GetBreakpoint
方法引用初始的SBBreakpoint
,就可以拿到ID
了。然后需要将此数字转换为字符串并将其分配给变量键。 - 从类属性
optict
中获取对应的值,并将其分配给变量options
。 - 检查
options
变量是否为空。 - 获取
options.condition
中的条件语句。 - 调用
evaluateCondition
函数。返回函数的返回值,该值将影响是否应停止执行。
我们来试一试。并把第二个动作改为bar NSURL\(.*init -c '\[\[$obj absoluteString\] containsString:@\"amazon\"\]'
。
现在断点只会停在满足条件的断点上了。
网友评论