(十一)自定义LLDB命令 选项和参数

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附带的三个有用的模块,在解析选项和参数时非常值得研究:getoptoptparseargparse

getopt是一种底层的操作。optparse正逐渐退出历史舞台,因为它在Python 2.7之后就被弃用了。不幸的是,argparse主要设计为与Pythonsys.argv一起使用。然而,Python LLDB命令脚本无法使用sys.argv。这意味着optparse将是我们唯一的选项。FacebookChisel、苹果自己定制的LLDB脚本都使用这个模块。所以,它实际上是解析参数的标准方式。

optparse模块将允许我们定义OptionParser类型的实例,OptionParser是一个负责解析所有参数的类。要使这个类工作,需要声明我们的命令支持哪些参数和选项。因为可选参数可能接受,也可能不接受该特定选项的附加值。比如

some_command woot -b 34 -a "hello world"

这个命令名为some_command。但是传递给这个命令的参数和选项是什么?如果没有为解析器提供任何上下文,则此语句是不明确的。解析器不知道-b-a选项是否应该接受该选项的参数。

例如,解析器可能认为这个命令传递了三个参数:[woot34hello world],还有两个选项-b-a,没有参数。但是,如果解析器希望-b-a接受参数。那么解析器将为您提供参数[woot]、-b选项的34-ahello world

添加不带参数的选项

你需要告诉你的解析器需要什么参数。我们先来添加第一个选项,该选项将改变bar命令的功能,以便在不使用正则表达式的情况下应用SBBreakpoint,而使用普通表达式。

此参数最终将由布尔值表示,因此此选项不需要参数。此选项的存在与否是确定布尔值所需要的所有信息。如果这个参数存在,那么它为真;否则为假。

值得注意的是,一些脚本作者会设计一个鼓励你设置布尔值的选项。该选项显式传入布尔值。如果未提供该选项,则默认为TrueFalse

//比如这个命令
some_command -f
//上面的命令等价于
some_command -f1

那并不是我的风格。但是如果你希望更多的人使用这个脚本,那么可能需要考虑这个设计。因为它为用户提供了更明确的意图。

打开BreakAfterRegex.py,并导入optparseshlex模块。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
  1. 创建OptionParser实例并为其提供usage参数和prog参数。如果使用错了,给解析器一个不知道如何处理的参数,就会显示用法。prog选项用于处理函数的名字。
    我们需要设置这个参数。因为它解决了一个奇怪的小问题,允许我们在运行-h--help选项时,可以获取自定义命令的所有支持选项。如果prog不在其中,-h命令将无法正常工作。
  2. 在解析器中添加--non_regex-n参数。
  3. action参数,指明了如果提供了该参数的行为。store_true意思是,如果提供了该参数,那么解析器就保存True
  4. 参数默值为False。即如果未提供此选项,则保存值为False
  5. 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属性。
  6. help参数,将提供帮助文档。可以使用--help选项获取所有参数及其信息。例如,如果在bar命令中正确设置了此选项,则只需键入bar -h即可查看所有选项及其操作的列表。
  7. 将返回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)
  1. 我们输入在终端输入\'时,实际表示'。我们需要对命令中的\再转义一次。
  2. 传递到自定义LLDB脚本中的命令参数是一个字符串,包含所有输入参数。我们把这个变量传递到shlex.split方法中获得字符串列表。posix=False有助于处理包含的特殊字符(如破折号);否则,OptionParse将错误地假设这是一个传入的选项。

    这非常重要。因为Objective-C在实例方法中有破折号,所以我们不希望破折号被错误地解释为一个选项!

  3. 使用generateOptionParser函数,创建一个解析器来处理命令的输入。
  4. 解析输入很可能出错。Python通常的错误处理方法是抛出异常。如果optparse发现一个错误,它就会抛出这个错误。如果在脚本中没有捕捉异常,LLDB将退出。因此,将解析包含在try-except块中,防止LLDB因输入错误而退出。
  5. 将command_args变量传递给OptionParser类的parse_args方法,并将接收一个元组作为返回值。这个元组由两个值组成:options可选参数,它包含所有的选项参数(目前只有non_regex选项);args必选参数,这些参数由解析器解析的任何其他输入组成。
  6. 获取第一个捕获的参数(断点查询),并将其分配给一个名为clean_command的变量。还记得第2条中提到的posix=False吗?该逻辑将保持捕获的参数周围的引号,从而保持精确的语法。如果没有posix=False,您可以只使用args[0],但是如果不能在正则表达式搜索中使用转义\,将丧失正则表达式中的很多功能。
  7. 检查options.non_regex的布尔值。如果为True,则在SBTarget中执行BreakpointCreateByName方法以实现非正则表达式断点。如果non_regexFalse(可能是默认值),则脚本将使用正则表达式搜索。所以,我们需要做的只是将-n添加到bar命令的输入中,就可以使non_regexTrue
测试一下

创建一个符号断点。符号为getenv,添加两个命令br dis 1bar -n "-[NSUserDefaults(NSUserDefaults) objectForKey:]",勾选自动继续。

符号断点

我们在getenvC函数上创建了一个符号断点。在LLDB中,如果想在自己的代码开始执行之前设置断点,或者在逆向别人的app之前设置断点,那么这是hook任何逻辑的好地方。

我不喜欢使用main,因为许多可执行文件都包含main函数,并且主可执行文件的main符号可能在可执行文件的生产版本中被剥离。但我们知道getenv肯定会命中,而且会在我们的代码开始运行之前被命中。

第一个动作是去掉getenv断点。我们不是在删除它,只是在禁用它。之所以使用1,是因为这是我们的第一个断点,其ID1

NSUSerDefaultsobjectForKey:方法上创建一个非正则表达式断点。我们希望这个方法返回一个idnil,所以让我们看看这个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

将地址复制到剪贴板中。在粘贴到地址之前,我们看一下Person类实现的所有方法。methods Person命令将列出OC运行时知道Person类实现的所有方法。这个类实现的Swift方法仍然有可能是OC运行时不知道的。

(lldb) methods Person
:
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
  1. 声明一个名为BarOptions的类,该类继承自object。可以把object看作是Python中的NSObject
    2。声明一个名为optdict的类变量。如果要声明实例变量,它必须在init函数中。因为只使用这个类变量,所以不会为这个类设置任何初始化方法。
  2. 声明一个名为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
  1. 创建一个SBCommandReturnObject来处理传递过来的condition参数。
  2. 创建并执行传入的自定义表达式。

    注意:我们声明了实例变量obj,并将其从返回寄存器强制转换为类型id。这样,我们就可以方便地将返回值引用为obj,而不是硬件特定的寄存器。再将提供的表达式返回值转换为Objective-C BOOL,它将返回YESNO输出。

  3. 如果返回值包含错误,则打印出错误。

    注意:如果函数返回TrueSBBreakpoint回调函数断点处理程序将停止执行。如果返回的不是True(即FalseNoneno return),则执行不会停止。

  4. 把结果赋值给retval变量。
  5. 将输出与预期结果进行比较。如果表达式的计算结果为YES,则暂停执行。
  6. 如果执行返回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)
  1. bp_locSBBreakpointLocation类型。这个类允许您通过GetBreakpoint方法引用初始的SBBreakpoint,就可以拿到ID了。然后需要将此数字转换为字符串并将其分配给变量键。
  2. 从类属性optict中获取对应的值,并将其分配给变量options
  3. 检查options变量是否为空。
  4. 获取options.condition中的条件语句。
  5. 调用evaluateCondition函数。返回函数的返回值,该值将影响是否应停止执行。

我们来试一试。并把第二个动作改为bar NSURL\(.*init -c '\[\[$obj absoluteString\] containsString:@\"amazon\"\]'

断点

现在断点只会停在满足条件的断点上了。

你可能感兴趣的:((十一)自定义LLDB命令 选项和参数)